//
//  EventKitService.swift
//  Confab
//
//  Calendar and Reminders integration via EventKit
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of Confab.
//
// Confab is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling Confab or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://confab.no>.


import EventKit
import Foundation
import os

/// Shared tri-state authorization status for the Settings UI across all Personal Data services.
/// `.denied` also covers `.restricted` and (for Calendar) `.writeOnly` — states where the OS
/// will not show a request dialog again; the user must go to System Settings manually.
enum PersonalDataAccessState {
    case notDetermined
    case denied
    case granted
}

@Observable
class EventKitService {
    static let shared = EventKitService()

    private let store = EKEventStore()

    private init() {}

    // MARK: - Authorization

    var calendarAuthStatus: EKAuthorizationStatus {
        let status = EKEventStore.authorizationStatus(for: .event)
        Log.mcp.debug("EventKitService.calendarAuthStatus -> \(Self.describe(status)) (raw=\(status.rawValue))")
        return status
    }

    var reminderAuthStatus: EKAuthorizationStatus {
        let status = EKEventStore.authorizationStatus(for: .reminder)
        Log.mcp.debug("EventKitService.reminderAuthStatus -> \(Self.describe(status)) (raw=\(status.rawValue))")
        return status
    }

    var calendarAuthorized: Bool {
        calendarAuthStatus == .fullAccess
    }

    var reminderAuthorized: Bool {
        reminderAuthStatus == .fullAccess
    }

    var calendarAccessState: PersonalDataAccessState {
        switch calendarAuthStatus {
        case .fullAccess: return .granted
        case .notDetermined: return .notDetermined
        default: return .denied  // .denied, .restricted, .writeOnly (no read access for our tools)
        }
    }

    var reminderAccessState: PersonalDataAccessState {
        switch reminderAuthStatus {
        case .fullAccess: return .granted
        case .notDetermined: return .notDetermined
        default: return .denied
        }
    }

    @discardableResult
    func requestCalendarAccess() async -> Bool {
        let before = EKEventStore.authorizationStatus(for: .event)
        Log.mcp.info("requestCalendarAccess: status before request = \(Self.describe(before)) (raw=\(before.rawValue))")
        do {
            let granted = try await store.requestFullAccessToEvents()
            let after = EKEventStore.authorizationStatus(for: .event)
            Log.mcp.info("requestCalendarAccess: API returned granted=\(granted); status after request = \(Self.describe(after)) (raw=\(after.rawValue))")
            return granted
        } catch {
            let after = EKEventStore.authorizationStatus(for: .event)
            Log.mcp.error("requestCalendarAccess: threw error: \(error.localizedDescription); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
            return false
        }
    }

    @discardableResult
    func requestReminderAccess() async -> Bool {
        let before = EKEventStore.authorizationStatus(for: .reminder)
        Log.mcp.info("requestReminderAccess: status before request = \(Self.describe(before)) (raw=\(before.rawValue))")
        do {
            let granted = try await store.requestFullAccessToReminders()
            let after = EKEventStore.authorizationStatus(for: .reminder)
            Log.mcp.info("requestReminderAccess: API returned granted=\(granted); status after request = \(Self.describe(after)) (raw=\(after.rawValue))")
            return granted
        } catch {
            let after = EKEventStore.authorizationStatus(for: .reminder)
            Log.mcp.error("requestReminderAccess: threw error: \(error.localizedDescription); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
            return false
        }
    }

    nonisolated static func describe(_ status: EKAuthorizationStatus) -> String {
        switch status {
        case .notDetermined: return "notDetermined"
        case .restricted: return "restricted"
        case .denied: return "denied"
        case .fullAccess: return "fullAccess"
        case .writeOnly: return "writeOnly"
        @unknown default: return "unknown"
        }
    }

    // MARK: - Tool Schemas

    func getToolSchemas(calendarEnabled: Bool, remindersEnabled: Bool) -> [Tool] {
        var tools: [Tool] = []

        if calendarEnabled {
            tools.append(makeTool(
                name: "calendar_list_calendars",
                description: "List all calendars available on this Mac (e.g. iCloud, Work, Home).",
                properties: [:],
                required: []
            ))
            tools.append(makeTool(
                name: "calendar_list_events",
                description: "List calendar events within a date range. Dates are ISO8601 (e.g. '2026-06-20T00:00:00' or '2026-06-20'). Range is limited to 1 year. For open-ended queries like 'next appointment' or 'upcoming events', do NOT limit the range to just today — use a generous forward-looking window (e.g. today through +90 days) so you don't miss events further out.",
                properties: [
                    "start_date": prop("string", "Start of the date range (ISO8601)"),
                    "end_date": prop("string", "End of the date range (ISO8601)"),
                    "calendar_name": prop("string", "Optional: only list events from this calendar")
                ],
                required: ["start_date", "end_date"]
            ))
            tools.append(makeTool(
                name: "calendar_create_event",
                description: "Create a new calendar event. Requires user approval before it is actually created.",
                properties: [
                    "title": prop("string", "Event title"),
                    "start_date": prop("string", "Start date/time (ISO8601, e.g. '2026-06-20T14:00:00')"),
                    "end_date": prop("string", "End date/time (ISO8601)"),
                    "calendar_name": prop("string", "Optional: calendar to add the event to (defaults to the system default calendar)"),
                    "location": prop("string", "Optional: event location text"),
                    "notes": prop("string", "Optional: event notes"),
                    "all_day": prop("boolean", "Optional: whether this is an all-day event (default: false)"),
                    "alarm_minutes_before": prop("number", "Optional: minutes before the start time to show an alert")
                ],
                required: ["title", "start_date", "end_date"]
            ))
        }

        if remindersEnabled {
            tools.append(makeTool(
                name: "reminders_list_lists",
                description: "List all reminder lists available on this Mac.",
                properties: [:],
                required: []
            ))
            tools.append(makeTool(
                name: "reminders_list",
                description: "List reminders. Omit list_name to search across ALL reminder lists in a single call — prefer this over calling once per list. Incomplete reminders only unless include_completed is true.",
                properties: [
                    "list_name": prop("string", "Optional: only list reminders from this one list (omit to search all lists at once)"),
                    "include_completed": prop("boolean", "Optional: include completed reminders (default: false)")
                ],
                required: []
            ))
            tools.append(makeTool(
                name: "reminders_create",
                description: "Create a new reminder. Requires user approval before it is actually created.",
                properties: [
                    "title": prop("string", "Reminder title"),
                    "list_name": prop("string", "Optional: reminder list to add to (defaults to the system default list)"),
                    "due_date": prop("string", "Optional: due date/time (ISO8601)"),
                    "priority": prop("string", "Optional: priority level", enumValues: ["none", "low", "medium", "high"]),
                    "notes": prop("string", "Optional: reminder notes")
                ],
                required: ["title"]
            ))
            tools.append(makeTool(
                name: "reminders_complete",
                description: "Mark a reminder as completed. Requires user approval. Use reminders_list to find the reminder_id first.",
                properties: [
                    "reminder_id": prop("string", "The reminder's identifier, from reminders_list")
                ],
                required: ["reminder_id"]
            ))
        }

        return tools
    }

    // MARK: - Read Tool Execution

    func executeTool(name: String, arguments: String) async -> [String: Any] {
        Log.mcp.info("Executing EventKit tool: \(name)")
        let args = Self.parseArgs(arguments)

        switch name {
        case "calendar_list_calendars":
            guard calendarAuthorized else { return Self.permissionError(domain: "Calendar") }
            return listCalendars()

        case "calendar_list_events":
            guard calendarAuthorized else { return Self.permissionError(domain: "Calendar") }
            guard let startStr = args["start_date"] as? String, let start = Self.parseDate(startStr) else {
                return ["error": "Missing or invalid parameter: start_date"]
            }
            guard let endStr = args["end_date"] as? String, let end = Self.parseDate(endStr) else {
                return ["error": "Missing or invalid parameter: end_date"]
            }
            let calendarName = args["calendar_name"] as? String
            return listEvents(start: start, end: end, calendarName: calendarName)

        case "reminders_list_lists":
            guard reminderAuthorized else { return Self.permissionError(domain: "Reminders") }
            return listReminderLists()

        case "reminders_list":
            guard reminderAuthorized else { return Self.permissionError(domain: "Reminders") }
            let listName = args["list_name"] as? String
            let includeCompleted = args["include_completed"] as? Bool ?? false
            return await listReminders(listName: listName, includeCompleted: includeCompleted)

        default:
            return ["error": "Unknown EventKit tool: \(name)"]
        }
    }

    // MARK: - Write Tool Execution (called only after approval)

    func executeWriteTool(name: String, arguments: String) async -> [String: Any] {
        Log.mcp.info("Executing EventKit write tool: \(name)")
        let args = Self.parseArgs(arguments)

        switch name {
        case "calendar_create_event":
            guard calendarAuthorized else { return Self.permissionError(domain: "Calendar") }
            return createEvent(args: args)

        case "reminders_create":
            guard reminderAuthorized else { return Self.permissionError(domain: "Reminders") }
            return createReminder(args: args)

        case "reminders_complete":
            guard reminderAuthorized else { return Self.permissionError(domain: "Reminders") }
            guard let reminderId = args["reminder_id"] as? String else {
                return ["error": "Missing required parameter: reminder_id"]
            }
            return completeReminder(reminderId: reminderId)

        default:
            return ["error": "Unknown EventKit write tool: \(name)"]
        }
    }

    // MARK: - Approval Summary

    /// Human-readable description shown in the approval sheet before a write tool runs.
    func approvalSummary(forTool name: String, arguments: String) -> String {
        let args = Self.parseArgs(arguments)
        switch name {
        case "calendar_create_event":
            let title = args["title"] as? String ?? "Untitled event"
            let start = (args["start_date"] as? String).flatMap(Self.parseDate) ?? Date()
            let end = (args["end_date"] as? String).flatMap(Self.parseDate) ?? start
            return "Create calendar event \"\(title)\" from \(Self.displayFormatter.string(from: start)) to \(Self.displayFormatter.string(from: end))"
        case "reminders_create":
            let title = args["title"] as? String ?? "Untitled reminder"
            if let dueStr = args["due_date"] as? String, let due = Self.parseDate(dueStr) {
                return "Create reminder \"\(title)\" due \(Self.displayFormatter.string(from: due))"
            }
            return "Create reminder \"\(title)\""
        case "reminders_complete":
            return "Mark reminder as completed"
        default:
            return "Perform action: \(name)"
        }
    }

    // MARK: - Calendar Read Implementations

    private func listCalendars() -> [String: Any] {
        let calendars = store.calendars(for: .event).map { cal -> [String: Any] in
            [
                "name": cal.title,
                "type": calendarTypeDescription(cal),
                "allows_modifications": cal.allowsContentModifications
            ]
        }
        return ["calendars": calendars]
    }

    private func listEvents(start: Date, end: Date, calendarName: String?) -> [String: Any] {
        guard end > start else { return ["error": "end_date must be after start_date"] }
        guard end.timeIntervalSince(start) <= 366 * 24 * 60 * 60 else {
            return ["error": "Date range too large — limit to 1 year or less"]
        }

        var calendars = store.calendars(for: .event)
        if let calendarName {
            calendars = calendars.filter { $0.title.caseInsensitiveCompare(calendarName) == .orderedSame }
            if calendars.isEmpty {
                return ["error": "No calendar found named '\(calendarName)'"]
            }
        }

        let predicate = store.predicateForEvents(withStart: start, end: end, calendars: calendars)
        let events = store.events(matching: predicate)
            .sorted { $0.startDate < $1.startDate }
            .prefix(200)
            .map { event -> [String: Any] in
                var item: [String: Any] = [
                    "id": event.eventIdentifier ?? "",
                    "title": event.title ?? "Untitled",
                    "start": Self.isoFormatter.string(from: event.startDate),
                    "end": Self.isoFormatter.string(from: event.endDate),
                    "all_day": event.isAllDay,
                    "calendar": event.calendar?.title ?? ""
                ]
                if let location = event.location, !location.isEmpty {
                    item["location"] = location
                }
                if let notes = event.notes, !notes.isEmpty {
                    item["notes"] = String(notes.prefix(500))
                }
                return item
            }

        return ["count": events.count, "events": Array(events)]
    }

    private func createEvent(args: [String: Any]) -> [String: Any] {
        guard let title = args["title"] as? String, !title.isEmpty else {
            return ["error": "Missing required parameter: title"]
        }
        guard let startStr = args["start_date"] as? String, let start = Self.parseDate(startStr) else {
            return ["error": "Missing or invalid parameter: start_date"]
        }
        guard let endStr = args["end_date"] as? String, let end = Self.parseDate(endStr) else {
            return ["error": "Missing or invalid parameter: end_date"]
        }
        guard end >= start else {
            return ["error": "end_date must not be before start_date"]
        }

        let event = EKEvent(eventStore: store)
        event.title = title
        event.startDate = start
        event.endDate = end
        event.isAllDay = args["all_day"] as? Bool ?? false

        if let calendarName = args["calendar_name"] as? String,
           let calendar = store.calendars(for: .event).first(where: { $0.title.caseInsensitiveCompare(calendarName) == .orderedSame }) {
            event.calendar = calendar
        } else if let defaultCalendar = store.defaultCalendarForNewEvents {
            event.calendar = defaultCalendar
        } else {
            guard let fallback = store.calendars(for: .event).first(where: { $0.allowsContentModifications }) else {
                return ["error": "No writable calendar available"]
            }
            event.calendar = fallback
        }

        if let location = args["location"] as? String { event.location = location }
        if let notes = args["notes"] as? String { event.notes = notes }
        if let minutesBefore = (args["alarm_minutes_before"] as? Double) ?? (args["alarm_minutes_before"] as? Int).map(Double.init) {
            event.addAlarm(EKAlarm(relativeOffset: -(minutesBefore * 60)))
        }

        do {
            try store.save(event, span: .thisEvent, commit: true)
            return ["success": true, "event_id": event.eventIdentifier ?? "", "calendar": event.calendar?.title ?? ""]
        } catch {
            Log.mcp.error("calendar_create_event failed: \(error.localizedDescription)")
            return ["error": "Failed to create event: \(error.localizedDescription)"]
        }
    }

    // MARK: - Reminders Read Implementations

    private func listReminderLists() -> [String: Any] {
        let lists = store.calendars(for: .reminder).map { cal -> [String: Any] in
            ["name": cal.title, "allows_modifications": cal.allowsContentModifications]
        }
        return ["lists": lists]
    }

    private func listReminders(listName: String?, includeCompleted: Bool) async -> [String: Any] {
        var lists = store.calendars(for: .reminder)
        if let listName {
            lists = lists.filter { $0.title.caseInsensitiveCompare(listName) == .orderedSame }
            if lists.isEmpty {
                return ["error": "No reminder list found named '\(listName)'"]
            }
        }

        let predicate = store.predicateForReminders(in: lists)
        let reminders: [EKReminder] = await withCheckedContinuation { continuation in
            store.fetchReminders(matching: predicate) { results in
                continuation.resume(returning: results ?? [])
            }
        }

        let filtered = reminders
            .filter { includeCompleted || !$0.isCompleted }
            .sorted { lhs, rhs in
                let l = lhs.dueDateComponents?.date ?? .distantFuture
                let r = rhs.dueDateComponents?.date ?? .distantFuture
                return l < r
            }
            .prefix(200)
            .map { reminder -> [String: Any] in
                var item: [String: Any] = [
                    "id": reminder.calendarItemIdentifier,
                    "title": reminder.title ?? "Untitled",
                    "completed": reminder.isCompleted,
                    "list": reminder.calendar?.title ?? ""
                ]
                if let due = reminder.dueDateComponents?.date {
                    item["due"] = Self.isoFormatter.string(from: due)
                }
                if reminder.priority > 0 {
                    item["priority"] = priorityDescription(reminder.priority)
                }
                if let notes = reminder.notes, !notes.isEmpty {
                    item["notes"] = String(notes.prefix(500))
                }
                return item
            }

        return ["count": filtered.count, "reminders": Array(filtered)]
    }

    private func createReminder(args: [String: Any]) -> [String: Any] {
        guard let title = args["title"] as? String, !title.isEmpty else {
            return ["error": "Missing required parameter: title"]
        }

        let reminder = EKReminder(eventStore: store)
        reminder.title = title

        if let listName = args["list_name"] as? String,
           let list = store.calendars(for: .reminder).first(where: { $0.title.caseInsensitiveCompare(listName) == .orderedSame }) {
            reminder.calendar = list
        } else if let defaultList = store.defaultCalendarForNewReminders() {
            reminder.calendar = defaultList
        } else {
            guard let fallback = store.calendars(for: .reminder).first(where: { $0.allowsContentModifications }) else {
                return ["error": "No writable reminder list available"]
            }
            reminder.calendar = fallback
        }

        if let dueStr = args["due_date"] as? String, let due = Self.parseDate(dueStr) {
            reminder.dueDateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: due)
        }
        if let notes = args["notes"] as? String { reminder.notes = notes }
        if let priority = args["priority"] as? String { reminder.priority = priorityValue(priority) }

        do {
            try store.save(reminder, commit: true)
            return ["success": true, "reminder_id": reminder.calendarItemIdentifier, "list": reminder.calendar?.title ?? ""]
        } catch {
            Log.mcp.error("reminders_create failed: \(error.localizedDescription)")
            return ["error": "Failed to create reminder: \(error.localizedDescription)"]
        }
    }

    private func completeReminder(reminderId: String) -> [String: Any] {
        guard let item = store.calendarItem(withIdentifier: reminderId) as? EKReminder else {
            return ["error": "No reminder found with id '\(reminderId)'"]
        }
        item.isCompleted = true
        item.completionDate = Date()

        do {
            try store.save(item, commit: true)
            return ["success": true, "reminder_id": reminderId, "title": item.title ?? ""]
        } catch {
            Log.mcp.error("reminders_complete failed: \(error.localizedDescription)")
            return ["error": "Failed to complete reminder: \(error.localizedDescription)"]
        }
    }

    // MARK: - Helpers

    private func calendarTypeDescription(_ cal: EKCalendar) -> String {
        switch cal.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 priorityDescription(_ value: Int) -> String {
        switch value {
        case 1...4: return "high"
        case 5: return "medium"
        case 6...9: return "low"
        default: return "none"
        }
    }

    private func priorityValue(_ description: String) -> Int {
        switch description.lowercased() {
        case "high": return 1
        case "medium": return 5
        case "low": return 9
        default: return 0
        }
    }

    private func makeTool(name: String, description: String, properties: [String: Tool.Function.Parameters.Property], required: [String]) -> Tool {
        Tool(
            type: "function",
            function: Tool.Function(
                name: name,
                description: description,
                parameters: Tool.Function.Parameters(
                    type: "object",
                    properties: properties,
                    required: required
                )
            )
        )
    }

    private func prop(_ type: String, _ description: String, enumValues: [String]? = nil) -> Tool.Function.Parameters.Property {
        Tool.Function.Parameters.Property(type: type, description: description, enum: enumValues)
    }

    nonisolated static func parseArgs(_ arguments: String) -> [String: Any] {
        guard let data = arguments.data(using: .utf8),
              let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            return [:]
        }
        return dict
    }

    nonisolated static func permissionError(domain: String) -> [String: Any] {
        ["error": "\(domain) permission not granted. Grant access in Settings > MCP."]
    }

    nonisolated(unsafe) static let isoFormatter: ISO8601DateFormatter = {
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime]
        return f
    }()

    nonisolated static let displayFormatter: DateFormatter = {
        let f = DateFormatter()
        f.dateStyle = .medium
        f.timeStyle = .short
        return f
    }()

    nonisolated static func parseDate(_ string: String) -> Date? {
        if let date = isoFormatter.date(from: string) { return date }

        let isoNoTimezone = ISO8601DateFormatter()
        isoNoTimezone.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        if let date = isoNoTimezone.date(from: string) { return date }

        let localDateTime = DateFormatter()
        localDateTime.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
        if let date = localDateTime.date(from: string) { return date }

        let dateOnly = DateFormatter()
        dateOnly.dateFormat = "yyyy-MM-dd"
        if let date = dateOnly.date(from: string) { return date }

        return nil
    }
}
