import EventKit
import Foundation
import LilithLogging
import MacSyncShared

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

// MARK: - Wire types

public struct CalendarSendPayload: Sendable, Decodable, Equatable {
    public let eventIdentifier: String?
    public let calendarIdentifier: String?
    public let title: String?
    public let notes: String?
    public let location: String?
    public let startDate: String?
    public let endDate: String?
    public let isAllDay: Bool?
    public let url: String?

    public init(
        eventIdentifier: String? = nil,
        calendarIdentifier: String? = nil,
        title: String? = nil,
        notes: String? = nil,
        location: String? = nil,
        startDate: String? = nil,
        endDate: String? = nil,
        isAllDay: Bool? = nil,
        url: String? = nil
    ) {
        self.eventIdentifier = eventIdentifier
        self.calendarIdentifier = calendarIdentifier
        self.title = title
        self.notes = notes
        self.location = location
        self.startDate = startDate
        self.endDate = endDate
        self.isAllDay = isAllDay
        self.url = url
    }
}

public struct PendingCalendarSend: Sendable, Decodable, Equatable {
    public let id: String
    public let action: String
    public let payload: CalendarSendPayload
    public let createdAt: String

    public init(id: String, action: String, payload: CalendarSendPayload, createdAt: String) {
        self.id = id
        self.action = action
        self.payload = payload
        self.createdAt = createdAt
    }
}

// MARK: - Applier protocol (allows hermetic tests without touching EKEventStore)

/// Narrow protocol the `CalendarSender` calls into so tests can substitute a
/// fake that does not require EventKit authorization.
@MainActor
public protocol CalendarEventApplying {
    func calendar(withIdentifier id: String) -> EKCalendar?
    func event(withIdentifier id: String) -> EKEvent?
    func makeEvent() -> EKEvent
    func save(_ event: EKEvent, span: EKSpan) throws
    func remove(_ event: EKEvent, span: EKSpan) throws
}

/// Default `CalendarEventApplying` backed by a real `EKEventStore`.
@MainActor
public final class EKEventStoreApplier: CalendarEventApplying {
    private let store: EKEventStore
    public init(eventStore: EKEventStore) { self.store = eventStore }

    public func calendar(withIdentifier id: String) -> EKCalendar? {
        store.calendar(withIdentifier: id)
    }

    public func event(withIdentifier id: String) -> EKEvent? {
        store.event(withIdentifier: id)
    }

    public func makeEvent() -> EKEvent {
        EKEvent(eventStore: store)
    }

    public func save(_ event: EKEvent, span: EKSpan) throws {
        try store.save(event, span: span, commit: true)
    }

    public func remove(_ event: EKEvent, span: EKSpan) throws {
        try store.remove(event, span: span, commit: true)
    }
}

// MARK: - Sender

/// Applies pending calendar send-queue items against the local EventKit store.
@MainActor
public final class CalendarSender {
    private let applier: any CalendarEventApplying
    private let iso: ISO8601DateFormatter

    public init(applier: any CalendarEventApplying) {
        self.applier = applier
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        self.iso = f
    }

    public convenience init(eventStore: EKEventStore) {
        self.init(applier: EKEventStoreApplier(eventStore: eventStore))
    }

    public func apply(_ item: PendingCalendarSend) async -> SendQueueApplyResult {
        log.info("apply id=\(item.id) action=\(item.action)")
        switch item.action {
        case "create_event": return applyCreate(item.payload)
        case "update_event": return applyUpdate(item.payload)
        case "delete_event": return applyDelete(item.payload)
        default:
            return .failed(reason: "unknown action: \(item.action)")
        }
    }

    // MARK: - create

    private func applyCreate(_ p: CalendarSendPayload) -> SendQueueApplyResult {
        guard let calId = p.calendarIdentifier, !calId.isEmpty else {
            return .failed(reason: "missing calendarIdentifier")
        }
        guard let title = p.title, !title.isEmpty else {
            return .failed(reason: "missing title")
        }
        guard let startStr = p.startDate, !startStr.isEmpty else {
            return .failed(reason: "missing startDate")
        }
        guard let endStr = p.endDate, !endStr.isEmpty else {
            return .failed(reason: "missing endDate")
        }
        guard let start = parseDate(startStr) else {
            return .failed(reason: "invalid startDate")
        }
        guard let end = parseDate(endStr) else {
            return .failed(reason: "invalid endDate")
        }
        guard let calendar = applier.calendar(withIdentifier: calId) else {
            return .failed(reason: "calendar not found: \(calId)")
        }

        let event = applier.makeEvent()
        event.calendar = calendar
        event.title = title
        event.startDate = start
        event.endDate = end
        if let notes = p.notes { event.notes = notes }
        if let location = p.location { event.location = location }
        if let isAllDay = p.isAllDay { event.isAllDay = isAllDay }
        if let urlStr = p.url, let url = URL(string: urlStr) { event.url = url }

        do {
            try applier.save(event, span: .thisEvent)
            return .sent
        } catch {
            return .failed(reason: "save failed: \(error.localizedDescription)")
        }
    }

    // MARK: - update

    private func applyUpdate(_ p: CalendarSendPayload) -> SendQueueApplyResult {
        guard let eventId = p.eventIdentifier, !eventId.isEmpty else {
            return .failed(reason: "missing eventIdentifier")
        }
        guard let event = applier.event(withIdentifier: eventId) else {
            return .failed(reason: "event not found: \(eventId)")
        }
        if let title = p.title { event.title = title }
        if let notes = p.notes { event.notes = notes }
        if let location = p.location { event.location = location }
        if let isAllDay = p.isAllDay { event.isAllDay = isAllDay }
        if let urlStr = p.url, let url = URL(string: urlStr) { event.url = url }
        if let startStr = p.startDate {
            guard let start = parseDate(startStr) else {
                return .failed(reason: "invalid startDate")
            }
            event.startDate = start
        }
        if let endStr = p.endDate {
            guard let end = parseDate(endStr) else {
                return .failed(reason: "invalid endDate")
            }
            event.endDate = end
        }

        do {
            try applier.save(event, span: .thisEvent)
            return .sent
        } catch {
            return .failed(reason: "save failed: \(error.localizedDescription)")
        }
    }

    // MARK: - delete

    private func applyDelete(_ p: CalendarSendPayload) -> SendQueueApplyResult {
        guard let eventId = p.eventIdentifier, !eventId.isEmpty else {
            return .failed(reason: "missing eventIdentifier")
        }
        guard let event = applier.event(withIdentifier: eventId) else {
            return .failed(reason: "event not found: \(eventId)")
        }
        do {
            try applier.remove(event, span: .thisEvent)
            return .sent
        } catch {
            return .failed(reason: "remove failed: \(error.localizedDescription)")
        }
    }

    // MARK: - date parsing

    /// Parse ISO8601 with or without fractional seconds.
    private func parseDate(_ s: String) -> Date? {
        if let d = iso.date(from: s) { return d }
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime]
        return f.date(from: s)
    }
}

// MARK: - SendQueueTransport adapter

public struct CalendarSendTransport: SendQueueTransport {
    public typealias PendingItem = PendingCalendarSend

    private let apiClient: any ICalAPIClientProtocol

    public init(apiClient: any ICalAPIClientProtocol) {
        self.apiClient = apiClient
    }

    public func id(of item: PendingCalendarSend) -> String { item.id }

    public func fetchPending() async throws -> [PendingCalendarSend] {
        try await apiClient.getPendingSends()
    }

    public func reportResult(id: String, status: String, error: String?) async throws {
        try await apiClient.reportSendResult(id: id, status: status, error: error)
    }
}
