package ru.forbion.f7cloud.core.network

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.Credentials
import okhttp3.FormBody
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.net.URLDecoder

data class QrLoginResult(
    val serverUrl: String,
    val username: String,
    val appPassword: String,
)

object LoginFlowClient {
    /** F7cloud mobile QR scheme. */
    const val F7_LOGIN_PREFIX = "f7://login/"
    const val F7_OTP_PREFIX = "f7://onetime-login/"

    /** Browser login flow v2 landing URL prefix (QR on web login page). */
    private val BROWSER_FLOW_REGEX = Regex("""/login/v2/flow/([A-Za-z0-9]+)""")

    /** True when the scanned text looks like a complete F7 or browser login QR payload. */
    fun isCompleteQrPayload(qrData: String): Boolean {
        val trimmed = qrData.trim()
        return when {
            trimmed.startsWith(F7_LOGIN_PREFIX) || trimmed.startsWith(F7_OTP_PREFIX) ->
                parseCredentialParams(extractParams(trimmed)) != null
            isBrowserLoginFlowUrl(trimmed) ->
                (trimmed.startsWith("http://", ignoreCase = true) ||
                    trimmed.startsWith("https://", ignoreCase = true)) &&
                    BROWSER_FLOW_REGEX.containsMatchIn(trimmed)
            else -> false
        }
    }

    suspend fun completeQrLogin(qrData: String, trustAllCerts: Boolean = false): QrLoginResult? =
        withContext(Dispatchers.IO) {
            when {
                isBrowserLoginFlowUrl(qrData) -> null
                else -> when (val payload = extractPayload(qrData)) {
                    null -> null
                    QrPayload.DirectLogin -> parseDirectLogin(extractParams(qrData))
                    QrPayload.OneTimeLogin -> parseOneTimeLogin(extractParams(qrData), trustAllCerts)
                }
            }
        }

    /**
     * Approve browser login after scanning Login Flow v2 QR (logged-in mobile user).
     */
    suspend fun approveBrowserLoginFromQr(
        qrData: String,
        username: String,
        appPassword: String,
        trustAllCerts: Boolean = false,
    ): Boolean = withContext(Dispatchers.IO) {
        val flowUrl = qrData.trim()
        val landingUrl = normalizeBrowserFlowUrl(flowUrl) ?: return@withContext false
        val cookies = mutableListOf<Cookie>()
        val cookieJar = object : CookieJar {
            override fun saveFromResponse(url: HttpUrl, list: List<Cookie>) {
                cookies.removeAll { existing ->
                    list.any { it.name == existing.name && it.domain == existing.domain && it.path == existing.path }
                }
                cookies.addAll(list)
            }

            override fun loadForRequest(url: HttpUrl): List<Cookie> =
                cookies.filter { it.matches(url) }
        }
        val client = OkHttpClient.Builder()
            .cookieJar(cookieJar)
            .followRedirects(true)
            .applyUnsafeSslIfNeeded(trustAllCerts)
            .build()

        val landingHtml = client.newCall(
            Request.Builder().url(landingUrl).get().build(),
        ).execute().use { response ->
            if (!response.isSuccessful) return@withContext false
            response.body?.string().orEmpty()
        }

        val stateToken = STATE_TOKEN_REGEX.find(landingHtml)?.groupValues?.get(1)?.trim().orEmpty()
        if (stateToken.isBlank()) return@withContext false

        val requestToken = REQUEST_TOKEN_REGEX.find(landingHtml)?.groupValues?.get(1)?.trim().orEmpty()
        val base = extractServerBase(landingUrl)
        val apptokenUrl = "$base/login/v2/apptoken"
        val form = FormBody.Builder()
            .add("stateToken", stateToken)
            .add("user", username)
            .add("password", appPassword)
        if (requestToken.isNotBlank()) {
            form.add("requesttoken", requestToken)
        }

        client.newCall(
            Request.Builder()
                .url(apptokenUrl)
                .post(form.build())
                .build(),
        ).execute().use { response ->
            response.isSuccessful || response.code == 200
        }
    }

    private fun isBrowserLoginFlowUrl(qrData: String): Boolean =
        BROWSER_FLOW_REGEX.containsMatchIn(qrData)

    private fun extractServerBase(url: String): String {
        val trimmed = url.trim()
        return when {
            trimmed.contains("/index.php") -> trimmed.substringBefore("/index.php")
            trimmed.contains("/login/v2") -> trimmed.substringBefore("/login/v2")
            else -> {
                val schemeEnd = trimmed.indexOf("://")
                if (schemeEnd == -1) return trimmed.trimEnd('/')
                val pathStart = trimmed.indexOf('/', schemeEnd + 3)
                if (pathStart == -1) trimmed else trimmed.substring(0, pathStart)
            }
        }.trimEnd('/')
    }

    private fun normalizeBrowserFlowUrl(qrData: String): String? {
        val trimmed = qrData.trim()
        if (trimmed.startsWith("http://", ignoreCase = true) || trimmed.startsWith("https://", ignoreCase = true)) {
            return trimmed
        }
        return null
    }

    private val STATE_TOKEN_REGEX = Regex("""name=["']stateToken["']\s+value=["']([^"']+)["']""", RegexOption.IGNORE_CASE)
    private val REQUEST_TOKEN_REGEX = Regex("""name=["']requesttoken["']\s+value=["']([^"']+)["']""", RegexOption.IGNORE_CASE)

    private enum class QrPayload { DirectLogin, OneTimeLogin }

    private fun extractPayload(qrData: String): QrPayload? = when {
        qrData.startsWith(F7_LOGIN_PREFIX) -> QrPayload.DirectLogin
        qrData.startsWith(F7_OTP_PREFIX) -> QrPayload.OneTimeLogin
        else -> null
    }

    private fun extractParams(qrData: String): String {
        val prefix = when {
            qrData.startsWith(F7_LOGIN_PREFIX) -> F7_LOGIN_PREFIX
            qrData.startsWith(F7_OTP_PREFIX) -> F7_OTP_PREFIX
            else -> return qrData
        }
        return qrData.removePrefix(prefix)
    }

    private fun parseDirectLogin(params: String): QrLoginResult? {
        val parsed = parseCredentialParams(params) ?: return null
        return QrLoginResult(parsed.server.trimEnd('/'), parsed.user, parsed.password)
    }

    private fun parseOneTimeLogin(params: String, trustAllCerts: Boolean): QrLoginResult? {
        val parsed = parseCredentialParams(params) ?: return null
        val client = OkHttpClient.Builder().applyUnsafeSslIfNeeded(trustAllCerts).build()
        val credentials = Credentials.basic(parsed.user, parsed.password)
        val url = "${parsed.server.trimEnd('/')}/ocs/v2.php/core/getapppassword-onetime"
        val request = Request.Builder()
            .url(url)
            .header("Authorization", credentials)
            .header("OCS-APIRequest", "true")
            .header("Accept", "application/json")
            .build()
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) return null
            val body = response.body?.string().orEmpty()
            val appPassword = JSONObject(body)
                .optJSONObject("ocs")
                ?.optJSONObject("data")
                ?.optString("apppassword")
                .orEmpty()
            if (appPassword.isBlank()) return null
            return QrLoginResult(parsed.server.trimEnd('/'), parsed.user, appPassword)
        }
    }

    internal data class CredentialParams(
        val server: String,
        val user: String,
        val password: String,
    )

    /**
     * Разбор `server:...&user:...&password:...` (формат nc-login). Значения НЕ split('&'):
     * пароль может содержать `&` и `:`, а раньше `split('&')`+`size>3` его резал/ронял вход.
     * Ищем маркеры `key:` (в начале или после `&`) и берём значение до следующего маркера.
     */
    internal fun parseCredentialParams(params: String): CredentialParams? {
        val keys = listOf("server", "user", "password")
        data class Marker(val key: String, val at: Int, val valueAt: Int)
        val markers = mutableListOf<Marker>()
        for (key in keys) {
            if (params.startsWith("$key:")) markers += Marker(key, 0, key.length + 1)
            var idx = params.indexOf("&$key:")
            while (idx >= 0) {
                markers += Marker(key, idx, idx + 1 + key.length + 1)
                idx = params.indexOf("&$key:", idx + 1)
            }
        }
        if (markers.isEmpty()) return null
        markers.sortBy { it.at }
        val map = mutableMapOf<String, String>()
        markers.forEachIndexed { i, m ->
            val end = if (i + 1 < markers.size) markers[i + 1].at else params.length
            // первое вхождение ключа выигрывает (не перезатираем более поздним мусором)
            map.putIfAbsent(m.key, decode(params.substring(m.valueAt, end)))
        }
        val server = map["server"].orEmpty()
        val user = map["user"].orEmpty()
        val password = map["password"].orEmpty()
        if (server.isBlank() || user.isBlank() || password.isBlank()) return null
        return CredentialParams(server, user, password)
    }

    suspend fun pollBrowserLogin(serverUrl: String, trustAllCerts: Boolean = false): QrLoginResult? =
        withContext(Dispatchers.IO) {
            val base = serverUrl.trimEnd('/')
            val client = OkHttpClient.Builder().applyUnsafeSslIfNeeded(trustAllCerts).build()
            val startRequest = Request.Builder()
                .url("$base/index.php/login/v2")
                .post(FormBody.Builder().build())
                .header("Clear-Site-Data", "cookies")
                .build()
            val startBody = client.newCall(startRequest).execute().use { response ->
                if (!response.isSuccessful) return@withContext null
                response.body?.string().orEmpty()
            }
            val json = JSONObject(startBody)
            val poll = json.optJSONObject("poll") ?: return@withContext null
            val token = poll.optString("token")
            val pollUrl = poll.optString("endpoint")
            if (token.isBlank() || pollUrl.isBlank()) return@withContext null
            repeat(120) {
                val pollRequest = Request.Builder()
                    .url(pollUrl)
                    .post(FormBody.Builder().add("token", token).build())
                    .build()
                val result = client.newCall(pollRequest).execute().use { response ->
                    if (!response.isSuccessful) return@use null
                    val body = response.body?.string().orEmpty()
                    if (body.isBlank()) return@use null
                    val obj = JSONObject(body)
                    QrLoginResult(
                        serverUrl = obj.optString("server", base).trimEnd('/'),
                        username = obj.optString("loginName"),
                        appPassword = obj.optString("appPassword"),
                    ).takeIf { it.username.isNotBlank() && it.appPassword.isNotBlank() }
                }
                if (result != null) return@withContext result
                delay(250)
            }
            null
        }

    private fun decode(value: String): String =
        runCatching { URLDecoder.decode(value, Charsets.UTF_8.name()) }.getOrDefault(value)
}
