import AppKit
import EventKit
import Foundation
import LilithLogging
import MacSyncShared

private let log = AppLogger.logger(for: "ICal.Sync")

// MARK: - Stats

public struct ICalSyncStats: Equatable, Sendable {
    public var calendarCount: Int = 0
    public var eventCount: Int = 0

    public init() {}
}

// MARK: - Sync Error

public enum ICalSyncError: Equatable, Sendable {
    case none
    case calendarAccessRequired
    case backendUnreachable
    case connectionFailed(String)

    public var message: String {
        switch self {
        case .none: return ""
        case .calendarAccessRequired: return "Calendar access required"
        case .backendUnreachable: return "Cannot connect to backend server"
        case .connectionFailed(let m): return m
        }
    }

    public var isConnectionError: Bool {
        switch self {
        case .backendUnreachable, .connectionFailed: return true
        default: return false
        }
    }
}

// MARK: - SyncManager

@MainActor
public final class SyncManager: BaseSyncManager<ICalSyncStats, ICalSyncError> {
    public static let shared = SyncManager()

    public let reader = CalendarReader.shared
    private let apiClient = APIClient.shared

    private let eventBatchSize = 200

    // Outbound send queue (server → Mac calendar writes).
    private lazy var sendQueueClient: SendQueueClient<CalendarSendTransport> = {
        let transport = CalendarSendTransport(apiClient: apiClient)
        let sender = CalendarSender(eventStore: reader.eventStore)
        return SendQueueClient(
            label: "ical",
            transport: transport,
            interval: 60
        ) { item in
            await sender.apply(item)
        }
    }()

    public override func didStartSync() {
        sendQueueClient.start()
    }

    public override func willStopSync() {
        sendQueueClient.stop()
    }

    private init() {
        super.init(
            initialStats: ICalSyncStats(),
            noError: .none,
            persistenceKey: "ical",
            timerInterval: 300
        )

        // Migrate legacy UserDefaults watermark keys (one-time).
        if lastSync == nil, let legacy = UserDefaults.standard.object(forKey: "icalLastSync") as? Date {
            setLastSync(legacy)
            UserDefaults.standard.removeObject(forKey: "icalLastSync")
        }
        if lastSyncCompletedAt == nil, let legacy = UserDefaults.standard.object(forKey: "icalLastSyncCompletedAt") as? Date {
            lastSyncCompletedAt = legacy
            UserDefaults.standard.set(legacy, forKey: "ical.lastSyncCompletedAt")
            UserDefaults.standard.removeObject(forKey: "icalLastSyncCompletedAt")
        }
    }

    // MARK: - Authorization hooks

    public override func isAuthorized() async -> Bool { reader.isAuthorized }

    public override func requestAuthorization() async -> Bool {
        await reader.requestAuthorization()
    }

    public override func onAuthorizationDenied() {
        log.warning("Calendar access denied")
        syncError = .calendarAccessRequired
    }

    public func openCalendarAccessSettings() {
        if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars") {
            NSWorkspace.shared.open(url)
        }
    }

    // MARK: - Sync cycle

    public override func performSync() async {
        log.info("performSync starting")

        // Phase 1: Calendars
        currentOperation = "Syncing calendars…"
        let calendars = reader.fetchCalendars()
        log.info("Fetched \(calendars.count) calendars")
        let df = ISO8601DateFormatter()

        if !calendars.isEmpty {
            let calPayloads = calendars.map { cal in
                SyncCalendarPayload(
                    calendarIdentifier: cal.calendarIdentifier,
                    title: cal.title,
                    calendarType: calendarTypeString(cal.type),
                    color: cal.color,
                    source: cal.source
                )
            }
            do {
                let synced = try await apiClient.syncCalendars(calPayloads)
                log.info("Synced \(synced) calendars")
                if syncError.isConnectionError { syncError = .none }
            } catch {
                log.warning("Calendar sync failed: \(error.localizedDescription)")
                setConnectionError(error)
            }
        }

        // Phase 2: Events
        currentOperation = "Fetching events…"
        let events = reader.fetchEvents(since: lastSync)
        log.info("Fetched \(events.count) events")

        if !events.isEmpty {
            let batches = stride(from: 0, to: events.count, by: eventBatchSize).map {
                Array(events[$0..<min($0 + eventBatchSize, events.count)])
            }

            for (idx, batch) in batches.enumerated() {
                currentOperation = "Syncing events… (\(idx + 1)/\(batches.count))"
                let payloads = batch.map { event in
                    SyncEventPayload(
                        eventIdentifier: event.eventIdentifier,
                        calendarIdentifier: event.calendarIdentifier,
                        title: event.title,
                        notes: event.notes,
                        location: event.location,
                        url: event.url,
                        isAllDay: event.isAllDay,
                        startDate: df.string(from: event.startDate),
                        endDate: df.string(from: event.endDate),
                        createdAt: event.creationDate.map { df.string(from: $0) },
                        modifiedAt: event.lastModifiedDate.map { df.string(from: $0) },
                        status: eventStatusString(event.status),
                        availability: eventAvailabilityString(event.availability),
                        isDetached: event.isDetached,
                        hasAlarms: event.hasAlarms,
                        hasRecurrenceRules: event.hasRecurrenceRules,
                        recurrenceRule: event.recurrenceRule,
                        organizerEmail: event.organizerEmail,
                        organizerName: event.organizerName,
                        attendeeCount: event.attendeeCount
                    )
                }
                do {
                    let synced = try await apiClient.syncEvents(payloads)
                    log.info("Batch \(idx + 1)/\(batches.count) synced=\(synced)")
                    if syncError.isConnectionError { syncError = .none }
                } catch {
                    log.warning("Event batch \(idx + 1) failed: \(error.localizedDescription)")
                    setConnectionError(error)
                }
            }
        }

        await updateStats()
        currentOperation = "Sync complete"
        log.info("performSync complete calendars=\(calendars.count) events=\(events.count)")
    }

    private func updateStats() async {
        do {
            let response = try await apiClient.getStats()
            stats.calendarCount = response.totalCalendars
            stats.eventCount = response.totalEvents
            log.info("Stats: calendars=\(response.totalCalendars) events=\(response.totalEvents)")
        } catch {
            log.warning("updateStats failed: \(error.localizedDescription)")
        }
    }

    private func setConnectionError(_ error: Error) {
        if SyncConnectionErrorHeuristic.isConnectionError(error) {
            syncError = .backendUnreachable
        }
    }

    // MARK: - String helpers

    private func calendarTypeString(_ type: EKCalendarType) -> String {
        switch type {
        case .local: return "local"
        case .calDAV: return "caldav"
        case .exchange: return "exchange"
        case .subscription: return "subscription"
        case .birthday: return "birthday"
        @unknown default: return "unknown"
        }
    }

    private func eventStatusString(_ status: EKEventStatus) -> String {
        switch status {
        case .none: return "none"
        case .confirmed: return "confirmed"
        case .tentative: return "tentative"
        case .canceled: return "canceled"
        @unknown default: return "none"
        }
    }

    private func eventAvailabilityString(_ av: EKEventAvailability) -> String {
        switch av {
        case .notSupported: return "none"
        case .busy: return "busy"
        case .free: return "free"
        case .tentative: return "tentative"
        case .unavailable: return "unavailable"
        @unknown default: return "none"
        }
    }
}
