import Foundation
import WatchKit
import WidgetKit

// Background refresh scheduler + runner. Phase C of the Apple Watch
// companion plan: every ~20 minutes the watch fetches the current
// tapestry bucket, writes a snapshot to the App Group container, and
// tells WidgetKit to reload the complication's timeline. Independent
// from the foreground `TapestryFeedView`'s 3s polling — that drives
// the in-app surface, this drives the on-watch-face complication.
//
// Cadence rationale. WidgetKit documents a 40–70 reloads/day budget
// per complication; 24h ÷ 20m = 72 invocations, which lines up with
// "as fresh as Apple lets us be without burning the cap." A run that
// finishes in <1s schedules the next refresh on completion, so the
// chain continues across cold launches; the foreground app also
// re-schedules on `.task` so the chain self-heals after a long
// suspension during which the OS dropped pending tasks.
//
// Reachability story. The runner reads credentials directly from
// `WatchKeychain` rather than from the live `AuthState`, because the
// app may not be running when the OS fires the task. This is the
// `kSecAttrAccessibleAfterFirstUnlock` rationale documented in
// WatchKeychain — without that accessibility class, this code path
// would silently fail every cold-from-power-off morning.

enum BackgroundRefresh {
    /// The complication's WidgetKit kind. Must match the `kind` declared
    /// inside the complication target's `HalflightTapestryComplication`
    /// widget. Drift here would silently break the reload — WidgetKit
    /// matches reload requests by kind string.
    static let widgetKind = "TapestryComplication"

    /// Default refresh interval. 20 min keeps us under WidgetKit's
    /// 40–70 reloads/day budget while still feeling responsive on a
    /// glanceable surface.
    static let interval: TimeInterval = 20 * 60

    /// Hard floor — never schedule the next refresh sooner than 5 min
    /// from now even if the previous run finished instantly. Defends
    /// against a runaway scheduler loop where a transient error path
    /// reschedules with a tiny interval and pegs the budget.
    static let minimumInterval: TimeInterval = 5 * 60

    /// Schedule the next `WKApplicationRefreshBackgroundTask`. Idempotent —
    /// the OS coalesces overlapping schedule requests, so calling this
    /// from both the foreground `.task` and the background-task completion
    /// handler is safe (one of them wins; the other is a no-op).
    @MainActor
    static func scheduleNext(after interval: TimeInterval = interval) {
        let when = Date().addingTimeInterval(max(minimumInterval, interval))
        WKApplication.shared().scheduleBackgroundRefresh(
            withPreferredDate: when,
            userInfo: nil
        ) { _ in
            // Errors here are rare and unactionable — typically "the OS
            // declined this schedule request because of low-power mode
            // or budget pressure." The next foreground appearance or
            // the next succeeding background task will re-schedule.
        }
    }

    /// One-shot refresh. Pulls credentials from the keychain (the
    /// foreground app may not be running), fetches the latest tapestry
    /// bucket, writes the snapshot to the App Group container, and
    /// asks WidgetKit to reload the complication's timeline. Then
    /// hands off to `LatestMediaRefresh.runOnce()` so a single
    /// `WKApplicationRefreshBackgroundTask` services both
    /// complications — Apple's documented "up to four background
    /// refresh tasks per hour with an active complication" budget
    /// is per-app, not per-complication.
    ///
    /// Returns as soon as the snapshot is written. Errors are swallowed
    /// — the previous snapshot stays in place and the next refresh
    /// retries.
    static func runOnce() async {
        await runTapestryOnce()
        await LatestMediaRefresh.runOnce()
    }

    private static func runTapestryOnce() async {
        guard let creds = WatchKeychain.read() else { return }
        // Non-supporting server: write an "unavailable" snapshot so a
        // user who pinned the complication then switched to a non-
        // Halflight account sees a self-explanatory message rather
        // than the previous account's letters frozen forever. The
        // writer is cheap (one App Group UserDefaults write + one
        // WidgetKit reload nudge) and runs once per refresh tick.
        guard creds.supportsTapestry else {
            let snapshot = AppGroupCache.ComplicationSnapshot(
                instance: creds.instance,
                username: creds.username,
                letters: "",
                bucket: "",
                updatedAt: Date(),
                supportsTapestry: false
            )
            AppGroupCache.writeSnapshot(snapshot)
            WidgetCenter.shared.reloadTimelines(ofKind: widgetKind)
            return
        }
        let client = WatchAPIClient(
            instance: creds.instance,
            accessToken: creds.accessToken,
            accountType: creds.accountType
        )
        do {
            let minute = TapestryWindow.currentBucket()
            let fetched = try await client.fetchTapestryBucket(minute)
            // Quiet-instance fallback: a 200 with an empty body for the
            // current minute is common. Use the freshest cached row so
            // the complication has something to render and its
            // `updatedAt` advances. If both are empty (brand-new
            // install with no history yet), write an empty-letters
            // snapshot — the complication renders dashed gaps and the
            // updatedAt refresh keeps the staleness label accurate.
            let latest =
                fetched.max(by: { $0.bucket < $1.bucket })
                ?? AppGroupCache.readTapestryHistory(instance: creds.instance)
                    .max(by: { $0.bucket < $1.bucket })
            let snapshot = AppGroupCache.ComplicationSnapshot(
                instance: creds.instance,
                username: creds.username,
                letters: latest?.letters ?? "",
                bucket: latest?.bucket ?? minute,
                updatedAt: Date(),
                supportsTapestry: true
            )
            AppGroupCache.writeSnapshot(snapshot)
            WidgetCenter.shared.reloadTimelines(ofKind: widgetKind)
        } catch {
            // Network errors are expected on a watch — sleep, fade in
            // and out of range, server hiccups. Leave the previous
            // snapshot in place; the complication's staleness guard
            // will surface the age textually if too much time passes.
        }
    }
}
