package ru.forbion.f7cloud.feature.calendar

import okhttp3.OkHttpClient
import ru.forbion.f7cloud.core.auth.AuthSession
import ru.forbion.f7cloud.core.auth.OcsUserResolver
import ru.forbion.f7cloud.core.network.CalendarAlarmData
import ru.forbion.f7cloud.core.network.CalendarAttendeeData
import ru.forbion.f7cloud.core.network.CalendarEventData
import ru.forbion.f7cloud.core.network.CalendarIcs
import ru.forbion.f7cloud.core.network.CalDavClient
import ru.forbion.f7cloud.core.network.DavCalendar
import ru.forbion.f7cloud.core.network.DavEvent
import ru.forbion.f7cloud.core.network.DavTask
import ru.forbion.f7cloud.core.network.DavTrashEvent
import ru.forbion.f7cloud.core.network.NetworkFactory
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.YearMonth
import java.time.ZoneId
import java.time.ZoneOffset

class CalendarRepository(
    private val apiClient: CalendarApiClient = CalendarApiClient(),
    // Кэш событий по CTag (null — без кэша, поведение как раньше)
    private val eventsCache: CalendarEventsCache? = null,
) {
    fun listCalendars(session: AuthSession): List<DavCalendar> = openDavContext(session).calendars

    fun loadMonth(session: AuthSession, month: YearMonth, visibleHrefs: Set<String>): List<CalendarEventItem> {
        return loadRange(session, monthGridStart(month), monthGridEnd(month), visibleHrefs)
    }

    fun loadRange(
        session: AuthSession,
        rangeStart: LocalDate,
        rangeEnd: LocalDate,
        visibleHrefs: Set<String>,
    ): List<CalendarEventItem> {
        val ctx = openDavContext(session)
        val startInstant = rangeStart.atStartOfDay(ZoneOffset.UTC).toInstant()
        val endInstant = rangeEnd.plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant()
        return fetchEvents(ctx, startInstant, endInstant, visibleHrefs, accountKey(session))
    }

    fun loadUnscheduledTasks(session: AuthSession): List<CalendarTaskItem> {
        val ctx = openDavContext(session)
        val out = mutableListOf<CalendarTaskItem>()
        for (cal in ctx.calendars.take(8)) {
            runCatching { CalDavClient.queryTasks(ctx.client, cal) }
                .onSuccess { tasks ->
                    out += tasks.filter { !it.isCompleted && it.dueRaw.isBlank() }
                        .map { it.toTaskItem() }
                }
        }
        return out.distinctBy { it.uid }
    }

    fun loadTrash(session: AuthSession): List<CalendarTrashItem> {
        val ctx = openDavContext(session)
        val userBase = CalDavClient.calendarsBase(
            session.serverUrl,
            session.davUserId ?: OcsUserResolver.resolveDavUserId(session),
        )
        return CalDavClient.listTrash(ctx.client, userBase).map { it.toTrashItem() }
    }

    fun restoreTrashItem(session: AuthSession, item: CalendarTrashItem) {
        val ctx = openDavContext(session)
        val name = item.href.substringAfterLast('/')
        CalDavClient.restoreTrashEvent(ctx.client, item.href, name)
    }

    fun purgeTrashItem(session: AuthSession, item: CalendarTrashItem) {
        val ctx = openDavContext(session)
        CalDavClient.purgeTrashEvent(ctx.client, item.href, item.etag.ifBlank { "*" })
    }

    fun createCalendar(session: AuthSession, name: String, withTasks: Boolean): CalendarBookItem {
        val ctx = openDavContext(session)
        val uri = slugify(name) + "-" + System.currentTimeMillis().toString(36)
        val cal = CalDavClient.createCalendar(ctx.client, calendarsRoot(session), uri, name, withTasks)
        return CalendarBookItem(cal.href, cal.displayName, visible = true, isSubscription = false)
    }

    fun subscribeCalendar(session: AuthSession, name: String, sourceUrl: String): CalendarBookItem {
        val ctx = openDavContext(session)
        val uri = "sub-" + System.currentTimeMillis().toString(36)
        val cal = CalDavClient.createSubscription(ctx.client, calendarsRoot(session), uri, name, sourceUrl)
        return CalendarBookItem(cal.href, cal.displayName, visible = true, isSubscription = true)
    }

    fun importIcs(session: AuthSession, calendarHref: String, icsContent: String): Int {
        val ctx = openDavContext(session)
        val calendar = ctx.calendars.firstOrNull { it.href == calendarHref } ?: ctx.defaultCalendar
        return CalDavClient.importEvents(ctx.client, calendar, icsContent)
    }

    fun searchAttendees(session: AuthSession, query: String) = apiClient.searchAttendees(session, query)

    fun searchLocations(session: AuthSession, query: String) = apiClient.searchLocations(session, query)

    fun saveUserSetting(session: AuthSession, key: String, value: String) = apiClient.setConfig(session, key, value)

    fun createTalkRoom(session: AuthSession, roomName: String): String = apiClient.createTalkRoom(session, roomName)

    fun syncTalkParticipants(session: AuthSession, callUrl: String, emails: List<String>) {
        apiClient.addTalkParticipants(session, callUrl, emails)
    }

    fun saveEvent(session: AuthSession, draft: CalendarEventDraft, existing: CalendarEventItem? = null): CalendarEventItem {
        val ctx = openDavContext(session)
        val calendar = ctx.calendars.firstOrNull { it.href == draft.calendarHref } ?: ctx.defaultCalendar
        var talkUrl = draft.talkRoomUrl
        if (draft.addTalkRoom && talkUrl.isBlank()) {
            talkUrl = apiClient.createTalkRoom(session, draft.title)
            val emails = draft.attendees.map { it.email }.filter { it.isNotBlank() }
            if (emails.isNotEmpty()) apiClient.addTalkParticipants(session, talkUrl, emails)
        }
        val data = draft.toEventData(existing?.uid, talkUrl)
        if (existing == null) {
            val url = calendar.href.trimEnd('/') + "/${data.uid}.ics"
            CalDavClient.putEventIcs(ctx.client, url, null, CalendarIcs.build(data))
            return data.toItem(calendar.displayName, calendar.href, url, "")
        } else {
            CalDavClient.updateEvent(ctx.client, calendar, existing.href, existing.etag, data)
            return existing.copy(
                summary = data.summary,
                startEpochMilli = data.startEpochMilli,
                endEpochMilli = data.endEpochMilli,
                allDay = data.allDay,
                description = data.description,
                location = data.location.ifBlank { talkUrl },
                rrule = data.rrule,
                categories = data.categories,
                status = data.status,
                classification = data.classification,
                attendees = data.attendees.map { it.toItem() },
                talkRoomUrl = talkUrl,
                alarms = data.alarms.map { CalendarReminderItem(it.minutesBefore, it.action) },
            )
        }
    }

    fun deleteEvent(session: AuthSession, event: CalendarEventItem) {
        val ctx = openDavContext(session)
        CalDavClient.deleteEvent(ctx.client, event.href, event.etag.ifBlank { "*" })
    }

    fun duplicateEvent(session: AuthSession, event: CalendarEventItem, day: LocalDate): CalendarEventItem {
        val ctx = openDavContext(session)
        val calendar = ctx.calendars.firstOrNull { it.href == event.calendarHref } ?: ctx.defaultCalendar
        val zone = ZoneId.systemDefault()
        val start = Instant.ofEpochMilli(event.startEpochMilli).atZone(zone)
        val end = Instant.ofEpochMilli(event.endEpochMilli).atZone(zone)
        val duration = java.time.Duration.between(start, end)
        val newStart = if (event.allDay) day.atStartOfDay(zone) else day.atTime(start.toLocalTime()).atZone(zone)
        val newEnd = if (event.allDay) day.plusDays(1).atStartOfDay(zone) else newStart.plus(duration)
        val data = CalendarEventData(
            uid = CalendarIcs.newUid(),
            summary = event.summary,
            description = event.description,
            location = event.location,
            startEpochMilli = newStart.toInstant().toEpochMilli(),
            endEpochMilli = newEnd.toInstant().toEpochMilli(),
            allDay = event.allDay,
            rrule = event.rrule,
            categories = event.categories,
            status = event.status,
            classification = event.classification,
            attendees = event.attendees.map { it.toData() },
            alarms = event.alarms.map { CalendarAlarmData(it.minutesBefore, it.action) },
            conferenceUri = event.talkRoomUrl,
        )
        val url = calendar.href.trimEnd('/') + "/${data.uid}.ics"
        CalDavClient.putEventIcs(ctx.client, url, null, CalendarIcs.build(data))
        return data.toItem(calendar.displayName, calendar.href, url, "")
    }

    fun respondToInvite(session: AuthSession, event: CalendarEventItem, partStat: String, userEmail: String) {
        val ctx = openDavContext(session)
        val calendar = ctx.calendars.firstOrNull { it.href == event.calendarHref } ?: ctx.defaultCalendar
        val attendees = event.attendees.map {
            if (it.email.equals(userEmail, ignoreCase = true)) it.copy(partStat = partStat) else it
        }
        val data = event.toEventData().copy(attendees = attendees.map { it.toData() })
        CalDavClient.updateEvent(ctx.client, calendar, event.href, event.etag, data)
    }

    fun openDavContext(session: AuthSession): CalDavContext {
        val client = NetworkFactory.newAuthedClient(
            session.username,
            session.appPassword,
            session.trustAllCerts,
        )
        val userId = session.davUserId ?: OcsUserResolver.resolveDavUserId(session)
        val base = CalDavClient.calendarsBase(session.serverUrl, userId)
        val calendars = CalDavClient.listCalendars(client, base)
        if (calendars.isEmpty()) throw IllegalStateException("Календари не найдены")
        val default = calendars.firstOrNull { cal ->
            val name = cal.displayName.lowercase()
            name.contains("личн") || name.contains("personal") || name == "calendar"
        } ?: calendars.first()
        return CalDavContext(client, calendars, default)
    }

    private fun calendarsRoot(session: AuthSession): String =
        CalDavClient.calendarsBase(session.serverUrl, session.davUserId ?: OcsUserResolver.resolveDavUserId(session))

    private fun fetchEvents(
        ctx: CalDavContext,
        rangeStart: Instant,
        rangeEnd: Instant,
        visibleHrefs: Set<String>,
        accountKey: String,
    ): List<CalendarEventItem> {
        val calendars = if (visibleHrefs.isEmpty()) ctx.calendars else ctx.calendars.filter { it.href in visibleHrefs }
        val rangeKey = "${rangeStart.epochSecond}-${rangeEnd.epochSecond}"
        val events = mutableListOf<DavEvent>()
        var successCount = 0
        var lastError: String? = null
        for (cal in calendars.take(12)) {
            runCatching {
                // CTag календаря не менялся → сырой REPORT-ответ берём с диска, сеть не трогаем
                val cachedXml = eventsCache?.get(accountKey, cal.href, rangeKey, cal.ctag)
                if (cachedXml != null) {
                    CalDavClient.parseEventsXml(cachedXml, cal)
                } else {
                    val xml = CalDavClient.queryEventsRawXml(ctx.client, cal, rangeStart, rangeEnd)
                    eventsCache?.put(accountKey, cal.href, rangeKey, cal.ctag, xml)
                    CalDavClient.parseEventsXml(xml, cal)
                }
            }
                .onSuccess { successCount++; events += it }
                .onFailure { lastError = it.message }
        }
        if (successCount == 0 && lastError != null) {
            throw IllegalStateException("Не удалось загрузить события календаря: $lastError")
        }
        return events.distinctBy { it.uid }.sortedBy { it.startEpochMilli }.map { it.toItem() }
    }

    private fun accountKey(session: AuthSession): String =
        "${session.serverUrl}|${session.username}"

    private fun DavEvent.toItem() = CalendarEventItem(
        uid = uid,
        href = href,
        etag = etag,
        summary = summary,
        startLabel = startLabel,
        startEpochMilli = startEpochMilli,
        endEpochMilli = endEpochMilli,
        calendarName = calendarName,
        calendarHref = calendarHref,
        allDay = allDay,
        description = description,
        location = location,
        rrule = rrule,
        categories = categories,
        status = status,
        classification = classification,
        attendees = attendees.map { it.toItem() },
        organizerEmail = organizerEmail,
        talkRoomUrl = conferenceUri.ifBlank { if (location.contains("/call/")) location else "" },
        alarms = alarms.map { CalendarReminderItem(it.minutesBefore, it.action) },
        source = CalendarEventSource.F7CLOUD,
    )

    private fun DavTrashEvent.toTrashItem() = CalendarTrashItem(
        uid = uid,
        href = href,
        etag = etag,
        summary = summary,
        deletedAt = deletedAt,
        calendarUri = calendarUri,
    )

    private fun DavTask.toTaskItem() = CalendarTaskItem(
        uid = uid,
        summary = summary,
        due = due,
        calendarName = calendarName,
    )

    private fun slugify(name: String): String =
        name.lowercase().replace(Regex("[^a-z0-9]+"), "-").trim('-').ifBlank { "calendar" }

    data class CalDavContext(
        val client: OkHttpClient,
        val calendars: List<DavCalendar>,
        val defaultCalendar: DavCalendar,
    )

    companion object {
        fun monthGridStart(month: YearMonth): LocalDate {
            var day = month.atDay(1)
            while (day.dayOfWeek.value != 1) day = day.minusDays(1)
            return day
        }

        fun monthGridEnd(month: YearMonth): LocalDate {
            var day = month.atEndOfMonth()
            while (day.dayOfWeek.value != 7) day = day.plusDays(1)
            return day
        }

        fun monthGridDays(month: YearMonth): List<LocalDate> {
            val start = monthGridStart(month)
            return (0 until 42).map { start.plusDays(it.toLong()) }
        }

        fun yearMonths(year: Int): List<YearMonth> = (1..12).map { YearMonth.of(year, it) }

        fun weekRange(day: LocalDate): Pair<LocalDate, LocalDate> {
            var start = day
            while (start.dayOfWeek.value != 1) start = start.minusDays(1)
            return start to start.plusDays(6)
        }

        fun listRange(day: LocalDate): Pair<LocalDate, LocalDate> = day.minusDays(7) to day.plusDays(60)

        fun eventOccursOnDay(event: CalendarEventItem, day: LocalDate, zone: ZoneId = ZoneId.systemDefault()): Boolean {
            val start = Instant.ofEpochMilli(event.startEpochMilli).atZone(zone).toLocalDate()
            val end = Instant.ofEpochMilli(event.endEpochMilli).atZone(zone).toLocalDate()
            return if (event.allDay) !day.isBefore(start) && day.isBefore(end) else day == start
        }

        fun formatEventTime(event: CalendarEventItem, zone: ZoneId = ZoneId.systemDefault()): String {
            if (event.allDay) return "Весь день"
            val start = Instant.ofEpochMilli(event.startEpochMilli).atZone(zone)
            val end = Instant.ofEpochMilli(event.endEpochMilli).atZone(zone)
            val fmt = java.time.format.DateTimeFormatter.ofPattern("HH:mm")
            return "${start.format(fmt)} – ${end.format(fmt)}"
        }
    }
}

enum class CalendarEventSource { F7CLOUD, GOOGLE }

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

data class CalendarEventItem(
    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 = false,
    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<CalendarAttendeeItem> = emptyList(),
    val organizerEmail: String = "",
    val talkRoomUrl: String = "",
    val alarms: List<CalendarReminderItem> = emptyList(),
    val source: CalendarEventSource = CalendarEventSource.F7CLOUD,
)

private fun CalendarEventDraft.toEventData(uid: String? = null, talkUrl: String = ""): CalendarEventData {
    val zone = ZoneId.systemDefault()
    val start = if (allDay) {
        date.atStartOfDay(zone).toInstant().toEpochMilli()
    } else {
        parseDateTime(date, startTime)?.atZone(zone)?.toInstant()?.toEpochMilli()
            ?: date.atTime(10, 0).atZone(zone).toInstant().toEpochMilli()
    }
    val end = if (allDay) {
        date.plusDays(1).atStartOfDay(zone).toInstant().toEpochMilli()
    } else {
        parseDateTime(date, endTime)?.atZone(zone)?.toInstant()?.toEpochMilli()
            ?: date.atTime(11, 0).atZone(zone).toInstant().toEpochMilli()
    }
    val rrule = when {
        recurrence != RecurrencePreset.NONE -> recurrence.rrule
        customRrule.isNotBlank() -> customRrule
        else -> ""
    }
    val cats = categories.split(',').map { it.trim() }.filter { it.isNotBlank() }
    val conf = talkUrl.ifBlank { this.talkRoomUrl }
    return CalendarEventData(
        uid = uid ?: CalendarIcs.newUid(),
        summary = title.trim(),
        description = description.trim(),
        location = location.trim().ifBlank { conf },
        startEpochMilli = start,
        endEpochMilli = end,
        allDay = allDay,
        rrule = rrule,
        categories = cats,
        status = status.icsValue,
        classification = classification.icsValue,
        attendees = attendees.map {
            CalendarAttendeeData(it.email, it.displayName, it.partStat)
        },
        alarms = if (reminderMinutes > 0) listOf(CalendarAlarmData(reminderMinutes)) else emptyList(),
        conferenceUri = conf,
    )
}

private fun CalendarEventItem.toEventData(): CalendarEventData = CalendarEventData(
    uid = uid,
    summary = summary,
    description = description,
    location = location,
    startEpochMilli = startEpochMilli,
    endEpochMilli = endEpochMilli,
    allDay = allDay,
    rrule = rrule,
    categories = categories,
    status = status,
    classification = classification,
    attendees = attendees.map { it.toData() },
    organizerEmail = organizerEmail,
    alarms = alarms.map { CalendarAlarmData(it.minutesBefore, it.action) },
    conferenceUri = talkRoomUrl,
)

private fun CalendarEventData.toItem(
    calendarName: String,
    calendarHref: String,
    href: String,
    etag: String,
) = CalendarEventItem(
    uid = uid,
    href = href,
    etag = etag,
    summary = summary,
    startLabel = "",
    startEpochMilli = startEpochMilli,
    endEpochMilli = endEpochMilli,
    calendarName = calendarName,
    calendarHref = calendarHref,
    allDay = allDay,
    description = description,
    location = location,
    rrule = rrule,
    categories = categories,
    status = status,
    classification = classification,
    attendees = attendees.map { it.toItem() },
    talkRoomUrl = conferenceUri,
    alarms = alarms.map { CalendarReminderItem(it.minutesBefore, it.action) },
)

private fun CalendarAttendeeData.toItem() = CalendarAttendeeItem(email, displayName, partStat)
private fun CalendarAttendeeItem.toData() = CalendarAttendeeData(email, displayName, partStat)

private fun parseDateTime(day: LocalDate, timeText: String): LocalDateTime? {
    val parts = timeText.trim().split(':')
    if (parts.size != 2) return null
    val hour = parts[0].toIntOrNull() ?: return null
    val minute = parts[1].toIntOrNull() ?: return null
    if (hour !in 0..23 || minute !in 0..59) return null
    return day.atTime(hour, minute)
}
