package ru.forbion.f7cloud.feature.calendar

import ru.forbion.f7cloud.core.auth.AuthSession
import ru.forbion.f7cloud.core.network.CalDavClient
import ru.forbion.f7cloud.core.network.DavEvent
import java.time.Instant

data class CalendarSyncResult(
    val pushedToDevice: Int = 0,
    val pushedToServer: Int = 0,
    val updated: Int = 0,
    val deleted: Int = 0,
    val linked: Int = 0,
)

class CalendarSyncEngine(
    private val repository: CalendarRepository,
    private val deviceClient: DeviceCalendarClient,
    private val store: CalendarSyncStore,
) {
    fun run(
        session: AuthSession,
        deviceCalendarId: Long,
    ): CalendarSyncResult {
        val accountKey = store.accountKey(session.serverUrl, session.username)
        val ctx = repository.openDavContext(session)
        val rangeStart = Instant.now().minusSeconds(RANGE_PAST_SEC)
        val rangeEnd = Instant.now().plusSeconds(RANGE_FUTURE_SEC)
        val ncEvents = CalDavClient.queryEventsInRange(
            ctx.client,
            ctx.defaultCalendar,
            rangeStart,
            rangeEnd,
        )
        val deviceEvents = deviceClient.queryEvents(
            deviceCalendarId,
            rangeStart.toEpochMilli(),
            rangeEnd.toEpochMilli(),
        )
        var mappings = store.loadMappings(accountKey)
            .filter { it.deviceCalendarId == deviceCalendarId }
            .toMutableList()
        var pushedToDevice = 0
        var pushedToServer = 0
        var updated = 0
        var deleted = 0
        var linked = 0

        val ncByUid = ncEvents.associateBy { it.uid }
        val devById = deviceEvents.associateBy { it.eventId }

        // Update linked pairs (last-write-wins)
        val mappingIterator = mappings.iterator()
        while (mappingIterator.hasNext()) {
            val map = mappingIterator.next()
            val nc = ncByUid[map.ncUid]
            val dev = devById[map.deviceEventId]
            when {
                nc == null && dev == null -> {
                    mappingIterator.remove()
                    deleted++
                }
                nc == null && dev != null -> {
                    runCatching { deviceClient.deleteEvent(dev.eventId) }
                    mappingIterator.remove()
                    deleted++
                }
                nc != null && dev == null -> {
                    runCatching {
                        CalDavClient.deleteEvent(ctx.client, map.ncHref, map.ncEtag.ifBlank { "*" })
                    }
                    mappingIterator.remove()
                    deleted++
                }
                nc != null && dev != null -> {
                    when {
                        nc.lastModifiedEpochMilli > dev.lastModifiedEpochMilli + SYNC_SKEW_MS -> {
                            deviceClient.updateEvent(
                                eventId = dev.eventId,
                                title = nc.summary,
                                description = dev.description,
                                startEpochMilli = nc.startEpochMilli,
                                endEpochMilli = nc.endEpochMilli,
                                allDay = nc.allDay,
                                ncUid = nc.uid,
                            )
                            updated++
                        }
                        dev.lastModifiedEpochMilli > nc.lastModifiedEpochMilli + SYNC_SKEW_MS -> {
                            CalDavClient.updateEvent(
                                client = ctx.client,
                                calendar = ctx.defaultCalendar,
                                href = nc.href,
                                etag = nc.etag.ifBlank { map.ncEtag }.ifBlank { "*" },
                                uid = nc.uid,
                                summary = dev.title.ifBlank { nc.summary },
                                startEpochMilli = dev.startEpochMilli,
                                endEpochMilli = dev.endEpochMilli,
                                allDay = dev.allDay,
                            )
                            updated++
                        }
                    }
                    val refreshed = mappings.indexOfFirst { it.ncUid == map.ncUid }
                    if (refreshed >= 0) {
                        mappings[refreshed] = map.copy(
                            ncHref = nc.href,
                            ncEtag = nc.etag,
                        )
                    }
                }
            }
        }

        val mappedNcUids = mappings.map { it.ncUid }.toSet()
        val mappedDevIds = mappings.map { it.deviceEventId }.toSet()

        // Fuzzy link: same title + close start
        for (nc in ncEvents) {
            if (nc.uid in mappedNcUids) continue
            val match = deviceEvents.firstOrNull { dev ->
                dev.eventId !in mappedDevIds &&
                    titlesMatch(nc.summary, dev.title) &&
                    kotlin.math.abs(nc.startEpochMilli - dev.startEpochMilli) < FUZZY_START_MS
            } ?: continue
            mappings += CalendarSyncMapping(
                ncUid = nc.uid,
                ncHref = nc.href,
                ncEtag = nc.etag,
                deviceEventId = match.eventId,
                deviceCalendarId = deviceCalendarId,
            )
            deviceClient.stampNcUid(match.eventId, nc.uid)
            linked++
        }

        val mappedNcUids2 = mappings.map { it.ncUid }.toSet()
        val mappedDevIds2 = mappings.map { it.deviceEventId }.toSet()

        // NC → device
        for (nc in ncEvents) {
            if (nc.uid in mappedNcUids2) continue
            val eventId = deviceClient.insertEvent(
                calendarId = deviceCalendarId,
                title = nc.summary,
                description = "",
                startEpochMilli = nc.startEpochMilli,
                endEpochMilli = nc.endEpochMilli,
                allDay = nc.allDay,
                ncUid = nc.uid,
            )
            mappings += CalendarSyncMapping(
                ncUid = nc.uid,
                ncHref = nc.href,
                ncEtag = nc.etag,
                deviceEventId = eventId,
                deviceCalendarId = deviceCalendarId,
            )
            pushedToDevice++
        }

        val mappedDevIds3 = mappings.map { it.deviceEventId }.toSet()

        // device → NC
        for (dev in deviceEvents) {
            if (dev.eventId in mappedDevIds3) continue
            val uid = dev.ncUid ?: "${java.util.UUID.randomUUID()}@f7cloud.mobile"
            val start = java.time.LocalDateTime.ofInstant(
                Instant.ofEpochMilli(dev.startEpochMilli),
                java.time.ZoneId.systemDefault(),
            )
            val durationMin = ((dev.endEpochMilli - dev.startEpochMilli) / 60_000).coerceAtLeast(15)
            CalDavClient.createEvent(
                ctx.client,
                ctx.defaultCalendar,
                dev.title.ifBlank { "(без названия)" },
                start,
                durationMin,
            )
            val href = ctx.defaultCalendar.href.trimEnd('/') + "/$uid.ics"
            mappings += CalendarSyncMapping(
                ncUid = uid,
                ncHref = href,
                ncEtag = "",
                deviceEventId = dev.eventId,
                deviceCalendarId = deviceCalendarId,
            )
            deviceClient.stampNcUid(dev.eventId, uid)
            pushedToServer++
        }

        store.saveMappings(accountKey, mappings)
        store.setLastSyncAt(accountKey, System.currentTimeMillis())
        return CalendarSyncResult(
            pushedToDevice = pushedToDevice,
            pushedToServer = pushedToServer,
            updated = updated,
            deleted = deleted,
            linked = linked,
        )
    }

    private fun titlesMatch(a: String, b: String): Boolean =
        a.trim().equals(b.trim(), ignoreCase = true)

    companion object {
        private const val RANGE_PAST_SEC = 60L * 60 * 24 * 60   // 60 days
        private const val RANGE_FUTURE_SEC = 60L * 60 * 24 * 365 // 1 year
        private const val FUZZY_START_MS = 120_000L
        private const val SYNC_SKEW_MS = 2_000L
    }
}
