package ru.forbion.f7cloud.core.network

import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory
import java.net.URLDecoder
import java.util.Base64
import java.util.UUID
import java.util.regex.Pattern

data class DavContact(
    val uid: String,
    val displayName: String,
    val email: String,
    val phone: String,
    val bookName: String,
    val photoBase64: String = "",
    val photoMimeType: String = "",
    val organization: String = "",
    val title: String = "",
    val address: String = "",
    val website: String = "",
    val birthday: String = "",
    val emails: String = "",
    val phones: String = "",
)

object CardDavClient {
    private val fnPattern = Pattern.compile("FN[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val nPattern = Pattern.compile("N[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val emailPattern = Pattern.compile("EMAIL[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val telPattern = Pattern.compile("TEL[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val uidPattern = Pattern.compile("UID[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val orgPattern = Pattern.compile("ORG[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val titlePattern = Pattern.compile("TITLE[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val urlPattern = Pattern.compile("URL[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val adrPattern = Pattern.compile("ADR[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val bdayPattern = Pattern.compile("BDAY[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)

    private val genericNames = setOf(
        "contact",
        "carddav",
        "card dav",
        "unknown",
        "vcard",
    )

    fun listContacts(
        client: OkHttpClient,
        serverUrl: String,
        userId: String,
        limitPerBook: Int = 500,
    ): List<DavContact> {
        val base = davAddressBooksBaseUrl(serverUrl, userId)
        val books = listAddressBooks(client, base)
        val out = mutableListOf<DavContact>()
        var fetchFailed = false
        for (book in books) {
            runCatching { fetchContactsFromBook(client, book, limitPerBook) }
                .onSuccess { out += it }
                .onFailure { fetchFailed = true }
        }
        if (out.isEmpty() && fetchFailed) {
            error("Не удалось загрузить контакты")
        }
        return out.distinctBy { "${it.uid}|${it.email}" }
    }

    /**
     * Дешёвая «подпись» состояния адресных книг — CTag коллекций (расширение
     * CalendarServer, поддерживается Nextcloud), с фолбэком на sync-token.
     * Один PROPFIND depth:1 без address-data. Если подпись не изменилась с прошлой
     * синхронизации — контакты качать не нужно. null при ошибке/неподдержке → полный sync.
     */
    fun collectionSignature(
        client: OkHttpClient,
        serverUrl: String,
        userId: String,
    ): String? = runCatching {
        val base = davAddressBooksBaseUrl(serverUrl, userId)
        val body = """
            <?xml version="1.0" encoding="utf-8"?>
            <d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
              <d:prop><cs:getctag/><d:sync-token/></d:prop>
            </d:propfind>
        """.trimIndent()
        val xml = propfind(client, base, depth = 1, body)
        parseCollectionSignature(xml).ifBlank { null }
    }.getOrNull()

    /**
     * Парсит PROPFIND-ответ в стабильную подпись «href=ctag» (по книгам, отсортировано).
     * Regex-парсинг (как ICS в проекте) — чистая функция, тестируется без Android XmlPull.
     */
    internal fun parseCollectionSignature(xml: String): String {
        val entries = sortedSetOf<String>()
        for (m in responseBlockPattern.findAll(xml)) {
            val block = m.value
            val href = hrefTagPattern.find(block)?.groupValues?.get(1)?.trim().orEmpty()
            val tag = (
                getctagPattern.find(block)?.groupValues?.get(1)
                    ?: syncTokenPattern.find(block)?.groupValues?.get(1)
                )?.trim().orEmpty()
            if (href.isNotBlank() && tag.isNotBlank()) {
                entries += "$href=$tag"
            }
        }
        return entries.joinToString("\n")
    }

    private val responseBlockPattern = Regex("(?is)<(?:\\w+:)?response\\b.*?</(?:\\w+:)?response>")
    private val hrefTagPattern = Regex("(?is)<(?:\\w+:)?href>(.*?)</(?:\\w+:)?href>")
    private val getctagPattern = Regex("(?is)<(?:\\w+:)?getctag>(.*?)</(?:\\w+:)?getctag>")
    private val syncTokenPattern = Regex("(?is)<(?:\\w+:)?sync-token>(.*?)</(?:\\w+:)?sync-token>")

    fun createContact(
        client: OkHttpClient,
        serverUrl: String,
        userId: String,
        displayName: String,
        email: String,
        phone: String = "",
    ): DavContact {
        val base = davAddressBooksBaseUrl(serverUrl, userId)
        val books = listAddressBooks(client, base)
        val book = books.firstOrNull()
            ?: error("Не найдена адресная книга")
        val (bookUrl, bookName) = book
        val uid = UUID.randomUUID().toString()
        val vcard = buildVCard(uid, displayName, email, phone)
        val url = bookUrl.trimEnd('/') + "/$uid.vcf"
        val req = Request.Builder()
            .url(url)
            .put(vcard.toRequestBody("text/vcard; charset=utf-8".toMediaType()))
            .build()
        client.newCall(req).execute().use { response ->
            if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
                error("Не удалось создать контакт (HTTP ${response.code})")
            }
        }
        return DavContact(
            uid = uid,
            displayName = displayName.trim(),
            email = email.trim(),
            phone = phone.trim(),
            bookName = bookName,
        )
    }

    private fun buildVCard(uid: String, displayName: String, email: String, phone: String): String {
        val fn = displayName.trim()
        val parts = fn.split(Regex("\\s+"), limit = 2)
        val given = parts.getOrNull(0).orEmpty()
        val family = parts.getOrNull(1).orEmpty()
        return buildString {
            append("BEGIN:VCARD\n")
            append("VERSION:3.0\n")
            append("UID:$uid\n")
            append("FN:$fn\n")
            append("N:$family;$given;;;\n")
            if (email.isNotBlank()) {
                append("EMAIL;TYPE=INTERNET:$email\n")
            }
            if (phone.isNotBlank()) {
                append("TEL;TYPE=CELL:$phone\n")
            }
            append("END:VCARD\n")
        }
    }

    private fun listAddressBooks(client: OkHttpClient, baseUrl: String): List<Pair<String, String>> {
        val body = """
            <?xml version="1.0" encoding="utf-8"?>
            <d:propfind xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
              <d:prop><d:displayname/><d:resourcetype/></d:prop>
            </d:propfind>
        """.trimIndent()
        val xml = propfind(client, baseUrl, depth = 1, body)
        val parsed = parseAddressBooks(xml, baseUrl)
        return parsed.ifEmpty {
            listOf(resolveHref(baseUrl, "contacts") to "Contacts")
        }
    }

    private fun fetchContactsFromBook(
        client: OkHttpClient,
        book: Pair<String, String>,
        limit: Int,
    ): List<DavContact> {
        val (href, bookName) = book
        val url = href.trimEnd('/') + "/"
        val body = """
            <?xml version="1.0" encoding="utf-8"?>
            <d:propfind xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
              <d:prop><d:getetag/><card:address-data/></d:prop>
            </d:propfind>
        """.trimIndent()
        val xml = propfind(client, url, depth = 1, body)
        return parseContacts(xml, bookName).take(limit)
    }

    private fun propfind(client: OkHttpClient, url: String, depth: Int, body: String): String {
        val req = Request.Builder()
            .url(url)
            .header("Depth", depth.toString())
            .method("PROPFIND", body.toRequestBody("application/xml; charset=utf-8".toMediaType()))
            .build()
        client.newCall(req).execute().use { response ->
            val code = response.code
            if (code !in 200..299 && code != 207) {
                error("CardDAV error HTTP $code")
            }
            val xml = response.body?.string().orEmpty()
            if (xml.isBlank()) {
                error("CardDAV empty response")
            }
            return xml
        }
    }

    private fun parseAddressBooks(xml: String, baseUrl: String): List<Pair<String, String>> {
        val parser = newParser(xml)
        val out = mutableListOf<Pair<String, String>>()
        val basePath = baseUrl.toDavPath()
        var inResponse = false
        var href = ""
        var displayName = ""
        var isCollection = false
        while (parser.eventType != XmlPullParser.END_DOCUMENT) {
            when (parser.eventType) {
                XmlPullParser.START_TAG -> when (parser.localTag()) {
                    "response" -> {
                        inResponse = true
                        href = ""
                        displayName = ""
                        isCollection = false
                    }
                    "collection" -> if (inResponse) isCollection = true
                    "displayname" -> if (inResponse) displayName = parser.readText().trim()
                    "href" -> if (inResponse) href = parser.readText().trim()
                }
                XmlPullParser.END_TAG -> if (parser.localTag() == "response" && inResponse) {
                    if (isCollection && href.isNotBlank()) {
                        val full = resolveHref(baseUrl, href)
                        val fullPath = full.toDavPath()
                        if (fullPath != basePath &&
                            fullPath.startsWith(basePath) &&
                            !fullPath.contains("/system/")
                        ) {
                            val name = displayName.ifBlank {
                                fullPath.removePrefix(basePath).trim('/').substringAfterLast('/')
                            }
                            out += full to name
                        }
                    }
                    inResponse = false
                }
            }
            parser.next()
        }
        return out
    }

    private fun parseContacts(xml: String, bookName: String): List<DavContact> {
        val parser = newParser(xml)
        val out = mutableListOf<DavContact>()
        var inAddressData = false
        val data = StringBuilder()
        while (parser.eventType != XmlPullParser.END_DOCUMENT) {
            when (parser.eventType) {
                XmlPullParser.START_TAG -> if (parser.localTag() == "address-data") {
                    inAddressData = true
                    data.clear()
                }
                XmlPullParser.TEXT -> if (inAddressData) data.append(parser.text)
                XmlPullParser.END_TAG -> if (parser.localTag() == "address-data" && inAddressData) {
                    val vcard = unfoldVCard(data.toString())
                    val uid = extract(uidPattern, vcard).ifBlank {
                        vcard.hashCode().toString()
                    }
                    val name = resolveContactName(vcard)
                    val allEmails = extractAll(emailPattern, vcard)
                    val allPhones = extractAll(telPattern, vcard)
                    val email = allEmails.firstOrNull().orEmpty()
                    val phone = allPhones.firstOrNull().orEmpty()
                    val photo = parseContactPhoto(vcard)
                    if (name.isNotBlank() || email.isNotBlank()) {
                        out += DavContact(
                            uid = uid,
                            displayName = name,
                            email = email,
                            phone = phone,
                            bookName = bookName,
                            photoBase64 = photo?.base64.orEmpty(),
                            photoMimeType = photo?.mimeType.orEmpty(),
                            organization = formatOrganization(extract(orgPattern, vcard)),
                            title = extract(titlePattern, vcard),
                            address = formatAddress(extract(adrPattern, vcard)),
                            website = extract(urlPattern, vcard),
                            birthday = formatBirthday(extract(bdayPattern, vcard)),
                            emails = allEmails.joinToString("\n"),
                            phones = allPhones.joinToString("\n"),
                        )
                    }
                    inAddressData = false
                }
            }
            parser.next()
        }
        return out
    }

    private fun unfoldVCard(raw: String): String {
        val normalized = raw.replace("\r\n", "\n").replace('\r', '\n')
        val lines = normalized.split('\n')
        val unfolded = StringBuilder()
        for (line in lines) {
            if (line.startsWith(' ') || line.startsWith('\t')) {
                if (unfolded.isNotEmpty()) {
                    unfolded.append(line.drop(1))
                }
            } else {
                if (unfolded.isNotEmpty()) unfolded.append('\n')
                unfolded.append(line)
            }
        }
        return unfolded.toString()
    }

    private fun resolveHref(baseUrl: String, href: String): String {
        if (href.startsWith("http")) return href
        if (href.startsWith("/")) {
            val server = baseUrl.substringBefore("/remote.php")
            return server + href
        }
        return baseUrl.trimEnd('/') + "/" + href.trimStart('/')
    }

    private fun extract(pattern: Pattern, text: String): String {
        val m = pattern.matcher(text)
        return if (m.find()) m.group(1)?.trim().orEmpty() else ""
    }

    private fun extractAll(pattern: Pattern, text: String): List<String> {
        val m = pattern.matcher(text)
        val out = mutableListOf<String>()
        while (m.find()) {
            m.group(1)?.trim()?.takeIf { it.isNotBlank() }?.let { out += it }
        }
        return out.distinct()
    }

    private fun formatOrganization(raw: String): String {
        if (raw.isBlank()) return ""
        return raw.split(';').firstOrNull()?.trim().orEmpty()
    }

    private fun formatAddress(raw: String): String {
        if (raw.isBlank()) return ""
        val parts = raw.split(';')
        return listOf(
            parts.getOrNull(2).orEmpty(),
            parts.getOrNull(3).orEmpty(),
            parts.getOrNull(4).orEmpty(),
            parts.getOrNull(5).orEmpty(),
            parts.getOrNull(6).orEmpty(),
        )
            .map { it.trim() }
            .filter { it.isNotBlank() }
            .joinToString(", ")
    }

    private fun formatBirthday(raw: String): String {
        val value = raw.trim()
        if (value.isBlank()) return ""
        if (value.length == 8 && value.all { it.isDigit() }) {
            return "${value.substring(6, 8)}.${value.substring(4, 6)}.${value.substring(0, 4)}"
        }
        return value
    }

    private fun resolveContactName(vcard: String): String {
        val fn = extract(fnPattern, vcard)
        if (fn.isNotBlank() && !isGenericName(fn)) return fn
        val nRaw = extract(nPattern, vcard)
        if (nRaw.isNotBlank()) {
            val parts = nRaw.split(';')
            val family = parts.getOrNull(0).orEmpty().trim()
            val given = parts.getOrNull(1).orEmpty().trim()
            val additional = parts.getOrNull(2).orEmpty().trim()
            val composed = listOf(given, additional, family)
                .filter { it.isNotBlank() }
                .joinToString(" ")
            if (composed.isNotBlank() && !isGenericName(composed)) return composed
        }
        val email = extract(emailPattern, vcard)
        if (email.isNotBlank()) return email.substringBefore('@')
        return fn.ifBlank { "" }
    }

    private fun isGenericName(name: String): Boolean {
        val normalized = name.trim().lowercase()
        if (normalized.isBlank()) return true
        return genericNames.contains(normalized)
    }

    private data class ParsedContactPhoto(
        val base64: String,
        val mimeType: String,
    )

    private fun parseContactPhoto(vcard: String): ParsedContactPhoto? {
        val line = vcard.lineSequence()
            .firstOrNull { it.startsWith("PHOTO", ignoreCase = true) }
            ?: return null
        val colon = line.indexOf(':')
        if (colon < 0) return null
        val header = line.substring(0, colon)
        val payload = line.substring(colon + 1).trim()
        if (payload.isBlank()) return null

        if (payload.startsWith("data:", ignoreCase = true)) {
            val mime = payload.substringAfter("data:", "")
                .substringBefore(';')
                .ifBlank { "image/jpeg" }
            val base64 = payload.substringAfter("base64,", "")
            return encodePhotoBase64(base64, mime)
        }
        if (header.contains("VALUE=URI", ignoreCase = true)) {
            return null
        }
        val mime = when {
            header.contains("TYPE=PNG", ignoreCase = true) ||
                header.contains("MEDIATYPE=image/png", ignoreCase = true) -> "image/png"
            header.contains("TYPE=GIF", ignoreCase = true) ||
                header.contains("MEDIATYPE=image/gif", ignoreCase = true) -> "image/gif"
            header.contains("TYPE=WEBP", ignoreCase = true) ||
                header.contains("MEDIATYPE=image/webp", ignoreCase = true) -> "image/webp"
            else -> "image/jpeg"
        }
        return encodePhotoBase64(payload, mime)
    }

    private fun encodePhotoBase64(raw: String, mimeType: String): ParsedContactPhoto? {
        val normalized = raw.replace("\\s".toRegex(), "")
        if (normalized.isBlank()) return null
        val bytes = runCatching {
            Base64.getDecoder().decode(normalized)
        }.getOrNull() ?: return null
        if (bytes.isEmpty()) return null
        return ParsedContactPhoto(
            base64 = Base64.getEncoder().encodeToString(bytes),
            mimeType = mimeType,
        )
    }

    private fun newParser(xml: String): XmlPullParser {
        val parser = XmlPullParserFactory.newInstance().newPullParser()
        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true)
        parser.setInput(xml.reader())
        return parser
    }

    private fun XmlPullParser.localTag(): String = name.substringAfter(':')

    private fun XmlPullParser.readText(): String {
        if (next() != XmlPullParser.TEXT) return ""
        return text.orEmpty()
    }

    private fun String.toDavPath(): String {
        val path = if (contains("://")) {
            java.net.URI(this).path.orEmpty()
        } else {
            this
        }
        return try {
            URLDecoder.decode(path, Charsets.UTF_8.name()).trim('/')
        } catch (_: Exception) {
            path.trim('/')
        }
    }
}
