package com.aletheia.app.data

import com.aletheia.app.model.QBooksBook
import com.aletheia.app.model.CatalogPage
import java.io.StringReader
import java.util.Locale
import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.xml.sax.InputSource

object OpdsCatalogParser {
    fun isOpdsFeed(xml: String): Boolean = runCatching {
        val root = parseDocument(xml).documentElement
        root.localName == "feed" && root.namespaceURI == ATOM_NAMESPACE
    }.getOrDefault(false)

    fun parseBooks(xml: String, resolveUrl: (String) -> String): List<QBooksBook> =
        parsePage(xml, resolveUrl).books

    fun parsePage(xml: String, resolveUrl: (String) -> String): CatalogPage {
        val document = parseDocument(xml)
        val root = document.documentElement
        require(root.localName == "feed" && root.namespaceURI == ATOM_NAMESPACE) {
            "Сервер вернул документ, который не является OPDS-каталогом."
        }

        val entries = document.getElementsByTagNameNS(ATOM_NAMESPACE, "entry")
        val books = buildList {
            for (index in 0 until entries.length) {
                val entry = entries.item(index) as? Element ?: continue
                parseBook(entry, resolveUrl)?.let(::add)
            }
        }
        val nextPageUrl = root.childElements("link")
            .firstOrNull { link ->
                link.getAttribute("rel").split(' ').any { it.equals("next", ignoreCase = true) }
            }
            ?.getAttribute("href")
            ?.trim()
            ?.takeIf(String::isNotBlank)
            ?.let(resolveUrl)
        return CatalogPage(books = books, nextPageUrl = nextPageUrl)
    }

    private fun parseBook(entry: Element, resolveUrl: (String) -> String): QBooksBook? {
        val links = entry.getElementsByTagNameNS(ATOM_NAMESPACE, "link")
        var coverHref: String? = null
        var shareHref: String? = null
        val acquisitions = mutableListOf<Acquisition>()

        for (index in 0 until links.length) {
            val link = links.item(index) as? Element ?: continue
            val rel = link.getAttribute("rel")
            val href = link.getAttribute("href").trim()
            val type = link.getAttribute("type").lowercase(Locale.US)
            if (href.isBlank()) continue

            if (coverHref == null && rel in COVER_RELS) {
                coverHref = href
            }

            val relTokens = rel.split(WHITESPACE).filter(String::isNotBlank)
            if (
                shareHref == null &&
                type.substringBefore(';').trim() == "text/html" &&
                relTokens.any { it.equals("alternate", ignoreCase = true) }
            ) {
                shareHref = href
            }

            if (rel.contains("acquisition", ignoreCase = true)) {
                acquisitionFor(href, type)?.let(acquisitions::add)
            }
        }

        val acquisition = acquisitions.minByOrNull(Acquisition::priority) ?: return null
        val title = entry.firstText(ATOM_NAMESPACE, "title")
            .cleanCatalogTitle()
            .ifBlank { "Без названия" }
        val authors = entry.getElementsByTagNameNS(ATOM_NAMESPACE, "author")
        val authorText = buildList {
            for (index in 0 until authors.length) {
                val author = authors.item(index) as? Element ?: continue
                author.firstText(ATOM_NAMESPACE, "name").takeIf(String::isNotBlank)?.let(::add)
            }
        }.joinToString(", ").ifBlank { "Неизвестный автор" }

        val rawDescription = entry.firstText(ATOM_NAMESPACE, "content")
            .ifBlank { entry.firstText(ATOM_NAMESPACE, "summary") }

        return QBooksBook(
            id = entry.firstText(ATOM_NAMESPACE, "id")
                .ifBlank { resolveUrl(acquisition.href) },
            title = title,
            author = authorText,
            format = acquisition.format,
            downloadUrl = resolveUrl(acquisition.href),
            shareUrl = shareHref?.let(resolveUrl),
            coverUrl = coverHref?.let(resolveUrl),
            description = rawDescription.cleanMarkup().sanitizeSourceAttribution().ifBlank { null },
            language = entry.firstText(DC_TERMS_NAMESPACE, "language").ifBlank { null },
            publisher = entry.firstText(DC_TERMS_NAMESPACE, "publisher")
                .sanitizeSourceAttribution()
                .ifBlank { null },
            published = entry.firstText(DC_TERMS_NAMESPACE, "issued")
                .ifBlank { entry.firstText(ATOM_NAMESPACE, "published") }
                .ifBlank { null }
        )
    }

    private fun acquisitionFor(href: String, type: String): Acquisition? = when {
        type.contains("epub") -> Acquisition(href, "epub", priority = 0)
        type == "application/x-fictionbook+xml" || type == "application/fb2" -> {
            Acquisition(href, "fb2", priority = 1)
        }
        href.substringBefore('?').endsWith(".epub", ignoreCase = true) -> {
            Acquisition(href, "epub", priority = 2)
        }
        href.substringBefore('?').endsWith(".fb2", ignoreCase = true) -> {
            Acquisition(href, "fb2", priority = 3)
        }
        else -> null
    }

    private fun Element.firstText(namespace: String, localName: String): String =
        getElementsByTagNameNS(namespace, localName)
            .item(0)
            ?.textContent
            ?.trim()
            .orEmpty()

    private fun Element.childElements(localName: String): List<Element> = buildList {
        val children = childNodes
        for (index in 0 until children.length) {
            val child = children.item(index) as? Element ?: continue
            if (child.namespaceURI == ATOM_NAMESPACE && child.localName == localName) {
                add(child)
            }
        }
    }

    private fun String.cleanMarkup(): String =
        replace(HTML_TAG, " ")
            .replace(WHITESPACE, " ")
            .trim()

    private fun String.sanitizeSourceAttribution(): String =
        replace(SOURCE_URL, " ")
            .replace(SOURCE_NAME, " ")
            .replace(WHITESPACE, " ")
            .trim(' ', '-', '—', ':')

    private fun String.cleanCatalogTitle(): String =
        replace(TRAILING_METADATA_TAG, "").trim()

    private fun parseDocument(xml: String): Document {
        require(!xml.contains("<!DOCTYPE", ignoreCase = true)) {
            "DOCTYPE запрещён в OPDS-ответе."
        }
        val factory = DocumentBuilderFactory.newInstance().apply {
            isNamespaceAware = true
            runCatching { isXIncludeAware = false }
            runCatching { setExpandEntityReferences(false) }
            runCatching { setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) }
            runCatching { setFeature("http://xml.org/sax/features/external-general-entities", false) }
            runCatching { setFeature("http://xml.org/sax/features/external-parameter-entities", false) }
            runCatching { setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) }
        }
        return factory.newDocumentBuilder().parse(InputSource(StringReader(xml)))
    }

    private data class Acquisition(
        val href: String,
        val format: String,
        val priority: Int
    )

    private const val ATOM_NAMESPACE = "http://www.w3.org/2005/Atom"
    private const val DC_TERMS_NAMESPACE = "http://purl.org/dc/terms/"
    private val COVER_RELS = setOf(
        "http://opds-spec.org/image",
        "http://opds-spec.org/image/thumbnail",
        "x-stanza-cover-image",
        "x-stanza-cover-image-thumbnail"
    )
    private val HTML_TAG = Regex("<[^>]+>")
    private val WHITESPACE = Regex("\\s+")
    private val SOURCE_URL = Regex("https?://(?:[a-z0-9-]+\\.)?flibusta\\.is\\S*", RegexOption.IGNORE_CASE)
    private val SOURCE_NAME = Regex("flibusta|флибуста", RegexOption.IGNORE_CASE)
    private val TRAILING_METADATA_TAG = Regex("\\s*\\[(?:litres(?:\\.ru)?|[a-z]{2,3})]\\s*$", RegexOption.IGNORE_CASE)
}
