import Combine
import Foundation
import LilithLogging

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

/// Open base class that captures the cross-module sync lifecycle every module
/// previously re-implemented:
///
/// * `@Published` state machine (`isSyncing`, `lastSyncCompletedAt`,
///   `currentOperation`, `syncError`, `stats`)
/// * UserDefaults watermark persistence keyed by a per-module
///   `persistenceKey`
/// * Periodic `Timer` lifecycle (`startSync`/`stopSync`/`syncNow`)
/// * Authorization gate hook (`requestAuthorization` / `isAuthorized` /
///   `onAuthorizationDenied`)
///
/// Subclasses override `performSync()` to do the actual work and may override
/// the authorization hooks when the module needs OS permission (EventKit,
/// AppleScript automation, etc.). All lifecycle methods are `final` so the
/// contract cannot drift between modules.
///
/// Generic parameters are intentionally lightweight: this class deliberately
/// avoids constraining `SyncError` to a protocol since each module's error
/// enum has its own associated values. Connection-error detection lives in
/// the free helper `SyncConnectionErrorHeuristic` so subclasses can map a
/// caught `Error` into their own enum.
@MainActor
open class BaseSyncManager<Stats: Sendable, SyncError: Sendable>: ObservableObject {
    // MARK: - Published state (single source of truth across all modules)

    @Published public var isSyncing: Bool = false
    @Published public var lastSyncCompletedAt: Date?
    @Published public var currentOperation: String = ""
    @Published public var syncError: SyncError
    @Published public var stats: Stats

    // MARK: - Watermark (Mac → server delta cursor)

    /// Last time a sync cycle started successfully. Subclasses read this to
    /// scope incremental fetches; the base updates it after every cycle.
    public private(set) var lastSync: Date?

    // MARK: - Configuration

    public let persistenceKey: String
    public let timerInterval: TimeInterval

    // MARK: - Internals

    private var syncTimer: Timer?

    // MARK: - Init

    public init(
        initialStats: Stats,
        noError: SyncError,
        persistenceKey: String,
        timerInterval: TimeInterval = 300
    ) {
        precondition(!persistenceKey.isEmpty, "persistenceKey must be non-empty")
        precondition(timerInterval > 0, "timerInterval must be positive")
        self.stats = initialStats
        self.syncError = noError
        self.persistenceKey = persistenceKey
        self.timerInterval = timerInterval
        self.lastSync = UserDefaults.standard.object(forKey: Self.lastSyncKey(persistenceKey)) as? Date
        self.lastSyncCompletedAt = UserDefaults.standard.object(forKey: Self.lastSyncCompletedKey(persistenceKey)) as? Date
        log.info("[\(self.persistenceKey)] init lastSync=\(String(describing: self.lastSync))")
    }

    deinit {
        syncTimer?.invalidate()
    }

    // MARK: - Lifecycle (final — single source of truth)

    /// Start periodic syncing. Kicks one cycle immediately, then schedules a
    /// repeating `Timer` at `timerInterval`. Calls `didStartSync()` once the
    /// read timer is armed so subclasses can start additional resources
    /// (e.g. a SendQueueClient for the outbound direction).
    public final func startSync() {
        log.info("[\(self.persistenceKey)] startSync")
        Task { [weak self] in
            guard let self else { return }
            await self.gatedRunCycle()
        }
        syncTimer?.invalidate()
        syncTimer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true) { [weak self] _ in
            Task { @MainActor in self?.syncNow() }
        }
        log.info("[\(self.persistenceKey)] scheduled timer every \(self.timerInterval)s")
        didStartSync()
    }

    /// Stop periodic syncing. Calls `willStopSync()` before invalidating the
    /// read timer so subclasses can tear down additional resources.
    public final func stopSync() {
        willStopSync()
        syncTimer?.invalidate()
        syncTimer = nil
    }

    /// Run a single cycle immediately if one is not already in flight.
    /// `isSyncing` is flipped synchronously here so rapid back-to-back calls
    /// from the same actor coalesce into one in-flight cycle.
    public final func syncNow() {
        guard !isSyncing else { return }
        isSyncing = true
        Task { [weak self] in
            await self?.gatedRunCycle()
        }
    }

    private func gatedRunCycle() async {
        defer { isSyncing = false }
        if !(await self.isAuthorized()) {
            let granted = await self.requestAuthorization()
            guard granted else {
                self.onAuthorizationDenied()
                return
            }
        }
        await self.runCycle()
    }

    private func runCycle() async {
        await performSync()
        let now = Date()
        setLastSync(now)
        lastSyncCompletedAt = now
        UserDefaults.standard.set(now, forKey: Self.lastSyncCompletedKey(persistenceKey))
    }

    // MARK: - Watermark helpers (subclasses may need to reset on full re-sync)

    /// Update `lastSync` and persist it. Pass `nil` to clear the watermark
    /// (e.g., a "Reset and resync" flow).
    public func setLastSync(_ date: Date?) {
        lastSync = date
        if let date {
            UserDefaults.standard.set(date, forKey: Self.lastSyncKey(persistenceKey))
        } else {
            UserDefaults.standard.removeObject(forKey: Self.lastSyncKey(persistenceKey))
        }
    }

    // MARK: - Subclass hooks (override these — not the lifecycle methods)

    /// Subclasses MUST override. Perform the actual sync work; the base
    /// handles state transitions before and after the call.
    open func performSync() async {
        assertionFailure("BaseSyncManager.performSync must be overridden by \(type(of: self))")
    }

    /// Override when the module requires OS permission (EventKit, AppleScript
    /// automation, Mail.app, etc.). Default returns `true` (no permission
    /// required).
    open func requestAuthorization() async -> Bool { true }

    /// Override to check the current authorization status without prompting.
    /// Default returns `true`.
    open func isAuthorized() async -> Bool { true }

    /// Override to set a module-specific `syncError` when authorization is
    /// denied. Default is a no-op.
    open func onAuthorizationDenied() { }

    /// Called from `startSync()` after the read timer is armed. Subclasses
    /// with a bidirectional Sender override this to start their
    /// `SendQueueClient`. Default is a no-op.
    open func didStartSync() { }

    /// Called from `stopSync()` before the read timer is torn down.
    /// Subclasses override to stop additional resources (SendQueueClient
    /// timers, AppleScript probes, etc.). Default is a no-op.
    open func willStopSync() { }

    // MARK: - UserDefaults keys

    private static func lastSyncKey(_ persistenceKey: String) -> String {
        "\(persistenceKey).lastSync"
    }

    private static func lastSyncCompletedKey(_ persistenceKey: String) -> String {
        "\(persistenceKey).lastSyncCompletedAt"
    }
}
