package com.aletheia.app.ui.reader

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.webkit.JavascriptInterface
import android.webkit.RenderProcessGoneDetail
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.webkit.WebViewAssetLoader
import com.aletheia.app.model.ReadingNote
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import org.json.JSONArray
import org.json.JSONObject
import kotlin.coroutines.resume

class ReaderWebController(
    context: Context,
    private val webView: WebView,
    private val onEvent: (ReaderEvent) -> Unit
) {
    private val appContext = context.applicationContext
    private val booksRoot = File(appContext.filesDir, BOOKS_DIRECTORY).canonicalFile
    @Volatile private var currentBookResource: BookResource? = null
    private var shellReady = false
    private var pendingBook: PendingBook? = null
    private var renderProcessGone = false

    private val assetLoader = WebViewAssetLoader.Builder()
        .addPathHandler(BOOK_PATH_PREFIX, CurrentBookPathHandler())
        .addPathHandler(ASSET_PATH_PREFIX, WebViewAssetLoader.AssetsPathHandler(appContext))
        .addPathHandler(RESOURCE_PATH_PREFIX, WebViewAssetLoader.ResourcesPathHandler(appContext))
        .build()

    init {
        configureWebView()
    }

    @SuppressLint("SetJavaScriptEnabled")
    private fun configureWebView() {
        webView.settings.apply {
            javaScriptEnabled = true
            domStorageEnabled = true
            allowFileAccess = false
            allowContentAccess = false
            allowFileAccessFromFileURLs = false
            allowUniversalAccessFromFileURLs = false
            mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
            loadsImagesAutomatically = true
            mediaPlaybackRequiresUserGesture = true
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) safeBrowsingEnabled = true
        }
        webView.isVerticalScrollBarEnabled = false
        webView.isHorizontalScrollBarEnabled = false
        webView.addJavascriptInterface(Bridge(), BRIDGE_NAME)
        webView.webViewClient = object : WebViewClient() {
            override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
                val uri = request?.url ?: return emptyResponse(404, "Not Found")
                assetLoader.shouldInterceptRequest(uri)?.let { return it }
                return when (uri.scheme?.lowercase(Locale.US)) {
                    "blob", "data", "about" -> super.shouldInterceptRequest(view, request)
                    else -> emptyResponse(403, "Blocked")
                }
            }

            override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
                val uri = request?.url ?: return true
                if (
                    uri.scheme.equals("https", ignoreCase = true) &&
                    uri.host.equals(APP_ASSET_HOST, ignoreCase = true)
                ) {
                    return false
                }
                validatedExternalUrl(uri)?.let { onEvent(ReaderEvent.ExternalLink(it)) }
                return true
            }

            override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                shellReady = false
                renderProcessGone = false
                super.onPageStarted(view, url, favicon)
            }

            override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
                if (request?.isForMainFrame == true) {
                    onEvent(
                        ReaderEvent.Error(
                            stage = "shell.load",
                            code = error?.errorCode?.let { "WebViewError$it" } ?: "WebViewLoadError",
                            message = error?.description?.toString().orEmpty().ifBlank {
                                "Не удалось запустить читалку"
                            },
                            recoverable = true
                        )
                    )
                }
            }

            override fun onRenderProcessGone(view: WebView?, detail: RenderProcessGoneDetail?): Boolean {
                renderProcessGone = true
                onEvent(
                    ReaderEvent.Error(
                        stage = "renderer",
                        code = if (detail?.didCrash() == true) "RenderProcessCrash" else "RenderProcessGone",
                        message = "Процесс отображения книги был перезапущен",
                        recoverable = true
                    )
                )
                return true
            }
        }
        webView.loadUrl(READER_URL)
    }

    fun loadBook(
        file: File,
        format: String,
        locator: String?,
        progress: Double,
        cachedLocations: String?,
        preferences: ReaderPreferences,
        title: String
    ) {
        val canonical = file.canonicalFile
        require(canonical.parentFile == booksRoot) { "Книга находится вне внутреннего каталога приложения" }
        require(canonical.isFile) { "Файл книги не найден" }
        val normalizedFormat = format.lowercase(Locale.US)
        require(normalizedFormat == "epub" || normalizedFormat == "fb2") {
            "Поддерживаются только EPUB и FB2"
        }
        val mime = when (normalizedFormat) {
            "epub" -> "application/epub+zip"
            "fb2" -> "application/xml"
            else -> error("Unsupported book format")
        }
        currentBookResource = BookResource(canonical, mime, normalizedFormat)
        val compatibleLocator = validatedLocator(normalizedFormat, locator)
        pendingBook = PendingBook(
            normalizedFormat,
            compatibleLocator,
            normalizedProgress(progress),
            cachedLocations,
            preferences,
            title
        )
        sendPendingBookIfReady()
    }

    fun setPreferences(preferences: ReaderPreferences) = call("setPreferences", preferences.toEngineJson())
    fun next() = call("next")
    fun previous() = call("previous")
    fun goToProgress(progress: Double) = call("goToProgress", normalizedProgress(progress))
    fun goToLocator(locator: String) {
        val compatible = validatedLocator(currentBookResource?.format.orEmpty(), locator) ?: return
        call("goToLocator", encodeLocator(compatible))
    }
    fun goToChapter(href: String) = call("goToChapter", href)
    fun search(query: String) = call("search", query)
    fun nextSearch() = call("nextSearch")
    fun previousSearch() = call("previousSearch")
    fun clearSearch() = call("clearSearch")
    fun clearSelection() = call("clearSelection")
    fun addHighlight(locator: String, color: String) {
        val compatible = validatedLocator(currentBookResource?.format.orEmpty(), locator) ?: return
        call("addHighlight", encodeLocator(compatible), color)
    }

    fun removeHighlight(locator: String) {
        val compatible = validatedLocator(currentBookResource?.format.orEmpty(), locator) ?: return
        call("removeHighlight", encodeLocator(compatible))
    }

    fun setHighlights(notes: List<ReadingNote>) {
        val format = currentBookResource?.format ?: return
        val highlights = JSONArray()
        notes.asSequence()
            .filter { !it.cfi.isNullOrBlank() && !it.highlightColor.isNullOrBlank() }
            .forEach { note ->
                val locator = validatedLocator(format, note.cfi) ?: return@forEach
                highlights.put(
                    JSONObject()
                        .put("id", note.id.toString())
                        .put("locator", encodeLocator(locator))
                        .put("color", note.highlightColor)
                )
            }
        call("setHighlights", highlights)
    }

    suspend fun getStateJson(): String? = withContext(Dispatchers.Main.immediate) {
        withTimeoutOrNull(JS_TIMEOUT_MS) {
            suspendCancellableCoroutine { continuation ->
                webView.evaluateJavascript(
                    "window.ReaderV2 && window.ReaderV2.getStateJson ? window.ReaderV2.getStateJson() : null"
                ) { value ->
                    if (continuation.isActive) continuation.resume(decodeJavascriptString(value))
                }
            }
        }
    }

    suspend fun getSpeechPage(maxCharacters: Int): ReaderSpeechPage? =
        withContext(Dispatchers.Main.immediate) {
            withTimeoutOrNull(JS_TIMEOUT_MS) {
                suspendCancellableCoroutine { continuation ->
                    val limit = maxCharacters.coerceIn(200, MAX_SPEECH_PAGE_CHARACTERS)
                    webView.evaluateJavascript(
                        "window.ReaderV2 && window.ReaderV2.getSpeechPageJson ? " +
                            "window.ReaderV2.getSpeechPageJson($limit) : null"
                    ) { value ->
                        val result = decodeJavascriptString(value)
                            ?.let { runCatching { JSONObject(it) }.getOrNull() }
                            ?.let(ReaderSpeechPage::fromJson)
                        if (continuation.isActive) continuation.resume(result)
                    }
                }
            }
        }

    fun reload(): Boolean {
        if (renderProcessGone) return false
        shellReady = false
        webView.reload()
        return true
    }

    fun updatePendingPosition(locator: String?, progress: Double, preferences: ReaderPreferences) {
        val pending = pendingBook ?: return
        pendingBook = pending.copy(
            locator = validatedLocator(pending.format, locator),
            progress = normalizedProgress(progress, pending.progress),
            preferences = preferences
        )
    }

    fun destroy() {
        currentBookResource = null
        pendingBook = null
        webView.removeJavascriptInterface(BRIDGE_NAME)
        webView.stopLoading()
        webView.destroy()
    }

    private fun sendPendingBookIfReady() {
        val pending = pendingBook ?: return
        if (!shellReady || currentBookResource == null) return
        val payload = JSONObject()
            .put("url", BOOK_URL)
            .put("format", pending.format.lowercase(Locale.US))
            .put("locator", pending.locator?.let(::encodeLocator) ?: JSONObject.NULL)
            .put("progress", normalizedProgress(pending.progress))
            .put("cachedLocations", pending.cachedLocations ?: JSONObject.NULL)
            .put("preferences", pending.preferences.toEngineJson())
            .put("title", pending.title)
        call("loadBook", payload)
    }

    private fun call(method: String, vararg arguments: Any?) {
        val encoded = arguments.joinToString(",") { argument ->
            when (argument) {
                null -> "null"
                is JSONObject, is JSONArray -> argument.toString()
                is Number, is Boolean -> argument.toString()
                else -> JSONObject.quote(argument.toString())
            }
        }
        webView.post {
            webView.evaluateJavascript(
                "window.ReaderV2 && window.ReaderV2.$method && window.ReaderV2.$method($encoded)",
                null
            )
        }
    }

    private fun encodeLocator(locator: String): Any =
        runCatching { JSONObject(locator) }.getOrElse { locator }

    private inner class Bridge {
        @JavascriptInterface
        fun postMessage(message: String) {
            val event = ReaderEvent.parse(message) ?: return
            webView.post {
                if (event is ReaderEvent.ShellReady) {
                    shellReady = true
                    sendPendingBookIfReady()
                }
                onEvent(event)
            }
        }
    }

    private inner class CurrentBookPathHandler : WebViewAssetLoader.PathHandler {
        override fun handle(path: String): WebResourceResponse? {
            if (path.substringBefore('?').trim('/') != BOOK_RESOURCE_NAME) return null
            val resource = currentBookResource ?: return emptyResponse(404, "Book not selected")
            val file = resource.file
            if (!file.isFile || file.canonicalFile.parentFile != booksRoot) {
                return emptyResponse(404, "Book not found")
            }
            return WebResourceResponse(resource.mime, null, FileInputStream(file)).apply {
                responseHeaders = mapOf(
                    "Cache-Control" to "no-store",
                    "X-Content-Type-Options" to "nosniff"
                )
            }
        }
    }

    private data class PendingBook(
        val format: String,
        val locator: String?,
        val progress: Double,
        val cachedLocations: String?,
        val preferences: ReaderPreferences,
        val title: String
    )

    private data class BookResource(
        val file: File,
        val mime: String,
        val format: String
    )

    companion object {
        private const val BRIDGE_NAME = "AndroidBridge"
        private const val BOOKS_DIRECTORY = "Books"
        private const val APP_ASSET_HOST = "appassets.androidplatform.net"
        private const val ASSET_PATH_PREFIX = "/assets/"
        private const val RESOURCE_PATH_PREFIX = "/res/"
        private const val BOOK_PATH_PREFIX = "/book/"
        private const val BOOK_RESOURCE_NAME = "current"
        private const val READER_URL = "https://$APP_ASSET_HOST/assets/reader_v2/index.html"
        private const val BOOK_URL = "https://$APP_ASSET_HOST/book/$BOOK_RESOURCE_NAME"
        private const val JS_TIMEOUT_MS = 5_000L
        private const val MAX_SPEECH_PAGE_CHARACTERS = 3_500

        private fun emptyResponse(status: Int, reason: String): WebResourceResponse =
            WebResourceResponse(
                "text/plain",
                "UTF-8",
                status,
                reason,
                emptyMap(),
                ByteArrayInputStream(ByteArray(0))
            )

        private fun decodeJavascriptString(value: String?): String? {
            if (value == null || value == "null" || value == "undefined") return null
            return runCatching { JSONArray("[$value]").getString(0) }.getOrNull()
        }

        private fun normalizedProgress(progress: Double, fallback: Double = 0.0): Double =
            if (progress.isFinite()) progress.coerceIn(0.0, 1.0) else fallback.coerceIn(0.0, 1.0)

        internal fun validatedLocator(format: String, locator: String?): String? {
            val raw = locator?.trim()?.takeIf(String::isNotEmpty) ?: return null
            val objectLocator = runCatching { JSONObject(raw) }.getOrNull()
            return when (format) {
                "epub" -> when {
                    objectLocator != null &&
                        objectLocator.optString("type") == "epub" &&
                        objectLocator.optString("cfi").startsWith("epubcfi(") -> objectLocator.toString()
                    objectLocator == null && raw.startsWith("epubcfi(") -> raw
                    else -> null
                }
                "fb2" -> when {
                    objectLocator != null &&
                        objectLocator.optString("type") == "fb2" &&
                        objectLocator.optString("sectionId").isNotBlank() &&
                        objectLocator.optInt("offset", -1) >= 0 -> objectLocator.toString()
                    objectLocator == null && FB2_LEGACY_LOCATOR.matches(raw) -> raw
                    else -> null
                }
                else -> null
            }
        }

        private fun validatedExternalUrl(uri: Uri): String? = when (uri.scheme?.lowercase(Locale.US)) {
            "http", "https" -> uri.toString().takeIf { !uri.host.isNullOrBlank() }
            "mailto" -> uri.toString().takeIf { !uri.schemeSpecificPart.isNullOrBlank() }
            else -> null
        }

        private val FB2_LEGACY_LOCATOR = Regex("^fb2:[^:]+:\\d+(?::\\d+)?$")
    }
}

data class ReaderSpeechPage(
    val text: String,
    val locator: String?,
    val canAdvance: Boolean
) {
    companion object {
        internal fun fromJson(json: JSONObject): ReaderSpeechPage? {
            val text = json.optString("text").trim()
            if (text.isBlank()) return null
            val locator = when (val value = json.opt("locator")) {
                is JSONObject -> value.toString()
                is String -> value.takeIf(String::isNotBlank)
                else -> null
            }
            return ReaderSpeechPage(
                text = text,
                locator = locator,
                canAdvance = json.optBoolean("canAdvance", true)
            )
        }
    }
}
