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.URI
import java.net.URLDecoder
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.UUID
import java.util.regex.Pattern

data class DavCalendar(
    val href: String,
    val displayName: String,
    val color: String? = null,
    // CTag коллекции (CalendarServer-расширение, Nextcloud поддерживает):
    // меняется при любом изменении в календаре → ключ инкрементального кэша событий.
    val ctag: String = "",
)

data class DavEvent(
    val uid: String,
    val href: String,
    val etag: String,
    val summary: String,
    val startLabel: String,
    val startEpochMilli: Long,
    val endEpochMilli: Long,
    val calendarName: String,
    val calendarHref: String,
    val allDay: Boolean,
    val lastModifiedEpochMilli: Long,
    val description: String = "",
    val location: String = "",
    val rrule: String = "",
    val categories: List<String> = emptyList(),
    val status: String = "CONFIRMED",
    val classification: String = "PUBLIC",
    val attendees: List<CalendarAttendeeData> = emptyList(),
    val organizerEmail: String = "",
    val organizerName: String = "",
    val alarms: List<CalendarAlarmData> = emptyList(),
    val conferenceUri: String = "",
)

data class DavTrashEvent(
    val uid: String,
    val href: String,
    val etag: String,
    val summary: String,
    val deletedAt: String,
    val calendarUri: String,
    val calendarData: String,
)

data class DavTask(
    val uid: String,
    val href: String,
    val etag: String,
    val summary: String,
    val status: String,
    val due: String,
    val dueRaw: String,
    val priority: Int,
    val percentComplete: Int,
    val description: String,
    val calendarName: String,
    val calendarHref: String,
) {
    val isCompleted: Boolean
        get() = status.equals("COMPLETED", ignoreCase = true) || percentComplete >= 100
}

object CalDavClient {
    private val icsSummary = Pattern.compile("SUMMARY:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val icsDtStart = Pattern.compile("DTSTART[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val icsDtEnd = Pattern.compile("DTEND[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val icsDue = Pattern.compile("DUE[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val icsStatus = Pattern.compile("STATUS:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val icsUid = Pattern.compile("UID:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val icsDtStamp = Pattern.compile("DTSTAMP[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val icsLastModified = Pattern.compile("LAST-MODIFIED[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val icsPriority = Pattern.compile("PRIORITY:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val icsPercent = Pattern.compile("PERCENT-COMPLETE:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val icsDescription = Pattern.compile("DESCRIPTION:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
    private val icsLocation = Pattern.compile("LOCATION:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)

    fun calendarsBase(serverUrl: String, userId: String): String {
        val base = serverUrl.trimEnd('/')
        val encodedUser = userId.trim().trimEnd('/').split('/').joinToString("/") { segment ->
            java.net.URLEncoder.encode(segment, Charsets.UTF_8.name()).replace("+", "%20")
        }
        return "$base/remote.php/dav/calendars/$encodedUser/"
    }

    fun listCalendars(client: OkHttpClient, baseUrl: String): List<DavCalendar> {
        val body = """
            <?xml version="1.0" encoding="utf-8"?>
            <d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:ical="http://apple.com/ns/ical/">
              <d:prop><d:displayname/><d:resourcetype/><ical:calendar-color/><cs:getctag/></d:prop>
            </d:propfind>
        """.trimIndent()
        val xml = propfind(client, baseUrl, depth = 1, body)
        return parseCalendars(xml, baseUrl)
    }

    fun queryEvents(
        client: OkHttpClient,
        calendar: DavCalendar,
        daysAhead: Int = 30,
    ): List<DavEvent> {
        val now = Instant.now()
        return queryEventsInRange(
            client,
            calendar,
            now,
            now.plusSeconds(daysAhead.toLong() * 86400L),
        )
    }

    fun queryEventsInRange(
        client: OkHttpClient,
        calendar: DavCalendar,
        rangeStart: Instant,
        rangeEnd: Instant,
    ): List<DavEvent> = parseEventsXml(queryEventsRawXml(client, calendar, rangeStart, rangeEnd), calendar)

    /** Сырой REPORT-ответ (для кэширования на диске с ключом по CTag). */
    fun queryEventsRawXml(
        client: OkHttpClient,
        calendar: DavCalendar,
        rangeStart: Instant,
        rangeEnd: Instant,
    ): String {
        val startStr = formatCalDavTime(rangeStart)
        val endStr = formatCalDavTime(rangeEnd)
        val body = """
            <?xml version="1.0" encoding="UTF-8"?>
            <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
              <d:prop><d:getetag/><c:calendar-data/></d:prop>
              <c:filter>
                <c:comp-filter name="VCALENDAR">
                  <c:comp-filter name="VEVENT">
                    <c:time-range start="$startStr" end="$endStr"/>
                  </c:comp-filter>
                </c:comp-filter>
              </c:filter>
            </c:calendar-query>
        """.trimIndent()
        val href = calendar.href.trimEnd('/') + "/"
        return report(client, href, body)
    }

    /** Парсинг REPORT-ответа (в т.ч. взятого из кэша). */
    fun parseEventsXml(xml: String, calendar: DavCalendar): List<DavEvent> =
        parseCalendarQueryResponses(xml, calendar)

    fun createEvent(
        client: OkHttpClient,
        calendar: DavCalendar,
        summary: String,
        start: LocalDateTime,
        durationMinutes: Long = 60,
        allDay: Boolean = false,
        description: String = "",
        location: String = "",
        end: LocalDateTime? = null,
    ): String {
        val zone = ZoneId.systemDefault()
        val startZoned = if (allDay) {
            start.toLocalDate().atStartOfDay(zone)
        } else {
            start.atZone(zone)
        }
        val endZoned = when {
            allDay -> start.toLocalDate().plusDays(1).atStartOfDay(zone)
            end != null -> end.atZone(zone)
            else -> startZoned.plusMinutes(durationMinutes)
        }
        val uid = "${UUID.randomUUID()}@f7cloud.mobile"
        val ics = buildVEventIcs(
            uid = uid,
            summary = summary,
            startEpochMilli = startZoned.toInstant().toEpochMilli(),
            endEpochMilli = endZoned.toInstant().toEpochMilli(),
            allDay = allDay,
            description = description,
            location = location,
        )
        val url = calendar.href.trimEnd('/') + "/$uid.ics"
        val req = Request.Builder()
            .url(url)
            .header("If-None-Match", "*")
            .put(ics.toRequestBody("text/calendar; charset=utf-8".toMediaType()))
            .build()
        client.newCall(req).execute().use { response ->
            if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
                error("CalDAV create event HTTP ${response.code}")
            }
        }
        return uid
    }

    fun updateEvent(
        client: OkHttpClient,
        calendar: DavCalendar,
        href: String,
        etag: String,
        data: CalendarEventData,
    ) {
        putEventIcs(client, href, etag.ifBlank { "*" }, CalendarIcs.build(data))
    }

    fun updateEvent(
        client: OkHttpClient,
        calendar: DavCalendar,
        href: String,
        etag: String,
        uid: String,
        summary: String,
        startEpochMilli: Long,
        endEpochMilli: Long,
        allDay: Boolean,
        description: String = "",
        location: String = "",
    ) {
        updateEvent(
            client,
            calendar,
            href,
            etag,
            CalendarEventData(
                uid = uid,
                summary = summary,
                description = description,
                location = location,
                startEpochMilli = startEpochMilli,
                endEpochMilli = endEpochMilli,
                allDay = allDay,
            ),
        )
    }

    fun deleteEvent(client: OkHttpClient, href: String, etag: String) {
        val req = Request.Builder()
            .url(href)
            .header("If-Match", etag)
            .delete()
            .build()
        client.newCall(req).execute().use { response ->
            if (response.code !in 200..299 && response.code != 204 && response.code != 404) {
                error("CalDAV delete event HTTP ${response.code}")
            }
        }
    }

    fun fetchEventByHref(client: OkHttpClient, href: String): DavEvent? {
        val body = """
            <?xml version="1.0" encoding="utf-8"?>
            <d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
              <d:prop><d:getetag/><c:calendar-data/></d:prop>
            </d:propfind>
        """.trimIndent()
        val xml = propfind(client, href, depth = 0, body)
        val cal = DavCalendar(href.substringBeforeLast('/').substringBeforeLast('/') + "/", "")
        return parseCalendarQueryResponses(xml, cal).firstOrNull()
    }

    fun putEventIcs(
        client: OkHttpClient,
        url: String,
        etag: String?,
        ics: String,
    ) {
        val builder = Request.Builder()
            .url(url)
            .put(ics.toRequestBody("text/calendar; charset=utf-8".toMediaType()))
        if (!etag.isNullOrBlank()) {
            builder.header("If-Match", etag)
        } else {
            builder.header("If-None-Match", "*")
        }
        client.newCall(builder.build()).execute().use { response ->
            if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
                error("CalDAV put event HTTP ${response.code}")
            }
        }
    }

    fun createCalendar(
        client: OkHttpClient,
        baseUrl: String,
        calendarUri: String,
        displayName: String,
        withTasks: Boolean = false,
    ): DavCalendar {
        val url = baseUrl.trimEnd('/') + "/" + calendarUri.trim('/') + "/"
        val components = if (withTasks) {
            """<cal:comp name="VEVENT"/><cal:comp name="VTODO"/>"""
        } else {
            """<cal:comp name="VEVENT"/>"""
        }
        val body = """
            <?xml version="1.0" encoding="utf-8"?>
            <d:mkcalendar xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav">
              <d:set>
                <d:prop>
                  <d:displayname>${escapeXml(displayName)}</d:displayname>
                  <cal:supported-calendar-component-set>$components</cal:supported-calendar-component-set>
                </d:prop>
              </d:set>
            </d:mkcalendar>
        """.trimIndent()
        val req = Request.Builder()
            .url(url)
            .method("MKCALENDAR", body.toRequestBody("application/xml; charset=utf-8".toMediaType()))
            .build()
        client.newCall(req).execute().use { response ->
            if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
                error("CalDAV create calendar HTTP ${response.code}")
            }
        }
        return DavCalendar(href = url, displayName = displayName)
    }

    fun createSubscription(
        client: OkHttpClient,
        baseUrl: String,
        subscriptionUri: String,
        displayName: String,
        sourceUrl: String,
    ): DavCalendar {
        val url = baseUrl.trimEnd('/') + "/" + subscriptionUri.trim('/') + "/"
        val body = """
            <?xml version="1.0" encoding="utf-8"?>
            <d:mkcol xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
              <d:set>
                <d:prop>
                  <d:displayname>${escapeXml(displayName)}</d:displayname>
                  <d:resourcetype><d:collection/><cs:subscribed/></d:resourcetype>
                  <cs:source><d:href>${escapeXml(sourceUrl)}</d:href></cs:source>
                </d:prop>
              </d:set>
            </d:mkcol>
        """.trimIndent()
        val req = Request.Builder()
            .url(url)
            .method("MKCOL", body.toRequestBody("application/xml; charset=utf-8".toMediaType()))
            .build()
        client.newCall(req).execute().use { response ->
            if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
                error("CalDAV subscribe calendar HTTP ${response.code}")
            }
        }
        return DavCalendar(href = url, displayName = displayName)
    }

    fun listTrash(client: OkHttpClient, baseUrl: String): List<DavTrashEvent> {
        val trashUrl = baseUrl.trimEnd('/') + "/trashbin/objects/"
        val body = """
            <?xml version="1.0" encoding="utf-8"?>
            <d:propfind xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav">
              <d:prop>
                <d:getetag/>
                <c:calendar-data/>
                <nc:deleted-at/>
                <nc:calendar-uri/>
              </d:prop>
            </d:propfind>
        """.trimIndent()
        val xml = propfind(client, trashUrl, depth = 1, body)
        return parseTrashResponses(xml, trashUrl)
    }

    fun restoreTrashEvent(client: OkHttpClient, trashHref: String, objectName: String) {
        val base = trashHref.substringBefore("/trashbin/")
        val destination = "$base/trashbin/restore/$objectName"
        moveResource(client, trashHref, destination)
    }

    fun purgeTrashEvent(client: OkHttpClient, href: String, etag: String) {
        deleteEvent(client, href, etag)
    }

    fun importEvents(
        client: OkHttpClient,
        calendar: DavCalendar,
        icsContent: String,
    ): Int {
        val events = CalendarIcs.parseAll(icsContent)
        var count = 0
        for (event in events) {
            val uid = event.uid.ifBlank { CalendarIcs.newUid() }
            val url = calendar.href.trimEnd('/') + "/$uid.ics"
            putEventIcs(client, url, null, CalendarIcs.build(event.copy(uid = uid)))
            count++
        }
        return count
    }

    private fun moveResource(client: OkHttpClient, sourceHref: String, destinationHref: String) {
        val req = Request.Builder()
            .url(sourceHref)
            .method("MOVE", null)
            .header("Destination", destinationHref)
            .build()
        client.newCall(req).execute().use { response ->
            if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
                error("CalDAV move HTTP ${response.code}")
            }
        }
    }

    private fun parseTrashResponses(xml: String, trashUrl: String): List<DavTrashEvent> {
        val parser = newParser(xml)
        val out = mutableListOf<DavTrashEvent>()
        var inResponse = false
        var href = ""
        var etag = ""
        var deletedAt = ""
        var calendarUri = ""
        var calendarData = StringBuilder()
        var inCalendarData = false
        while (parser.eventType != XmlPullParser.END_DOCUMENT) {
            when (parser.eventType) {
                XmlPullParser.START_TAG -> when (parser.localTag()) {
                    "response" -> {
                        inResponse = true
                        href = ""
                        etag = ""
                        deletedAt = ""
                        calendarUri = ""
                        calendarData = StringBuilder()
                    }
                    "href" -> if (inResponse) href = parser.readText().trim()
                    "getetag" -> if (inResponse) etag = parser.readText().trim().trim('"')
                    "deleted-at" -> if (inResponse) deletedAt = parser.readText().trim()
                    "calendar-uri" -> if (inResponse) calendarUri = parser.readText().trim()
                    "calendar-data" -> {
                        inCalendarData = true
                        calendarData = StringBuilder()
                    }
                }
                XmlPullParser.TEXT -> if (inCalendarData) calendarData.append(parser.text)
                XmlPullParser.END_TAG -> when (parser.localTag()) {
                    "calendar-data" -> inCalendarData = false
                    "response" -> {
                        if (inResponse && href.isNotBlank() && href.endsWith(".ics")) {
                            val fullHref = resolveHref(trashUrl, href)
                            val parsed = CalendarIcs.parseSingle(calendarData.toString())
                            out += DavTrashEvent(
                                uid = parsed?.uid ?: href.substringAfterLast('/').removeSuffix(".ics"),
                                href = fullHref,
                                etag = etag,
                                summary = parsed?.summary ?: "(без названия)",
                                deletedAt = deletedAt,
                                calendarUri = calendarUri,
                                calendarData = calendarData.toString(),
                            )
                        }
                        inResponse = false
                    }
                }
            }
            parser.next()
        }
        return out
    }

    private fun escapeXml(text: String): String =
        text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

    private fun buildVEventIcs(data: CalendarEventData): String = CalendarIcs.build(data)

    private fun buildVEventIcs(
        uid: String,
        summary: String,
        startEpochMilli: Long,
        endEpochMilli: Long,
        allDay: Boolean,
        description: String = "",
        location: String = "",
    ): String = CalendarIcs.build(
        CalendarEventData(
            uid = uid,
            summary = summary,
            description = description,
            location = location,
            startEpochMilli = startEpochMilli,
            endEpochMilli = endEpochMilli,
            allDay = allDay,
        ),
    )

    private fun parseCalendarQueryResponses(xml: String, calendar: DavCalendar): List<DavEvent> {
        val parser = newParser(xml)
        val out = mutableListOf<DavEvent>()
        var inResponse = false
        var href = ""
        var etag = ""
        var calendarData = StringBuilder()
        var inCalendarData = false
        val calendarHref = calendar.href
        val calendarName = calendar.displayName
        while (parser.eventType != XmlPullParser.END_DOCUMENT) {
            when (parser.eventType) {
                XmlPullParser.START_TAG -> when (parser.localTag()) {
                    "response" -> {
                        inResponse = true
                        href = ""
                        etag = ""
                        calendarData = StringBuilder()
                    }
                    "href" -> if (inResponse) href = parser.readText().trim()
                    "getetag" -> if (inResponse) etag = parser.readText().trim().trim('"')
                    "calendar-data" -> {
                        inCalendarData = true
                        calendarData = StringBuilder()
                    }
                }
                XmlPullParser.TEXT -> if (inCalendarData) calendarData.append(parser.text)
                XmlPullParser.END_TAG -> when (parser.localTag()) {
                    "calendar-data" -> inCalendarData = false
                    "response" -> {
                        if (inResponse && href.isNotBlank() && calendarData.isNotEmpty()) {
                            val fullHref = resolveHref(calendarHref, href)
                            parseVEvent(
                                ics = calendarData.toString(),
                                calendarName = calendarName,
                                calendarHref = calendarHref,
                                href = fullHref,
                                etag = etag,
                            )?.let { out += it }
                        }
                        inResponse = false
                    }
                }
            }
            parser.next()
        }
        return out
    }

    private fun parseVEvent(
        ics: String,
        calendarName: String,
        calendarHref: String,
        href: String,
        etag: String,
    ): DavEvent? {
        val parsed = CalendarIcs.parseSingle(ics) ?: return null
        val startRaw = extractIcs(icsDtStart, ics)
        val lastModified = listOf(
            parseIcsInstant(extractIcs(icsDtStamp, ics)),
            parseIcsInstant(extractIcs(icsLastModified, ics)),
        ).filterNotNull().maxOrNull() ?: Instant.ofEpochMilli(parsed.startEpochMilli)
        return DavEvent(
            uid = parsed.uid,
            href = href,
            etag = etag,
            summary = parsed.summary,
            startLabel = formatIcsDateTime(startRaw.ifBlank { "" }),
            startEpochMilli = parsed.startEpochMilli,
            endEpochMilli = parsed.endEpochMilli,
            calendarName = calendarName,
            calendarHref = calendarHref,
            allDay = parsed.allDay,
            lastModifiedEpochMilli = lastModified.toEpochMilli(),
            description = parsed.description,
            location = parsed.location,
            rrule = parsed.rrule,
            categories = parsed.categories,
            status = parsed.status,
            classification = parsed.classification,
            attendees = parsed.attendees,
            organizerEmail = parsed.organizerEmail,
            organizerName = parsed.organizerName,
            alarms = parsed.alarms,
            conferenceUri = parsed.conferenceUri,
        )
    }

    // Единый парсер дат — в CalendarIcs (учитывает Z/TZID/floating). Раньше здесь была
    // расходящаяся копия (трактовала всё как UTC) — источник рассинхрона календаря и задач.
    fun parseIcsInstant(raw: String): Instant? = CalendarIcs.parseIcsInstant(raw)

    private fun formatIcsUtc(instant: Instant): String = formatCalDavTime(instant)

    private fun formatIcsLocal(zoned: ZonedDateTime): String =
        zoned.format(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss"))

    private fun escapeIcsText(text: String): String =
        text.replace("\\", "\\\\")
            .replace("\n", "\\n")
            .replace(",", "\\,")
            .replace(";", "\\;")

    private fun unescapeIcsText(text: String): String =
        text.replace("\\n", "\n")
            .replace("\\,", ",")
            .replace("\\;", ";")
            .replace("\\\\", "\\")

    fun queryTasks(client: OkHttpClient, calendar: DavCalendar): List<DavTask> {
        val body = """
            <?xml version="1.0" encoding="UTF-8"?>
            <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
              <d:prop><d:getetag/><c:calendar-data/></d:prop>
              <c:filter>
                <c:comp-filter name="VCALENDAR">
                  <c:comp-filter name="VTODO"/>
                </c:comp-filter>
              </c:filter>
            </c:calendar-query>
        """.trimIndent()
        val href = calendar.href.trimEnd('/') + "/"
        val xml = report(client, href, body)
        return parseCalendarQueryResponsesForTasks(xml, calendar)
    }

    fun createTask(
        client: OkHttpClient,
        calendar: DavCalendar,
        summary: String,
        dueDate: LocalDate? = null,
        priority: Int = 0,
        description: String = "",
    ): DavTask {
        val uid = "${UUID.randomUUID()}@f7cloud.mobile"
        val ics = buildVTodoIcs(
            uid = uid,
            summary = summary,
            status = "NEEDS-ACTION",
            dueRaw = dueDate?.format(DateTimeFormatter.BASIC_ISO_DATE).orEmpty(),
            priority = priority,
            percentComplete = 0,
            description = description,
        )
        val url = calendar.href.trimEnd('/') + "/$uid.ics"
        val req = Request.Builder()
            .url(url)
            .header("If-None-Match", "*")
            .put(ics.toRequestBody("text/calendar; charset=utf-8".toMediaType()))
            .build()
        client.newCall(req).execute().use { response ->
            if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
                error("CalDAV create task HTTP ${response.code}")
            }
        }
        val dueRaw = dueDate?.format(DateTimeFormatter.BASIC_ISO_DATE).orEmpty()
        return DavTask(
            uid = uid,
            href = url,
            etag = "",
            summary = summary,
            status = "NEEDS-ACTION",
            due = formatIcsDateTime(dueRaw),
            dueRaw = dueRaw,
            priority = priority,
            percentComplete = 0,
            description = description,
            calendarName = calendar.displayName,
            calendarHref = calendar.href,
        )
    }

    fun updateTask(
        client: OkHttpClient,
        task: DavTask,
        summary: String = task.summary,
        status: String = task.status,
        dueRaw: String = task.dueRaw,
        priority: Int = task.priority,
        percentComplete: Int = task.percentComplete,
        description: String = task.description,
    ) {
        val ics = buildVTodoIcs(
            uid = task.uid,
            summary = summary,
            status = status,
            dueRaw = dueRaw,
            priority = priority,
            percentComplete = percentComplete,
            description = description,
        )
        val req = Request.Builder()
            .url(task.href)
            .header("If-Match", task.etag.ifBlank { "*" })
            .put(ics.toRequestBody("text/calendar; charset=utf-8".toMediaType()))
            .build()
        client.newCall(req).execute().use { response ->
            if (response.code !in 200..299 && response.code != 204) {
                error("CalDAV update task HTTP ${response.code}")
            }
        }
    }

    fun deleteTask(client: OkHttpClient, task: DavTask) {
        val req = Request.Builder()
            .url(task.href)
            .header("If-Match", task.etag.ifBlank { "*" })
            .delete()
            .build()
        client.newCall(req).execute().use { response ->
            if (response.code !in 200..299 && response.code != 204 && response.code != 404) {
                error("CalDAV delete task HTTP ${response.code}")
            }
        }
    }

    fun toggleTaskComplete(client: OkHttpClient, task: DavTask, completed: Boolean) {
        updateTask(
            client = client,
            task = task,
            status = if (completed) "COMPLETED" else "NEEDS-ACTION",
            percentComplete = if (completed) 100 else 0,
        )
    }

    /** Человекочитаемая дата из DTSTART/DUE (20250603T100000Z → 03.06.2026 10:00). */
    fun formatIcsDateTime(raw: String): String {
        val value = raw.trim()
        if (value.isBlank()) return ""
        return runCatching {
            when {
                value.length >= 15 && value[8] == 'T' -> {
                    val dt = LocalDateTime.parse(
                        value.take(15),
                        DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss"),
                    )
                    dt.atOffset(ZoneOffset.UTC).format(
                        DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"),
                    )
                }
                value.length >= 8 -> {
                    LocalDate.parse(value.take(8), DateTimeFormatter.BASIC_ISO_DATE)
                        .format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
                }
                else -> value
            }
        }.getOrDefault(value)
    }

    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()
        return execute(client, req)
    }

    private fun report(client: OkHttpClient, url: String, body: String): String {
        val req = Request.Builder()
            .url(url)
            .header("Depth", "1")
            .method("REPORT", body.toRequestBody("application/xml; charset=utf-8".toMediaType()))
            .build()
        return execute(client, req)
    }

    private fun execute(client: OkHttpClient, req: Request): String {
        client.newCall(req).execute().use { response ->
            val code = response.code
            if (code !in 200..299 && code != 207) {
                error("CalDAV error HTTP $code")
            }
            val xml = response.body?.string().orEmpty()
            if (xml.isBlank()) {
                error("CalDAV empty response")
            }
            return xml
        }
    }

    private fun formatCalDavTime(instant: java.time.Instant): String {
        return DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'")
            .withZone(ZoneOffset.UTC)
            .format(instant)
    }

    private fun parseCalendars(xml: String, baseUrl: String): List<DavCalendar> {
        val parser = newParser(xml)
        val out = mutableListOf<DavCalendar>()
        val basePath = baseUrl.toDavPath()
        var inResponse = false
        var href = ""
        var displayName = ""
        var calendarColor: String? = null
        var ctag = ""
        var isCollection = false
        while (parser.eventType != XmlPullParser.END_DOCUMENT) {
            when (parser.eventType) {
                XmlPullParser.START_TAG -> when (parser.localTag()) {
                    "response" -> {
                        inResponse = true
                        href = ""
                        displayName = ""
                        calendarColor = null
                        ctag = ""
                        isCollection = false
                    }
                    "collection" -> if (inResponse) isCollection = true
                    "displayname" -> if (inResponse) displayName = parser.readText().trim()
                    "calendar-color" -> if (inResponse) calendarColor = parser.readText().trim()
                    "getctag" -> if (inResponse) ctag = 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) &&
                            isUserCalendar(fullPath, basePath)
                        ) {
                            val name = displayName.ifBlank {
                                fullPath.removePrefix(basePath).trim('/').substringAfterLast('/')
                            }
                            out += DavCalendar(href = full, displayName = name, color = calendarColor, ctag = ctag)
                        }
                    }
                    inResponse = false
                }
            }
            parser.next()
        }
        return out.distinctBy { it.href }
    }

    private fun isUserCalendar(fullPath: String, basePath: String): Boolean {
        val rel = fullPath.removePrefix(basePath).trim('/').lowercase()
        if (rel.isBlank()) return false
        val segment = rel.substringBefore('/')
        return segment !in SKIP_CALENDAR_SEGMENTS
    }

    private val SKIP_CALENDAR_SEGMENTS = setOf(
        "inbox",
        "outbox",
        "trashbin",
        "archive",
        "deleted",
        "system",
        "contact_birthdays",
        "contacts",
    )

    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 parseCalendarQueryResponsesForTasks(xml: String, calendar: DavCalendar): List<DavTask> {
        val parser = newParser(xml)
        val out = mutableListOf<DavTask>()
        var inResponse = false
        var href = ""
        var etag = ""
        var calendarData = StringBuilder()
        var inCalendarData = false
        val calendarHref = calendar.href
        val calendarName = calendar.displayName
        while (parser.eventType != XmlPullParser.END_DOCUMENT) {
            when (parser.eventType) {
                XmlPullParser.START_TAG -> when (parser.localTag()) {
                    "response" -> {
                        inResponse = true
                        href = ""
                        etag = ""
                        calendarData = StringBuilder()
                    }
                    "href" -> if (inResponse) href = parser.readText().trim()
                    "getetag" -> if (inResponse) etag = parser.readText().trim().trim('"')
                    "calendar-data" -> {
                        inCalendarData = true
                        calendarData = StringBuilder()
                    }
                }
                XmlPullParser.TEXT -> if (inCalendarData) calendarData.append(parser.text)
                XmlPullParser.END_TAG -> when (parser.localTag()) {
                    "calendar-data" -> inCalendarData = false
                    "response" -> {
                        if (inResponse && href.isNotBlank() && calendarData.isNotEmpty()) {
                            val fullHref = resolveHref(calendarHref, href)
                            parseVTodo(
                                ics = calendarData.toString(),
                                calendarName = calendarName,
                                calendarHref = calendarHref,
                                href = fullHref,
                                etag = etag,
                            )?.let { out += it }
                        }
                        inResponse = false
                    }
                }
            }
            parser.next()
        }
        return out
    }

    private fun parseVTodo(
        ics: String,
        calendarName: String,
        calendarHref: String,
        href: String,
        etag: String,
    ): DavTask? {
        val uid = extractIcs(icsUid, ics)
        if (uid.isBlank()) return null
        val dueRaw = extractIcs(icsDue, ics)
        val percent = extractIcs(icsPercent, ics).toIntOrNull() ?: 0
        val status = extractIcs(icsStatus, ics).ifBlank { "NEEDS-ACTION" }
        return DavTask(
            uid = uid,
            href = href,
            etag = etag,
            summary = extractIcs(icsSummary, ics).ifBlank { "(без названия)" },
            status = status,
            due = formatIcsDateTime(dueRaw),
            dueRaw = dueRaw,
            priority = extractIcs(icsPriority, ics).toIntOrNull() ?: 0,
            percentComplete = percent,
            description = extractIcs(icsDescription, ics),
            calendarName = calendarName,
            calendarHref = calendarHref,
        )
    }

    private fun buildVTodoIcs(
        uid: String,
        summary: String,
        status: String,
        dueRaw: String,
        priority: Int,
        percentComplete: Int,
        description: String,
    ): String {
        val now = Instant.now()
        return buildString {
            appendLine("BEGIN:VCALENDAR")
            appendLine("VERSION:2.0")
            appendLine("PRODID:-//F7cloud Mobile//EN")
            appendLine("BEGIN:VTODO")
            appendLine("UID:$uid")
            appendLine("DTSTAMP:${formatIcsUtc(now)}")
            appendLine("SUMMARY:${escapeIcsText(summary)}")
            appendLine("STATUS:$status")
            if (dueRaw.isNotBlank()) {
                if (dueRaw.contains('T')) {
                    appendLine("DUE:${dueRaw.replace("Z", "", ignoreCase = true)}")
                } else {
                    appendLine("DUE;VALUE=DATE:${dueRaw.take(8)}")
                }
            }
            if (priority > 0) {
                appendLine("PRIORITY:$priority")
            }
            if (percentComplete > 0) {
                appendLine("PERCENT-COMPLETE:$percentComplete")
            }
            if (description.isNotBlank()) {
                appendLine("DESCRIPTION:${escapeIcsText(description)}")
            }
            appendLine("END:VTODO")
            appendLine("END:VCALENDAR")
        }
    }

    private fun <T> parseCalendarData(xml: String, map: (String) -> T?): List<T> {
        val parser = newParser(xml)
        val out = mutableListOf<T>()
        var inCalendarData = false
        val data = StringBuilder()
        while (parser.eventType != XmlPullParser.END_DOCUMENT) {
            when (parser.eventType) {
                XmlPullParser.START_TAG -> if (parser.localTag() == "calendar-data") {
                    inCalendarData = true
                    data.clear()
                }
                XmlPullParser.TEXT -> if (inCalendarData) data.append(parser.text)
                XmlPullParser.END_TAG -> if (parser.localTag() == "calendar-data" && inCalendarData) {
                    val ics = data.toString()
                    map(ics)?.let { out += it }
                    inCalendarData = false
                }
            }
            parser.next()
        }
        return out
    }

    /** Marker in Android event description for sync mapping. */
    const val F7CLOUD_UID_MARKER_PREFIX = "F7CLOUD_UID:"

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

    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("://")) {
            URI(this).path.orEmpty()
        } else {
            this
        }
        return try {
            URLDecoder.decode(path, Charsets.UTF_8.name()).trim('/')
        } catch (_: Exception) {
            path.trim('/')
        }
    }
}
