package com.syncflow.data.providers.dropbox

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 DropboxProvider(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 fun apiReq(url: String, argJson: JsonObject): Request =
        Request.Builder().url(url)
            .post(argJson.toString().toRequestBody("application/json".toMediaType()))
            .header("Authorization", "Bearer $token")
            .build()

    override suspend fun testConnection(): Result<Unit> = runCatching {
        val req = Request.Builder().url("https://api.dropboxapi.com/2/users/get_current_account")
            .post("null".toRequestBody("application/json".toMediaType()))
            .header("Authorization", "Bearer $token").build()
        client.newCall(req).execute().use { resp -> if (!resp.isSuccessful) throw Exception("HTTP ${resp.code}") }
    }

    override suspend fun listFiles(remotePath: String): Result<List<RemoteFile>> = runCatching {
        val path = if (remotePath == "/" || remotePath.isBlank()) "" else remotePath
        val arg = buildJsonObject { put("path", path); put("recursive", false) }
        val req = apiReq("https://api.dropboxapi.com/2/files/list_folder", arg)
        client.newCall(req).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["entries"]?.jsonArray
                ?.map { it.jsonObject.toRemoteFile() } ?: emptyList()
        }
    }

    override suspend fun uploadFile(localStream: InputStream, remotePath: String, sizeBytes: Long, onProgress: (Long) -> Unit): Result<RemoteFile> = runCatching {
        val bytes = localStream.readBytes()
        val arg = buildJsonObject {
            put("path", remotePath.normalizeDropbox())
            put("mode", "overwrite")
            put("autorename", false)
        }
        val req = Request.Builder().url("https://content.dropboxapi.com/2/files/upload")
            .post(bytes.toRequestBody("application/octet-stream".toMediaType()))
            .header("Authorization", "Bearer $token")
            .header("Dropbox-API-Arg", arg.toString()).build()
        client.newCall(req).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.toRemoteFile()
        }
    }

    override suspend fun downloadFile(remotePath: String, destination: OutputStream, onProgress: (Long) -> Unit): Result<Unit> = runCatching {
        val arg = buildJsonObject { put("path", remotePath.normalizeDropbox()) }
        val req = Request.Builder().url("https://content.dropboxapi.com/2/files/download")
            .post("".toRequestBody())
            .header("Authorization", "Bearer $token")
            .header("Dropbox-API-Arg", arg.toString()).build()
        client.newCall(req).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 {
        val arg = buildJsonObject { put("path", remotePath.normalizeDropbox()) }
        val req = apiReq("https://api.dropboxapi.com/2/files/delete_v2", arg)
        client.newCall(req).execute().use { resp -> if (!resp.isSuccessful) throw Exception("HTTP ${resp.code}") }
    }

    override suspend fun createDirectory(remotePath: String): Result<Unit> = runCatching {
        val arg = buildJsonObject { put("path", remotePath.normalizeDropbox()) }
        val req = apiReq("https://api.dropboxapi.com/2/files/create_folder_v2", arg)
        client.newCall(req).execute().use { resp -> if (!resp.isSuccessful && resp.code != 409) throw Exception("HTTP ${resp.code}") }
    }

    override suspend fun getFileMetadata(remotePath: String): Result<RemoteFile> = runCatching {
        val arg = buildJsonObject { put("path", remotePath.normalizeDropbox()) }
        val req = apiReq("https://api.dropboxapi.com/2/files/get_metadata", arg)
        client.newCall(req).execute().use { resp ->
            val body = resp.body?.string() ?: throw Exception("HTTP ${resp.code}")
            Json.parseToJsonElement(body).jsonObject.toRemoteFile()
        }
    }

    override suspend fun moveFile(fromPath: String, toPath: String): Result<Unit> = runCatching {
        val arg = buildJsonObject {
            put("from_path", fromPath.normalizeDropbox())
            put("to_path", toPath.normalizeDropbox())
        }
        val req = apiReq("https://api.dropboxapi.com/2/files/move_v2", arg)
        client.newCall(req).execute().use { resp -> if (!resp.isSuccessful) throw Exception("HTTP ${resp.code}") }
    }

    private fun JsonObject.toRemoteFile(): RemoteFile {
        val tag = get(".tag")?.jsonPrimitive?.content ?: ""
        return RemoteFile(
            path = get("path_display")?.jsonPrimitive?.content ?: "",
            name = get("name")?.jsonPrimitive?.content ?: "",
            isDirectory = tag == "folder",
            sizeBytes = get("size")?.jsonPrimitive?.content?.toLongOrNull() ?: 0L,
            modifiedAt = get("server_modified")?.jsonPrimitive?.content?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: Instant.EPOCH,
            etag = get("content_hash")?.jsonPrimitive?.content,
            mimeType = null,
        )
    }

    private fun String.normalizeDropbox() = if (this == "/") "" else if (!startsWith("/")) "/$this" else this
}
