import AppKit
import EventKit
import Foundation

/// Reads meetings from the local Calendar database via EventKit.
///
/// This deliberately avoids Google OAuth: your Google calendars are already synced
/// into macOS Calendar, so EventKit sees them with one permission prompt and no
/// tokens to store. If a calendar is missing here, add the account in
/// System Settings → Internet Accounts and it'll show up.
final class CalendarWatcher {
    /// One calendar, as shown in the Settings checklist.
    struct CalendarInfo: Identifiable, Hashable {
        let id: String
        let title: String
        /// Account it belongs to — "Samantha (Google)", a client account, "iCloud"…
        let accountName: String
        let colorHex: String
    }

    private let store = EKEventStore()
    private(set) var accessGranted = false

    init() {
        // If permission was granted in a previous run, don't wait for the prompt.
        let status = EKEventStore.authorizationStatus(for: .event)
        accessGranted = status == .fullAccess
        NSLog("[AlienHUD] calendar authorization status: \(Self.describe(status))")
    }

    /// Readable form of the TCC status, so "why is it not firing" is answerable
    /// from the log rather than by guesswork.
    private static func describe(_ status: EKAuthorizationStatus) -> String {
        switch status {
        case .notDetermined: return "notDetermined (will prompt)"
        case .restricted: return "restricted"
        case .denied: return "denied — re-enable in System Settings › Privacy & Security › Calendars"
        case .fullAccess: return "fullAccess"
        case .writeOnly: return "writeOnly (not enough — needs full access)"
        @unknown default: return "unknown"
        }
    }

    /// Prompts once (macOS remembers the answer).
    func requestAccess(_ completion: @escaping (Bool) -> Void) {
        store.requestFullAccessToEvents { [weak self] granted, error in
            if let error { NSLog("[AlienHUD] calendar access error: \(error)") }
            DispatchQueue.main.async {
                self?.accessGranted = granted
                completion(granted)
            }
        }
    }

    /// Every calendar on the machine, for the Settings checklist.
    func allCalendars() -> [CalendarInfo] {
        guard accessGranted else { return [] }
        return store.calendars(for: .event)
            .map { cal in
                CalendarInfo(id: cal.calendarIdentifier,
                             title: cal.title,
                             accountName: cal.source?.title ?? "Other",
                             colorHex: Self.hex(from: cal))
            }
            .sorted { ($0.accountName, $0.title) < ($1.accountName, $1.title) }
    }

    /// Meetings starting within the next `leadMinutes`, limited to `calendarIds`.
    /// Skips all-day events and any invite you've declined.
    func upcoming(leadMinutes: Int, calendarIds: [String]) -> [Reminder] {
        guard accessGranted else { return [] }

        let wanted = Set(calendarIds)
        let calendars = store.calendars(for: .event)
            .filter { wanted.contains($0.calendarIdentifier) }
        // Nothing ticked means nothing to announce — not "announce everything".
        guard !calendars.isEmpty else { return [] }

        let now = Date()
        // Look a little wider than the lead time, then filter precisely.
        let horizon = now.addingTimeInterval(Double(leadMinutes + 5) * 60)
        let predicate = store.predicateForEvents(withStart: now.addingTimeInterval(-60),
                                                 end: horizon,
                                                 calendars: calendars)

        return store.events(matching: predicate).compactMap { event -> Reminder? in
            guard !event.isAllDay, let start = event.startDate else { return nil }
            guard event.status != .canceled else { return nil }
            if declined(event) { return nil }

            let secondsAway = start.timeIntervalSince(now)
            // Only fire inside the lead window, and not for things already underway.
            guard secondsAway <= Double(leadMinutes) * 60, secondsAway > -60 else { return nil }

            let title = (event.title ?? "Untitled meeting")
                .trimmingCharacters(in: .whitespacesAndNewlines)
            let id = event.eventIdentifier ?? "\(title)-\(start.timeIntervalSince1970)"
            return Reminder(id: id, kind: .meeting, title: title, due: start)
        }
    }

    private func declined(_ event: EKEvent) -> Bool {
        guard let attendees = event.attendees else { return false }
        return attendees.contains { $0.isCurrentUser && $0.participantStatus == .declined }
    }

    private static func hex(from calendar: EKCalendar) -> String {
        guard let color = calendar.color,
              let rgb = color.usingColorSpace(.sRGB) else { return "#8439f6" }
        let r = Int((rgb.redComponent * 255).rounded())
        let g = Int((rgb.greenComponent * 255).rounded())
        let b = Int((rgb.blueComponent * 255).rounded())
        return String(format: "#%02X%02X%02X", r, g, b)
    }
}
