import SwiftUI
import UIKit
import WidgetKit
import os

// Halflight latest-media complication. Sibling of the Tapestry
// complication in the same extension target — bundled together via
// `HalflightWidgets` (`WidgetBundle`). One widget extension can ship
// multiple `Widget` types, exposed to the user as independently-
// pickable tiles in the watch-face gallery (see Apple's
// `WidgetBundle`).
//
// Data source: the watch app's `LatestMediaRefresh` writes a
// `LatestMediaSnapshot` to the shared App Group container on every
// background refresh tick (~20 min) and after foreground feed loads.
// This file is read-only against that record — extensions compile
// independently and have no keychain / network surface of their own,
// so the pre-downscaled JPEG bytes inline in the snapshot are the
// only path to a thumbnail.
//
// v2 reshape: the snapshot now carries an `items: [MediaItem]` array
// of up to 3 most-recent media posts. The rectangular family renders
// a 3-up grid edge-to-edge (image-only, no caption); the circular
// family renders the newest item filling the disc. Both supported via
// the same `Widget` declaration through `@Environment(\.widgetFamily)`.
//
// Tap routing: `widgetURL("halflight://watch/feed")` delivers a
// `NSUserActivity` to the watch app. The watch app's belt-and-
// suspenders handlers (`onContinueUserActivity`, `onOpenURL`,
// `WatchAppDelegate.handle(_:)`) all post a
// `.halflightOpenFeed` notification — `RootView` snaps `selection`
// to `.feed`. The redundancy is deliberate; Apple's docs don't
// crisply state which hook fires for watchOS widgetURL taps.

private let widgetKindString = "LatestMediaComplication"
// MUST match the writer's key in
// `targets/watch/State/AppGroupCache.swift`. Bumped on any shape
// change to LatestMediaSnapshot; the reader fails closed on decode
// error and falls back to the empty-state placeholder.
private let snapshotKey = "latest_media.complication.snapshot.v2"
private let auditKey = "latest_media.complication.audit.v1"
private let stalenessThreshold: TimeInterval = 2 * 60 * 60
private let timelineHorizon: TimeInterval = 20 * 60
private let deepLinkURL = URL(string: "halflight://watch/feed")

private let latestMediaLog = Logger(
    subsystem: "ht.halflig.app.watchkitapp",
    category: "complication.latest-media"
)

struct LatestMediaEntry: TimelineEntry {
    let date: Date
    let snapshot: Snapshot?

    struct Snapshot {
        let instance: String
        let username: String
        let items: [MediaItem]
        let updatedAt: Date
        let signedIn: Bool

        struct MediaItem {
            let statusId: String
            let mediaUrl: String
            let altText: String?
            let postedAt: Date
            let imageBytes: Data?
        }
    }
}

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

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

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

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

    /// Direct UserDefaults read against the shared App Group suite.
    /// Mirror of the contract in `AppGroupCache.LatestMediaSnapshot`
    /// — keep field names in sync with the writer. v1 records under
    /// the old `…snapshot.v1` key are ignored by suffix mismatch and
    /// fall through to the placeholder branch.
    private func readSnapshot() -> LatestMediaEntry.Snapshot? {
        guard let defaults = UserDefaults(suiteName: ComplicationAudit.appGroupSuiteName)
        else { return nil }
        guard let data = defaults.data(forKey: snapshotKey) else { return nil }
        struct DecodedItem: Codable {
            let statusId: String
            let mediaUrl: String
            let altText: String?
            let postedAt: Date
            let imageBytes: Data?
        }
        struct Decoded: Codable {
            let instance: String
            let username: String
            let items: [DecodedItem]
            let updatedAt: Date
            let signedIn: Bool
        }
        guard let decoded = try? JSONDecoder().decode(Decoded.self, from: data)
        else { return nil }
        return LatestMediaEntry.Snapshot(
            instance: decoded.instance,
            username: decoded.username,
            items: decoded.items.map { item in
                LatestMediaEntry.Snapshot.MediaItem(
                    statusId: item.statusId,
                    mediaUrl: item.mediaUrl,
                    altText: item.altText,
                    postedAt: item.postedAt,
                    imageBytes: item.imageBytes
                )
            },
            updatedAt: decoded.updatedAt,
            signedIn: decoded.signedIn
        )
    }
}

struct LatestMediaComplicationView: View {
    let entry: LatestMediaEntry
    @Environment(\.widgetFamily) private var family

    var body: some View {
        content
            .widgetURL(deepLinkURL)
    }

    @ViewBuilder
    private var content: some View {
        if let s = entry.snapshot, !s.signedIn {
            signedOut
        } else if let s = entry.snapshot, isStale(s) {
            stale(s)
        } else if let s = entry.snapshot, s.items.isEmpty {
            emptyFeed(s)
        } else if let s = entry.snapshot {
            live(s)
        } else {
            placeholder
        }
    }

    // MARK: - Live state

    @ViewBuilder
    private func live(_ s: LatestMediaEntry.Snapshot) -> some View {
        switch family {
        case .accessoryCircular:
            circularLive(s)
        default:
            rectLive(s)
        }
    }

    /// Rectangular: three squares edge-to-edge, image-only — drops
    /// the v1 caption per user feedback. The HStack lives in the
    /// foreground with infinite frame so it claims the full widget
    /// area; `containerBackground` is the system tile backdrop
    /// (visible only at the rounded corners on the watch face). On
    /// watchOS, putting non-trivial content inside the container
    /// background closure renders nothing — content must be in the
    /// foreground.
    @ViewBuilder
    private func rectLive(_ s: LatestMediaEntry.Snapshot) -> some View {
        let slots = paddedItems(s.items, count: 3)
        HStack(spacing: 2) {
            ForEach(0..<slots.count, id: \.self) { idx in
                squareThumb(slots[idx])
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .containerBackground(for: .widget) { Color.black }
        .accessibilityElement(children: .combine)
        .accessibilityLabel(accessibilityLabel(for: s))
    }

    /// Circular: the newest item fills the disc. The image lives in
    /// the foreground with infinite frame and `scaledToFill`; the
    /// system masks the foreground content to a circle on the
    /// accessoryCircular family, so the image clips automatically.
    @ViewBuilder
    private func circularLive(_ s: LatestMediaEntry.Snapshot) -> some View {
        if let item = s.items.first,
            let data = item.imageBytes,
            let image = UIImage(data: data)
        {
            Image(uiImage: image)
                .resizable()
                .scaledToFill()
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .clipped()
                .containerBackground(for: .widget) { Color.black }
                .accessibilityLabel(accessibilityLabel(for: s))
        } else {
            // First item exists but its image bytes failed — fall back
            // to the photo glyph rather than ghost an empty disc.
            Image(systemName: "photo")
                .foregroundStyle(.secondary)
                .containerBackground(for: .widget) {
                    AccessoryWidgetBackground()
                }
        }
    }

    /// Single square cell inside the rectangular 3-up grid. Falls
    /// through to a glyph tile if the slot is unfilled (fewer than 3
    /// media items) or the bytes can't decode.
    @ViewBuilder
    private func squareThumb(
        _ item: LatestMediaEntry.Snapshot.MediaItem?
    ) -> some View {
        if let item, let data = item.imageBytes, let image = UIImage(data: data) {
            Image(uiImage: image)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .clipped()
                .accessibilityHidden(true)
        } else {
            Rectangle()
                .fill(.secondary.opacity(0.2))
                .overlay(
                    Image(systemName: "photo")
                        .foregroundStyle(.secondary)
                )
                .accessibilityHidden(true)
        }
    }

    /// Pad the items array to exactly `count` entries (with nil
    /// placeholders trailing) so the rectangular grid always renders
    /// three equal-width cells. Truncates if items > count.
    private func paddedItems(
        _ items: [LatestMediaEntry.Snapshot.MediaItem],
        count: Int
    ) -> [LatestMediaEntry.Snapshot.MediaItem?] {
        var slots: [LatestMediaEntry.Snapshot.MediaItem?] = items
            .prefix(count)
            .map { $0 }
        while slots.count < count { slots.append(nil) }
        return slots
    }

    // MARK: - Non-live states

    @ViewBuilder
    private func stale(_ s: LatestMediaEntry.Snapshot) -> some View {
        switch family {
        case .accessoryCircular:
            VStack(spacing: 1) {
                Image(systemName: "photo")
                    .foregroundStyle(.secondary)
                Text(s.updatedAt, style: .relative)
                    .font(.caption2)
                    .foregroundStyle(.secondary)
                    .lineLimit(1)
            }
            .containerBackground(for: .widget) { AccessoryWidgetBackground() }
        default:
            HStack(spacing: 6) {
                RoundedRectangle(cornerRadius: 6)
                    .fill(.secondary.opacity(0.2))
                    .frame(width: 56, height: 56)
                    .overlay(
                        Image(systemName: "photo").foregroundStyle(.secondary)
                    )
                    .accessibilityHidden(true)
                VStack(alignment: .leading, spacing: 2) {
                    Text(s.username.isEmpty ? "Latest media" : "@\(s.username)")
                        .font(.caption.weight(.semibold))
                        .lineLimit(1)
                        .truncationMode(.middle)
                        .widgetAccentable()
                    HStack(spacing: 3) {
                        Text("Updated")
                        Text(s.updatedAt, style: .relative)
                    }
                    .font(.caption2)
                    .foregroundStyle(.secondary)
                    .lineLimit(1)
                }
                Spacer(minLength: 0)
            }
            .containerBackground(for: .widget) { AccessoryWidgetBackground() }
        }
    }

    @ViewBuilder
    private func emptyFeed(_ s: LatestMediaEntry.Snapshot) -> some View {
        switch family {
        case .accessoryCircular:
            Image(systemName: "photo.on.rectangle")
                .foregroundStyle(.secondary)
                .containerBackground(for: .widget) { AccessoryWidgetBackground() }
        default:
            HStack(spacing: 6) {
                RoundedRectangle(cornerRadius: 6)
                    .fill(.secondary.opacity(0.2))
                    .frame(width: 56, height: 56)
                    .overlay(
                        Image(systemName: "photo.on.rectangle")
                            .foregroundStyle(.secondary)
                    )
                    .accessibilityHidden(true)
                VStack(alignment: .leading, spacing: 2) {
                    Text("No media yet")
                        .font(.caption.weight(.semibold))
                        .lineLimit(1)
                        .widgetAccentable()
                    Text("@\(s.username)")
                        .font(.caption2)
                        .foregroundStyle(.secondary)
                        .lineLimit(1)
                        .truncationMode(.middle)
                }
                Spacer(minLength: 0)
            }
            .containerBackground(for: .widget) { AccessoryWidgetBackground() }
        }
    }

    @ViewBuilder
    private var signedOut: some View {
        switch family {
        case .accessoryCircular:
            Image(systemName: "person.crop.circle.badge.questionmark")
                .foregroundStyle(.secondary)
                .containerBackground(for: .widget) { AccessoryWidgetBackground() }
        default:
            HStack(spacing: 6) {
                RoundedRectangle(cornerRadius: 6)
                    .fill(.secondary.opacity(0.2))
                    .frame(width: 56, height: 56)
                    .overlay(
                        Image(systemName: "person.crop.circle.badge.questionmark")
                            .foregroundStyle(.secondary)
                    )
                    .accessibilityHidden(true)
                VStack(alignment: .leading, spacing: 2) {
                    Text("Sign in on iPhone")
                        .font(.caption.weight(.semibold))
                        .lineLimit(2)
                        .widgetAccentable()
                }
                Spacer(minLength: 0)
            }
            .containerBackground(for: .widget) { AccessoryWidgetBackground() }
        }
    }

    @ViewBuilder
    private var placeholder: some View {
        switch family {
        case .accessoryCircular:
            Image(systemName: "photo")
                .foregroundStyle(.secondary)
                .containerBackground(for: .widget) { AccessoryWidgetBackground() }
        default:
            HStack(spacing: 6) {
                RoundedRectangle(cornerRadius: 6)
                    .fill(.secondary.opacity(0.2))
                    .frame(width: 56, height: 56)
                    .overlay(
                        Image(systemName: "photo").foregroundStyle(.secondary)
                    )
                    .accessibilityHidden(true)
                VStack(alignment: .leading, spacing: 2) {
                    Text("Latest media")
                        .font(.caption.weight(.semibold))
                        .lineLimit(1)
                        .widgetAccentable()
                    Text("No data yet")
                        .font(.caption2)
                        .foregroundStyle(.secondary)
                        .lineLimit(1)
                }
                Spacer(minLength: 0)
            }
            .containerBackground(for: .widget) { AccessoryWidgetBackground() }
        }
    }

    // MARK: - Helpers

    private func accessibilityLabel(for s: LatestMediaEntry.Snapshot) -> String {
        let first = s.items.first
        let alt = first?.altText?.trimmingCharacters(in: .whitespacesAndNewlines)
        let posted = first.map { relativePosted($0.postedAt) } ?? ""
        let count = s.items.count
        let countPhrase = count == 1 ? "Latest post" : "Latest \(count) posts"
        let base = "\(countPhrase) by @\(s.username), \(posted)"
        if let alt, !alt.isEmpty {
            return "\(base). \(alt). Activates to open feed."
        }
        return "\(base). Activates to open feed."
    }

    private func relativePosted(_ date: Date) -> String {
        let f = RelativeDateTimeFormatter()
        f.unitsStyle = .full
        return f.localizedString(for: date, relativeTo: Date())
    }

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

struct HalflightLatestMediaComplication: Widget {
    let kind: String = widgetKindString

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: LatestMediaProvider()) { entry in
            LatestMediaComplicationView(entry: entry)
        }
        .configurationDisplayName("Halflight Latest Media")
        .description("The most recent media posts from your home feed.")
        .supportedFamilies([.accessoryRectangular, .accessoryCircular])
    }
}
