package com.syncflow.data.providers.onedrive

import com.syncflow.data.providers.CloudProvider
import com.syncflow.domain.model.CloudAccount
import com.syncflow.domain.model.RemoteFile
import kotlinx.serialization.json.*
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.InputStream
import java.io.OutputStream
import java.time.Instant

class OneDriveProvider(private val account: CloudAccount) : CloudProvider {

    private val token: String by lazy {
        Json.parseToJsonElement(account.credentialJson).jsonObject["access_token"]?.jsonPrimitive?.content ?: ""
    }
    private val client = OkHttpClient()
    private val base = "https://graph.microsoft.com/v1.0/me/drive/root"

    private fun auth(url: String) = Request.Builder().url(url).header("Authorization", "Bearer $token")

    override suspend fun testConnection(): Result<Unit> = runCatching {
        auth("https://graph.microsoft.com/v1.0/me").build()
            .let { client.newCall(it).execute() }.use { resp -> if (!resp.isSuccessful) throw Exception("HTTP ${resp.code}") }
    }

    override suspend fun listFiles(remotePath: String): Result<List<RemoteFile>> = runCatching {
        val url = if (remotePath == "/" || remotePath.isBlank()) "$base/children" else "$base:${remotePath}:/children"
        auth("$url?\$select=id,name,folder,size,lastModifiedDateTime,eTag,file").build()
            .let { client.newCall(it).execute() }.use { resp ->
                val body = resp.body?.string() ?: throw Exception("HTTP ${resp.code}")
                if (!resp.isSuccessful) throw Exception("HTTP ${resp.code}: $body")
                Json.parseToJsonElement(body).jsonObject["value"]?.jsonArray
                    ?.map { it.jsonObject.toFile(remotePath) } ?: emptyList()
            }
    }

    override suspend fun uploadFile(localStream: InputStream, remotePath: String, sizeBytes: Long, onProgress: (Long) -> Unit): Result<RemoteFile> = runCatching {
        val bytes = localStream.readBytes()
        val url = "$base:${remotePath}:/content"
        auth(url).put(bytes.toRequestBody("application/octet-stream".toMediaType())).build()
            .let { client.newCall(it).execute() }.use { resp ->
                val body = resp.body?.string() ?: throw Exception("HTTP ${resp.code}")
                if (!resp.isSuccessful) throw Exception("HTTP ${resp.code}: $body")
                onProgress(bytes.size.toLong())
                Json.parseToJsonElement(body).jsonObject.toFile(remotePath.substringBeforeLast('/'))
            }
    }

    override suspend fun downloadFile(remotePath: String, destination: OutputStream, onProgress: (Long) -> Unit): Result<Unit> = runCatching {
        auth("$base:${remotePath}:/content").build()
            .let { client.newCall(it).execute() }.use { resp ->
                if (!resp.isSuccessful) throw Exception("HTTP ${resp.code}")
                var total = 0L
                resp.body?.byteStream()?.use { src ->
                    val buf = ByteArray(65536)
                    var n: Int
                    while (src.read(buf).also { n = it } != -1) { destination.write(buf, 0, n); total += n; onProgress(total) }
                }
            }
    }

    override suspend fun deleteFile(remotePath: String): Result<Unit> = runCatching {
        auth("$base:${remotePath}:").delete().build()
            .let { client.newCall(it).execute() }.use { resp -> if (!resp.isSuccessful && resp.code != 404) throw Exception("HTTP ${resp.code}") }
    }

    override suspend fun createDirectory(remotePath: String): Result<Unit> = runCatching {
        val name = remotePath.substringAfterLast('/')
        val parent = remotePath.substringBeforeLast('/').ifBlank { "/" }
        val parentUrl = if (parent == "/") "$base/children" else "$base:${parent}:/children"
        val body = """{"name":"$name","folder":{}}""".toRequestBody("application/json".toMediaType())
        auth(parentUrl).post(body).build()
            .let { client.newCall(it).execute() }.use { resp -> if (!resp.isSuccessful && resp.code != 409) throw Exception("HTTP ${resp.code}") }
    }

    override suspend fun getFileMetadata(remotePath: String): Result<RemoteFile> = runCatching {
        auth("$base:${remotePath}:?\$select=id,name,folder,size,lastModifiedDateTime,eTag,file").build()
            .let { client.newCall(it).execute() }.use { resp ->
                val body = resp.body?.string() ?: throw Exception("HTTP ${resp.code}")
                Json.parseToJsonElement(body).jsonObject.toFile(remotePath.substringBeforeLast('/'))
            }
    }

    override suspend fun moveFile(fromPath: String, toPath: String): Result<Unit> = runCatching {
        val newName = toPath.substringAfterLast('/')
        val body = """{"name":"$newName"}""".toRequestBody("application/json".toMediaType())
        auth("$base:${fromPath}:").method("PATCH", body).build()
            .let { client.newCall(it).execute() }.use { resp -> if (!resp.isSuccessful) throw Exception("HTTP ${resp.code}") }
    }

    private fun JsonObject.toFile(parentPath: String) = RemoteFile(
        path = "$parentPath/${get("name")?.jsonPrimitive?.content}".replace("//", "/"),
        name = get("name")?.jsonPrimitive?.content ?: "",
        isDirectory = containsKey("folder"),
        sizeBytes = get("size")?.jsonPrimitive?.content?.toLongOrNull() ?: 0L,
        modifiedAt = get("lastModifiedDateTime")?.jsonPrimitive?.content?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: Instant.EPOCH,
        etag = get("eTag")?.jsonPrimitive?.content,
        mimeType = get("file")?.jsonObject?.get("mimeType")?.jsonPrimitive?.content,
    )
}
