package ru.forbion.f7cloud.core.network

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 CalendarAttendeeData(
    val email: String,
    val displayName: String = "",
    val partStat: String = "NEEDS-ACTION",
    val role: String = "REQ-PARTICIPANT",
    val rsvp: Boolean = true,
)

data class CalendarAlarmData(
    val minutesBefore: Int,
    val action: String = "DISPLAY",
)

data class CalendarEventData(
    val uid: String,
    val summary: String,
    val description: String = "",
    val location: String = "",
    val startEpochMilli: Long,
    val endEpochMilli: Long,
    val allDay: Boolean = false,
    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 = "",
)

object CalendarIcs {
    private val veventBlock = Pattern.compile("BEGIN:VEVENT([\\s\\S]*?)END:VEVENT", Pattern.CASE_INSENSITIVE)

    /** Одно ICS-свойство: имя, параметры (TZID/CN/PARTSTAT…) и значение. Параметры НЕ теряем —
     *  раньше их срезал regex, из-за чего пропадал TZID и схлопывались ATTENDEE. */
    private data class IcsProp(val name: String, val params: Map<String, String>, val value: String)

    fun parseAll(ics: String): List<CalendarEventData> {
        val unfolded = unfold(ics)
        val blocks = veventBlock.matcher(unfolded)
        val out = mutableListOf<CalendarEventData>()
        while (blocks.find()) {
            parseVEventBlock(blocks.group(1) ?: continue)?.let { out += it }
        }
        return out
    }

    fun parseSingle(ics: String): CalendarEventData? {
        return parseAll(ics).firstOrNull()
    }

    fun build(data: CalendarEventData): String = buildString {
        appendLine("BEGIN:VCALENDAR")
        appendLine("VERSION:2.0")
        appendLine("PRODID:-//F7cloud Mobile//EN")
        appendLine("CALSCALE:GREGORIAN")
        append(serializeVEvent(data))
        appendLine("END:VCALENDAR")
    }

    private fun parseVEventBlock(block: String): CalendarEventData? {
        val props = parseProps(block)
        // первое значение на имя — для простых одиночных свойств (SUMMARY/UID/…)
        val lines = props.associate { it.name to it.value }
        val uid = lines["UID"]?.trim().orEmpty()
        if (uid.isBlank()) return null
        val dtStart = props.firstOrNull { it.name == "DTSTART" } ?: return null
        val start = parseIcsInstant(dtStart.value, dtStart.params["TZID"]) ?: return null
        val allDay = dtStart.params["VALUE"].equals("DATE", ignoreCase = true) || !dtStart.value.contains('T')
        val dtEnd = props.firstOrNull { it.name == "DTEND" }
        val end = dtEnd?.let { parseIcsInstant(it.value, it.params["TZID"]) }
            ?: start.plusSeconds(if (allDay) 86400 else 3600)
        // каждый ATTENDEE — со своими параметрами (CN/PARTSTAT/ROLE), не схлопываем
        val attendees = props.filter { it.name == "ATTENDEE" }.mapNotNull { parseAttendee(it) }
        val organizerProp = props.firstOrNull { it.name == "ORGANIZER" }
        val orgEmail = organizerProp?.value?.substringAfter("mailto:", organizerProp.value)?.trim().orEmpty()
        val orgName = organizerProp?.params?.get("CN")?.let(::unescape).orEmpty()
        val alarms = parseAlarms(block)
        val conference = lines.entries
            .firstOrNull { it.key.startsWith("CONFERENCE") }
            ?.value
            ?.trim()
            .orEmpty()
        val location = unescape(lines["LOCATION"].orEmpty())
        val talkUrl = conference.ifBlank {
            if (location.contains("/call/")) location else ""
        }
        return CalendarEventData(
            uid = uid,
            summary = unescape(lines["SUMMARY"].orEmpty()).ifBlank { "(без названия)" },
            description = unescape(lines["DESCRIPTION"].orEmpty()),
            location = location,
            startEpochMilli = start.toEpochMilli(),
            endEpochMilli = end.toEpochMilli(),
            allDay = allDay,
            rrule = lines["RRULE"].orEmpty(),
            categories = lines["CATEGORIES"]?.split(',')?.map { unescape(it.trim()) }?.filter { it.isNotBlank() }.orEmpty(),
            status = lines["STATUS"]?.trim().orEmpty().ifBlank { "CONFIRMED" },
            classification = lines["CLASS"]?.trim().orEmpty().ifBlank { "PUBLIC" },
            attendees = attendees,
            organizerEmail = orgEmail,
            organizerName = orgName,
            alarms = alarms,
            conferenceUri = talkUrl,
        )
    }

    private fun serializeVEvent(data: CalendarEventData): String = buildString {
        val zone = ZoneId.systemDefault()
        val tzId = zone.id.replace(":", "\\:")
        val now = Instant.now()
        appendLine("BEGIN:VEVENT")
        appendLine("UID:${data.uid}")
        appendLine("DTSTAMP:${formatUtc(now)}")
        val startZ = Instant.ofEpochMilli(data.startEpochMilli).atZone(zone)
        val endZ = Instant.ofEpochMilli(data.endEpochMilli).atZone(zone)
        if (data.allDay) {
            appendLine("DTSTART;VALUE=DATE:${startZ.format(DateTimeFormatter.BASIC_ISO_DATE)}")
            appendLine("DTEND;VALUE=DATE:${endZ.format(DateTimeFormatter.BASIC_ISO_DATE)}")
        } else {
            appendLine("DTSTART;TZID=$tzId:${formatLocal(startZ)}")
            appendLine("DTEND;TZID=$tzId:${formatLocal(endZ)}")
        }
        appendLine("SUMMARY:${escape(data.summary)}")
        if (data.description.isNotBlank()) appendLine("DESCRIPTION:${escape(data.description)}")
        if (data.location.isNotBlank()) appendLine("LOCATION:${escape(data.location)}")
        if (data.rrule.isNotBlank()) appendLine("RRULE:${data.rrule}")
        if (data.categories.isNotEmpty()) {
            appendLine("CATEGORIES:${data.categories.joinToString(",") { escape(it) }}")
        }
        appendLine("STATUS:${data.status}")
        appendLine("CLASS:${data.classification}")
        val orgEmail = data.organizerEmail
        if (orgEmail.isNotBlank()) {
            val cn = if (data.organizerName.isNotBlank()) ";CN=${escape(data.organizerName)}" else ""
            appendLine("ORGANIZER;CUTYPE=INDIVIDUAL$cn:mailto:$orgEmail")
        }
        data.attendees.forEach { attendee ->
            val cn = if (attendee.displayName.isNotBlank()) ";CN=${escape(attendee.displayName)}" else ""
            val rsvp = if (attendee.rsvp) ";RSVP=TRUE" else ""
            appendLine(
                "ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=${attendee.role};PARTSTAT=${attendee.partStat}$rsvp$cn:mailto:${attendee.email}",
            )
        }
        val talk = data.conferenceUri.ifBlank {
            if (data.location.contains("/call/")) data.location else ""
        }
        if (talk.isNotBlank()) {
            appendLine("CONFERENCE;FEATURE=PHONE,VIDEO;VALUE=URI:$talk")
            if (data.location.isBlank()) appendLine("LOCATION:$talk")
        }
        data.alarms.forEach { alarm ->
            appendLine("BEGIN:VALARM")
            appendLine("ACTION:${alarm.action}")
            appendLine("TRIGGER:-PT${alarm.minutesBefore}M")
            appendLine("DESCRIPTION:${escape(data.summary)}")
            appendLine("END:VALARM")
        }
        appendLine("END:VEVENT")
    }

    fun newUid(): String = "${UUID.randomUUID()}@f7cloud.mobile"

    /**
     * ICS date-time → Instant с учётом зоны:
     *  - суффикс `Z` → UTC (раньше срезался и трактовался как локальное — баг);
     *  - параметр TZID → указанная зона;
     *  - иначе (floating) → локальная зона устройства;
     *  - только дата (VALUE=DATE, 8 цифр) → начало дня в зоне (TZID или локальной).
     */
    fun parseIcsInstant(raw: String, tzId: String? = null): Instant? {
        val value = raw.trim()
        if (value.isBlank()) return null
        val paramZone = tzId?.let { runCatching { ZoneId.of(it) }.getOrNull() }
        return runCatching {
            when {
                value.contains('T') -> {
                    val hasZ = value.endsWith("Z", ignoreCase = true)
                    val clean = value.trimEnd('Z', 'z').take(15)
                    val ldt = LocalDateTime.parse(clean, DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss"))
                    val zone = when {
                        hasZ -> ZoneOffset.UTC
                        else -> paramZone ?: ZoneId.systemDefault()
                    }
                    ldt.atZone(zone).toInstant()
                }
                value.length >= 8 -> {
                    LocalDate.parse(value.take(8), DateTimeFormatter.BASIC_ISO_DATE)
                        .atStartOfDay(paramZone ?: ZoneId.systemDefault()).toInstant()
                }
                else -> null
            }
        }.getOrNull()
    }

    /** Разбор ICS-строк в свойства с параметрами. Сворачивание строк уже снято в [unfold]. */
    private fun parseProps(block: String): List<IcsProp> {
        val out = mutableListOf<IcsProp>()
        unfold(block).lineSequence().forEach { raw ->
            val line = raw.trim()
            val colon = line.indexOf(':')
            if (colon <= 0) return@forEach
            val head = line.substring(0, colon)      // NAME;PARAM=VAL;PARAM2=VAL
            val value = line.substring(colon + 1)
            val parts = head.split(';')
            val name = parts[0].uppercase()
            val params = parts.drop(1).mapNotNull { p ->
                val eq = p.indexOf('=')
                if (eq <= 0) null else p.substring(0, eq).uppercase() to p.substring(eq + 1).trim('"')
            }.toMap()
            out += IcsProp(name, params, value)
        }
        return out
    }

    private fun parseLines(block: String): Map<String, String> =
        parseProps(block).associate { it.name to it.value }

    private fun parseAttendee(prop: IcsProp): CalendarAttendeeData? {
        val email = prop.value.substringAfter("mailto:", prop.value).trim()
        if (email.isBlank()) return null
        return CalendarAttendeeData(
            email = email,
            displayName = prop.params["CN"]?.let(::unescape).orEmpty(),
            partStat = prop.params["PARTSTAT"] ?: "NEEDS-ACTION",
            role = prop.params["ROLE"] ?: "REQ-PARTICIPANT",
            rsvp = !prop.params["RSVP"].equals("FALSE", ignoreCase = true),
        )
    }

    private fun parseAlarms(block: String): List<CalendarAlarmData> {
        val alarmPattern = Pattern.compile("BEGIN:VALARM([\\s\\S]*?)END:VALARM", Pattern.CASE_INSENSITIVE)
        val matcher = alarmPattern.matcher(block)
        val out = mutableListOf<CalendarAlarmData>()
        while (matcher.find()) {
            val lines = parseLines(matcher.group(1).orEmpty())
            val trigger = lines["TRIGGER"].orEmpty()
            val minutes = parseTriggerMinutes(trigger)
            if (minutes != null) {
                out += CalendarAlarmData(minutesBefore = minutes, action = lines["ACTION"] ?: "DISPLAY")
            }
        }
        return out
    }

    private fun parseTriggerMinutes(trigger: String): Int? {
        val t = trigger.trim()
        val relative = Regex("-PT(\\d+)M", RegexOption.IGNORE_CASE).find(t)?.groupValues?.get(1)?.toIntOrNull()
        if (relative != null) return relative
        val hours = Regex("-PT(\\d+)H", RegexOption.IGNORE_CASE).find(t)?.groupValues?.get(1)?.toIntOrNull()
        if (hours != null) return hours * 60
        val days = Regex("-P(\\d+)D", RegexOption.IGNORE_CASE).find(t)?.groupValues?.get(1)?.toIntOrNull()
        if (days != null) return days * 24 * 60
        return null
    }

    private fun unfold(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 escape(text: String): String =
        text.replace("\\", "\\\\").replace("\n", "\\n").replace(",", "\\,").replace(";", "\\;")

    /** Single-pass: последовательные replace ломались на экранированном бэкслеше (`\\n` → перенос). */
    private fun unescape(text: String): String {
        if (text.indexOf('\\') < 0) return text
        val sb = StringBuilder(text.length)
        var i = 0
        while (i < text.length) {
            val c = text[i]
            if (c == '\\' && i + 1 < text.length) {
                when (val n = text[i + 1]) {
                    'n', 'N' -> sb.append('\n')
                    ',' -> sb.append(',')
                    ';' -> sb.append(';')
                    '\\' -> sb.append('\\')
                    else -> sb.append(n)
                }
                i += 2
            } else {
                sb.append(c)
                i++
            }
        }
        return sb.toString()
    }

    private fun formatUtc(instant: Instant): String =
        instant.atZone(ZoneOffset.UTC).format(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'"))

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