package eu.geyskens.pdfscan.paperless

import android.content.Context
import eu.geyskens.pdfscan.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.util.concurrent.TimeUnit

/**
 * Minimal Paperless-ngx REST client. Talks only to the user-configured, self-hosted
 * instance. The token is sent as an `Authorization: Token …` header over the connection
 * the user set up (HTTPS strongly recommended — see SettingsScreen guidance).
 */
class PaperlessClient(
    private val baseUrl: String,
    private val token: String,
    private val context: Context,
) {
    private val client = OkHttpClient.Builder()
        .connectTimeout(15, TimeUnit.SECONDS)
        .writeTimeout(60, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()

    private val root = baseUrl.trim().trimEnd('/')

    sealed interface Result {
        /** Document was consumed and created in Paperless. */
        data object Success : Result
        /** Paperless rejected it as a duplicate of an existing document. */
        data object Duplicate : Result
        /** Upload was accepted but still processing when we stopped waiting. */
        data object Pending : Result
        data class Failure(val message: String) : Result
    }

    /** Verifies the URL + token by hitting an authenticated endpoint. */
    suspend fun testConnection(): Result = withContext(Dispatchers.IO) {
        val request = Request.Builder()
            .url("$root/api/documents/?page_size=1")
            .header("Authorization", "Token $token")
            .header("Accept", "application/json")
            .get()
            .build()
        runCatching {
            client.newCall(request).execute().use { resp ->
                when {
                    resp.isSuccessful -> Result.Success
                    resp.code == 401 || resp.code == 403 ->
                        Result.Failure(context.getString(R.string.err_invalid_token, resp.code))
                    else -> Result.Failure(context.getString(R.string.err_server_responded, resp.code))
                }
            }
        }.getOrElse { Result.Failure(it.message ?: context.getString(R.string.err_connection_failed)) }
    }

    /**
     * Uploads a PDF and then waits for Paperless to actually consume it. The upload
     * endpoint only queues the document and returns a task UUID; we poll the task so the
     * user learns whether it was added, was a duplicate, or failed.
     */
    suspend fun upload(pdf: File, title: String): Result = withContext(Dispatchers.IO) {
        val body = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("title", title)
            .addFormDataPart(
                "document", "$title.pdf",
                pdf.asRequestBody("application/pdf".toMediaType())
            )
            .build()
        val request = Request.Builder()
            .url("$root/api/documents/post_document/")
            .header("Authorization", "Token $token")
            .post(body)
            .build()

        // The POST runs NonCancellable so that "stop waiting" only skips the status poll —
        // the document is always really queued. post_document returns the task UUID string.
        val posted: kotlin.Result<String?> = withContext(NonCancellable) {
            runCatching {
                client.newCall(request).execute().use { resp ->
                    if (!resp.isSuccessful) {
                        error(context.getString(R.string.err_upload_failed_code, resp.code, resp.message))
                    }
                    resp.body?.string()?.trim()?.trim('"')?.takeIf { it.isNotBlank() }
                }
            }
        }
        val taskId = posted.getOrElse {
            return@withContext Result.Failure(it.message ?: context.getString(R.string.err_upload_failed))
        }

        // Older servers may not return a usable id; then we can only confirm acceptance.
        if (taskId == null || taskId.equals("OK", ignoreCase = true)) return@withContext Result.Pending

        pollTask(taskId)
    }

    /** Polls /api/tasks/ until the consume task finishes, or gives up after ~45s. */
    private suspend fun pollTask(taskId: String): Result {
        val deadline = System.currentTimeMillis() + 45_000
        while (System.currentTimeMillis() < deadline) {
            val task = runCatching {
                val req = Request.Builder()
                    .url("$root/api/tasks/?task_id=$taskId")
                    .header("Authorization", "Token $token")
                    .header("Accept", "application/json")
                    .get()
                    .build()
                client.newCall(req).execute().use { resp ->
                    if (resp.isSuccessful) firstTask(resp.body?.string()) else null
                }
            }.getOrNull()

            when (task?.status?.uppercase()) {
                "SUCCESS" -> return Result.Success
                "FAILURE" -> {
                    val msg = task.result.orEmpty()
                    return if (msg.contains("duplicate", ignoreCase = true)) Result.Duplicate
                    else Result.Failure(msg.ifBlank { context.getString(R.string.err_paperless_process) })
                }
                // PENDING / STARTED / RETRY / unknown → keep waiting.
            }
            delay(1500)
        }
        return Result.Pending
    }

    private data class Task(val status: String?, val result: String?)

    /** Parses the first task object from the /api/tasks/ response (a plain array). */
    private fun firstTask(json: String?): Task? {
        if (json.isNullOrBlank()) return null
        return runCatching {
            val trimmed = json.trim()
            val arr = if (trimmed.startsWith("[")) JSONArray(trimmed)
            else JSONObject(trimmed).optJSONArray("results")
            if (arr == null || arr.length() == 0) return null
            val o = arr.getJSONObject(0)
            Task(
                status = o.optString("status").takeIf { it.isNotBlank() && it != "null" },
                result = o.optString("result").takeIf { it.isNotBlank() && it != "null" },
            )
        }.getOrNull()
    }
}
