package com.aletheia.app.data

import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Base64
import com.aletheia.app.model.Book
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.util.Locale
import java.util.UUID
import java.util.zip.ZipFile
import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.xml.sax.InputSource

class BookParserService private constructor(
    private val context: Context?,
    private val booksDirectory: File,
    private val cacheDirectory: File,
    private val base64Decoder: (String) -> ByteArray
) {

    constructor(context: Context) : this(
        context = context,
        booksDirectory = File(context.filesDir, "Books").apply { mkdirs() },
        cacheDirectory = context.cacheDir.apply { mkdirs() },
        base64Decoder = { value -> Base64.decode(value, Base64.DEFAULT) }
    )

    internal constructor(
        booksDirectory: File,
        cacheDirectory: File,
        base64Decoder: (String) -> ByteArray
    ) : this(
        context = null,
        booksDirectory = booksDirectory.apply { mkdirs() },
        cacheDirectory = cacheDirectory.apply { mkdirs() },
        base64Decoder = base64Decoder
    )

    fun getBooksDirectory(): File = booksDirectory

    fun parseAndStoreBookFromUri(uri: Uri): Book {
        val appContext = requireNotNull(context) { "Context is required to import a book from Uri." }
        val originalFileName = resolveDisplayName(uri)
        val extension = originalFileName.substringAfterLast('.', "").lowercase(Locale.US)
        val tempFile = File(cacheDirectory, "${UUID.randomUUID()}.$extension")

        appContext.contentResolver.openInputStream(uri).use { input ->
            requireNotNull(input) { "Не удалось открыть выбранный документ." }
            FileOutputStream(tempFile).use { output -> input.copyTo(output) }
        }

        return try {
            parseAndStoreBook(tempFile, originalFileName)
        } finally {
            tempFile.delete()
        }
    }

    fun parseAndStoreBook(sourceFile: File, originalFileName: String): Book {
        val extension = originalFileName.substringAfterLast('.', "").lowercase(Locale.US)
        val destinationFile = File(booksDirectory, "${UUID.randomUUID()}.$extension")

        if (sourceFile.absolutePath != destinationFile.absolutePath) {
            sourceFile.copyTo(destinationFile, overwrite = true)
        }

        val baseBook = Book(
            title = originalFileName.substringBeforeLast('.'),
            author = "Unknown",
            filePath = destinationFile.absolutePath,
            fileName = originalFileName,
            format = extension,
            dateAdded = System.currentTimeMillis(),
            lastRead = System.currentTimeMillis()
        )

        return when (extension) {
            "epub" -> parseEpub(baseBook)
            "fb2" -> parseFb2(baseBook)
            else -> baseBook
        }
    }

    private fun parseEpub(book: Book): Book {
        return try {
            ZipFile(book.filePath).use { zipFile ->
                val rootFilePath = findEpubRootFile(zipFile) ?: return book
                val opfEntry = zipFile.getEntry(rootFilePath) ?: return book
                val opfDocument = parseXml(zipFile.getInputStream(opfEntry))
                val metadata = opfDocument.getElementsByTagNameNS("*", "metadata").item(0) as? Element

                val title = metadata?.getElementsByTagNameNS("*", "title")?.item(0)?.textContent?.trim().orEmpty()
                val author = metadata?.getElementsByTagNameNS("*", "creator")?.item(0)?.textContent?.trim().orEmpty()
                val coverBytes = findEpubCover(zipFile, rootFilePath, opfDocument)

                book.copy(
                    title = title.ifBlank { book.title },
                    author = author.ifBlank { "Unknown Author" },
                    coverImage = coverBytes ?: book.coverImage
                )
            }
        } catch (_: Exception) {
            book
        }
    }

    private fun parseFb2(book: Book): Book {
        return try {
            val xml = File(book.filePath).readText()
            val document = parseXml(ByteArrayInputStream(xml.toByteArray()))
            val title = document.getElementsByTagNameNS("*", "book-title").item(0)?.textContent?.trim().orEmpty()
            val authorNode = document.getElementsByTagNameNS("*", "author").item(0) as? Element
            val firstName = authorNode?.getElementsByTagNameNS("*", "first-name")?.item(0)?.textContent?.trim().orEmpty()
            val middleName = authorNode?.getElementsByTagNameNS("*", "middle-name")?.item(0)?.textContent?.trim().orEmpty()
            val lastName = authorNode?.getElementsByTagNameNS("*", "last-name")?.item(0)?.textContent?.trim().orEmpty()
            val author = listOf(firstName, middleName, lastName).filter { it.isNotBlank() }.joinToString(" ")

            book.copy(
                title = title.ifBlank { book.title },
                author = author.ifBlank { "Unknown Author" },
                coverImage = extractFb2Cover(document) ?: book.coverImage
            )
        } catch (_: Exception) {
            book
        }
    }

    private fun findEpubRootFile(zipFile: ZipFile): String? {
        val containerEntry = zipFile.getEntry("META-INF/container.xml") ?: return null
        val containerDocument = parseXml(zipFile.getInputStream(containerEntry))
        val rootFiles = containerDocument.getElementsByTagNameNS("*", "rootfile")
        val rootFile = rootFiles.item(0) as? Element ?: return null
        return rootFile.getAttribute("full-path").takeIf { it.isNotBlank() }
    }

    private fun findEpubCover(zipFile: ZipFile, rootFilePath: String, opfDocument: Document): ByteArray? {
        val manifest = opfDocument.getElementsByTagNameNS("*", "manifest").item(0) as? Element ?: return null
        val items = manifest.getElementsByTagNameNS("*", "item")

        var coverId: String? = null
        val metaNodes = opfDocument.getElementsByTagNameNS("*", "meta")
        for (index in 0 until metaNodes.length) {
            val node = metaNodes.item(index) as? Element ?: continue
            if (node.getAttribute("name").equals("cover", ignoreCase = true)) {
                coverId = node.getAttribute("content")
                break
            }
        }

        var coverHref: String? = null
        for (index in 0 until items.length) {
            val item = items.item(index) as? Element ?: continue
            val itemId = item.getAttribute("id")
            val href = item.getAttribute("href")
            val properties = item.getAttribute("properties")
            if (!coverId.isNullOrBlank() && itemId == coverId) {
                coverHref = href
                break
            }
            if (properties.contains("cover-image")) {
                coverHref = href
                break
            }
        }

        if (coverHref.isNullOrBlank()) {
            return null
        }

        val rootDirectory = rootFilePath.substringBeforeLast('/', "")
        val fullPath = if (rootDirectory.isBlank()) coverHref else "$rootDirectory/$coverHref"
        val coverEntry = zipFile.getEntry(fullPath) ?: return null
        return zipFile.getInputStream(coverEntry).use { it.readBytes() }
    }

    private fun extractFb2Cover(document: Document): ByteArray? {
        val binaryNodes = document.getElementsByTagNameNS("*", "binary")
        val imageNodes = document.getElementsByTagNameNS("*", "image")

        for (index in 0 until imageNodes.length) {
            val image = imageNodes.item(index) as? Element ?: continue
            val href = image.getAttribute("l:href")
                .ifBlank { image.getAttribute("xlink:href") }
                .ifBlank { image.getAttribute("href") }
                .removePrefix("#")

            if (href.isBlank()) {
                continue
            }

            for (binaryIndex in 0 until binaryNodes.length) {
                val binary = binaryNodes.item(binaryIndex) as? Element ?: continue
                if (binary.getAttribute("id") == href) {
                    return runCatching {
                        base64Decoder(binary.textContent.trim())
                    }.getOrNull()
                }
            }
        }

        return null
    }

    private fun parseXml(inputStream: InputStream): Document {
        inputStream.use { stream ->
            val factory = DocumentBuilderFactory.newInstance()
            factory.isNamespaceAware = true
            return factory.newDocumentBuilder().parse(InputSource(stream))
        }
    }

    private fun resolveDisplayName(uri: Uri): String {
        val appContext = requireNotNull(context) { "Context is required to resolve a Uri display name." }
        appContext.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null).use { cursor ->
            if (cursor != null && cursor.moveToFirst()) {
                val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
                if (index >= 0) {
                    return cursor.getString(index)
                }
            }
        }

        return uri.lastPathSegment?.substringAfterLast('/') ?: "book"
    }
}
