import SwiftUI
import WidgetKit
import os

// Unified-log mirror for the complication extension. The watch app's
// DebugRecorder lives in a separate target and can't see `getTimeline`
// (extensions run out-of-process), so we emit directly here. Same
// subsystem as the watch app so the dev-environment `log stream`
// predicate `subsystem == "ht.halflig.app.watchkitapp"` picks up both.
private let complicationLog = Logger(
    subsystem: "ht.halflig.app.watchkitapp",
    category: "complication"
)

// Halflight tapestry complication. Shows a snapshot of the most recent
// minute's tapestry weave on the watch face. WidgetKit's per-day reload
// budget (~40–70 reloads/day per Apple's docs) makes "live" out of
// reach — we render a snapshot and surface its age textually so users
// see "Updated 12m ago" instead of mistaking stale letters for silence.
//
// The snapshot data is read from the App Group container that the
// watch app writes on every successful poll (foreground 3s loop) and
// every successful background refresh (Phase C BackgroundRefresh).
//
// Reload mechanics. Each `getTimeline` call counts as one reload. We
// emit a single entry valid for `now`, then ask WidgetKit to come back
// in ~20 min via `.after(_:)`. That alone is 72 reloads/day — under
// the 40–70 cap with most faces, over with some. The watch app also
// fires `WidgetCenter.reloadTimelines` from its own refresh chain;
// the OS coalesces recent reloads with our scheduled `.after` so
// double-firing burns ~one extra reload, not two. In normal flow we
// stay within budget; the 24h audit counter (see audit increment
// below) makes a breach observable rather than silent.
//
// Staleness handling. The view uses `Text(_, style: .relative)` for
// the age string so it auto-ticks without re-entering getTimeline. If
// the snapshot is older than 2h, we suppress the letters in favor of
// a "Updated Xh ago" empty-state hint so a paused or unreachable
// watch can't show ancient letters as if they were live.

private let widgetKindString = "TapestryComplication"
// Key suffix MUST match the writer in
// `targets/watch/State/AppGroupCache.swift`. Bumped v1 → v2 when
// `supportsTapestry` was added to the snapshot shape; legacy v1 data
// returns nil here and the view falls back to the empty state for one
// refresh cycle until the new writer populates v2.
private let snapshotKey = "tapestry.complication.snapshot.v2"
private let auditKey = "tapestry.complication.audit.v1"
private let stalenessThreshold: TimeInterval = 2 * 60 * 60
private let timelineHorizon: TimeInterval = 20 * 60

struct TapestryEntry: TimelineEntry {
    let date: Date
    let snapshot: ComplicationSnapshot?

    struct ComplicationSnapshot {
        let instance: String
        let username: String
        let letters: String
        let bucket: String
        let updatedAt: Date
        // False when the active account's server doesn't expose the
        // tapestry endpoint. Drives the "Tapestry unavailable" branch
        // in the view below.
        let supportsTapestry: Bool
    }
}

struct TapestryProvider: TimelineProvider {
    func placeholder(in context: Context) -> TapestryEntry {
        TapestryEntry(date: Date(), snapshot: nil)
    }

    func getSnapshot(in context: Context, completion: @escaping (TapestryEntry) -> Void) {
        completion(currentEntry())
    }

    func getTimeline(
        in context: Context,
        completion: @escaping (Timeline<TapestryEntry>) -> Void
    ) {
        let audit = ComplicationAudit.bump(key: auditKey)
        let now = Date()
        let entry = currentEntry()
        let ageSec: String
        let supports: String
        if let s = entry.snapshot {
            ageSec = String(Int(now.timeIntervalSince(s.updatedAt)))
            supports = s.supportsTapestry ? "t" : "f"
        } else {
            ageSec = "nil"
            supports = "nil"
        }
        complicationLog.notice(
            "getTimeline audit=\(audit, privacy: .public) snapshotAge=\(ageSec, privacy: .public)s supports=\(supports, privacy: .public)"
        )
        let timeline = Timeline(
            entries: [entry],
            policy: .after(now.addingTimeInterval(timelineHorizon))
        )
        completion(timeline)
    }

    private func currentEntry() -> TapestryEntry {
        TapestryEntry(date: Date(), snapshot: readSnapshot())
    }

    // Direct UserDefaults read against the shared App Group suite. The
    // watch app's `AppGroupCache.swift` lives in a separate target and
    // can't be imported here — extensions compile independently. The
    // keys are duplicated; they MUST stay in sync with the watch
    // app's writer. Marked at the top of this file as a contract.
    private func readSnapshot() -> TapestryEntry.ComplicationSnapshot? {
        guard let defaults = UserDefaults(suiteName: ComplicationAudit.appGroupSuiteName)
        else { return nil }
        guard let data = defaults.data(forKey: snapshotKey) else { return nil }
        struct Decoded: Codable {
            let instance: String
            let username: String
            let letters: String
            let bucket: String
            let updatedAt: Date
            let supportsTapestry: Bool
        }
        guard let decoded = try? JSONDecoder().decode(Decoded.self, from: data) else { return nil }
        return TapestryEntry.ComplicationSnapshot(
            instance: decoded.instance,
            username: decoded.username,
            letters: decoded.letters,
            bucket: decoded.bucket,
            updatedAt: decoded.updatedAt,
            supportsTapestry: decoded.supportsTapestry
        )
    }

}

struct TapestryComplicationView: View {
    let entry: TapestryEntry
    @Environment(\.widgetRenderingMode) private var renderingMode

    var body: some View {
        Group {
            if let s = entry.snapshot, !s.supportsTapestry {
                unavailableSnapshot(s)
            } else if let s = entry.snapshot, !isStale(s) {
                liveSnapshot(s)
            } else if let s = entry.snapshot {
                staleSnapshot(s)
            } else {
                emptyState
            }
        }
        .containerBackground(for: .widget) {
            AccessoryWidgetBackground()
        }
    }

    // The active account's server doesn't expose the tapestry
    // endpoint. Show a self-explanatory message instead of the empty
    // state, naming the account so the user can connect "I switched
    // to @user@instance" with "the complication went dark."
    @ViewBuilder
    private func unavailableSnapshot(_ s: TapestryEntry.ComplicationSnapshot) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text("Tapestry unavailable")
                .font(.caption.weight(.semibold))
                .lineLimit(1)
                .widgetAccentable()
            Text("@\(s.username)@\(s.instance)")
                .font(.caption2)
                .foregroundStyle(.secondary)
                .lineLimit(1)
                .truncationMode(.middle)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    // Two lines of letters fill the tile; the live state's ticking
    // timestamp was chrome — staleness handling still kicks in past
    // `stalenessThreshold` via `staleSnapshot`, which keeps the
    // "Updated Xh ago" age string so the user can tell a paused watch
    // apart from a fresh weave.
    @ViewBuilder
    private func liveSnapshot(_ s: TapestryEntry.ComplicationSnapshot) -> some View {
        Text(s.letters.isEmpty ? "·" : s.letters)
            .font(.system(.body, design: .monospaced).weight(.bold))
            .lineLimit(2)
            .truncationMode(.tail)
            .widgetAccentable()
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
    }

    @ViewBuilder
    private func staleSnapshot(_ s: TapestryEntry.ComplicationSnapshot) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text("—")
                .font(.system(.body, design: .monospaced).weight(.bold))
                .widgetAccentable()
            // Use `style: .relative` so the age keeps ticking up
            // without WidgetKit re-entering getTimeline. The "Updated"
            // prefix disambiguates from the live state's bare ticker.
            HStack(spacing: 3) {
                Text("Updated")
                Text(s.updatedAt, style: .relative)
            }
            .font(.caption2)
            .foregroundStyle(.secondary)
            .lineLimit(1)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
    }

    private var emptyState: some View {
        VStack(alignment: .leading, spacing: 2) {
            Text("—")
                .font(.system(.body, design: .monospaced).weight(.bold))
                .widgetAccentable()
            Text("No data yet")
                .font(.caption2)
                .foregroundStyle(.secondary)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    private func isStale(_ s: TapestryEntry.ComplicationSnapshot) -> Bool {
        Date().timeIntervalSince(s.updatedAt) > stalenessThreshold
    }
}

// The `@main` entry point lives in `HalflightWidgets.swift` — a
// `WidgetBundle` that combines this Tapestry widget with the Latest
// Media widget. Only one `@main`-annotated type is allowed per target,
// and a WidgetBundle is Apple's documented multi-widget container
// (see `WidgetBundle`).
struct HalflightTapestryComplication: Widget {
    let kind: String = widgetKindString

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: TapestryProvider()) { entry in
            TapestryComplicationView(entry: entry)
        }
        .configurationDisplayName("Halflight Tapestry")
        .description("A snapshot of your server's tapestry weave.")
        .supportedFamilies([.accessoryRectangular])
    }
}
