import Foundation

/// The content the iOS Live Activity renders (UX_IOS §5.3), in a form the **host** can build and
/// push over APNs so the glance stays fresh while the phone is locked — the phone's local update
/// path is dead then (its socket is suspended), so the Mac has to supply the new state.
///
/// This is the wire mirror of the phone's `NucleicSessionAttributes.ContentState`: the APNs
/// `content-state` JSON this encodes to MUST decode into that type. So the stored-property names
/// here are kept byte-identical to it, and `backend`/`kind` are the exact enum *raw values* the
/// phone's `Backend`/`Kind` use ("claude"/"codex"/"grok"/"other" and "running"/"provisioning"/
/// "approval"/"needsInput"/"done"/"error"/"idle"). They're plain `String`s, not enums, so this
/// module needn't import the phone's ActivityKit types. `LiveActivitySnapshotTests` pins the JSON
/// shape; keep the two in lockstep when either changes.
public struct LiveActivitySnapshot: Sendable, Codable, Equatable {
    /// One agent session in the glance's detail rows.
    public struct Line: Sendable, Codable, Equatable {
        public let id: String
        public let title: String
        public let project: String
        public let backend: String
        public let kind: String
        public let detail: String
        /// The session's single most-urgent pending approval, when it's blocked on one — the id the
        /// glance's inline Allow/Deny resolve. Mirrors the phone's `SessionLine.approvalID`; the JSON
        /// keys MUST match so a pushed content-state decodes there. `nil` (omitted) when the session
        /// isn't awaiting approval, so a plain row falls back to a deep-link tap exactly as before.
        public let approvalID: String?
        /// Whether that approval is high-risk — the glance then hides inline Allow (§3.3). `nil` when
        /// there's no inline approval.
        public let approvalIsHighRisk: Bool?

        public init(
            id: String, title: String, project: String,
            backend: String, kind: String, detail: String,
            approvalID: String? = nil, approvalIsHighRisk: Bool? = nil
        ) {
            self.id = id
            self.title = title
            self.project = project
            self.backend = backend
            self.kind = kind
            self.detail = detail
            self.approvalID = approvalID
            self.approvalIsHighRisk = approvalIsHighRisk
        }
    }

    public let runningCount: Int
    public let needsYouCount: Int
    public let approvalCount: Int
    public let filesChanged: Int
    public let linesAdded: Int
    public let linesRemoved: Int
    public let lines: [Line]

    public init(
        runningCount: Int, needsYouCount: Int, approvalCount: Int,
        filesChanged: Int, linesAdded: Int, linesRemoved: Int, lines: [Line]
    ) {
        self.runningCount = runningCount
        self.needsYouCount = needsYouCount
        self.approvalCount = approvalCount
        self.filesChanged = filesChanged
        self.linesAdded = linesAdded
        self.linesRemoved = linesRemoved
        self.lines = lines
    }
}

extension LiveActivitySnapshot {
    /// How many detail rows the glance carries — matches the phone's `LiveActivityManager.maxLines`.
    public static let maxLines = 3

    /// The attention-bearing fingerprint of the glance: the headline counts plus each row's
    /// identity and `kind`. It deliberately EXCLUDES the mid-turn churn (files/±lines and the
    /// churn-derived row detail), which ticks on every transcript edit while the agent works.
    /// Live Activity pushes are gated on this so a push fires when a session's *status* changes —
    /// starts, finishes, needs approval/input — not on every diff delta mid-turn. Rapid churn
    /// pushes drain the phone's battery and burn APNs's Live Activity budget (risking throttling)
    /// for a number that's illegible at a glance anyway. Mirrors the phone's `ContentState`
    /// equivalent; the churn still rides along whenever a status change *does* push. (When the
    /// signature matches, the last-pushed glance's churn is a few edits stale — an accepted trade.)
    public var attentionSignature: [String] {
        var parts = ["run:\(runningCount)", "need:\(needsYouCount)", "appr:\(approvalCount)"]
        parts.append(contentsOf: lines.map { "\($0.id):\($0.kind)" })
        return parts
    }

    /// Aggregate a host's live sessions into the glance, or `nil` when nothing is running or
    /// waiting (the host then pushes an "end"). This mirrors the phone's `LiveActivityManager.sync`
    /// exactly — same attention-first ordering, counts, churn totals, per-row detail — so a pushed
    /// update while locked reads identically to the local update the phone would have made.
    public static func build(from sessions: [SessionSummary]) -> LiveActivitySnapshot? {
        let live = sessions.filter { !$0.archived }
        let running = live.filter { $0.status == .running || $0.status == .provisioning }
        let needsYou = live.filter(sessionNeedsYou)
        guard !running.isEmpty || !needsYou.isEmpty else { return nil }

        let active = live
            .filter { $0.status == .running || $0.status == .provisioning || sessionNeedsYou($0) }
            .sorted { lhs, rhs in
                let l = rank(lhs), r = rank(rhs)
                return l == r ? lhs.updatedAt > rhs.updatedAt : l < r
            }

        return LiveActivitySnapshot(
            runningCount: running.count,
            needsYouCount: needsYou.count,
            approvalCount: active.reduce(0) { $0 + $1.pendingApprovalCount },
            filesChanged: active.reduce(0) { $0 + ($1.diffStat?.filesChanged ?? 0) },
            linesAdded: active.reduce(0) { $0 + ($1.diffStat?.added ?? 0) },
            linesRemoved: active.reduce(0) { $0 + ($1.diffStat?.removed ?? 0) },
            lines: active.prefix(maxLines).map(line(for:)))
    }

    /// The terminal "all clear / Done" glance: the sessions that just finished, shown as `.done`
    /// rows with the counts zeroed. `build` returns nil once nothing is running or waiting; the host
    /// pushes this as an ordinary update to *hold* on an away phone's lock screen until the user opens
    /// the app and sees the finished sessions (`SyncHost.performLiveActivityPush`), so a completed run
    /// reads as finished and stays put rather than vanishing the instant it ends. Mirrors the phone's
    /// `LiveActivityManager.finishWithDoneGlance`. Nil when nothing completed is left to show (e.g.
    /// the work was discarded or archived), in which case the host ends the glance immediately.
    public static func done(from sessions: [SessionSummary]) -> LiveActivitySnapshot? {
        let done = sessions
            .filter { !$0.archived }
            .filter { $0.status == .finished || ($0.status == .awaitingInput && $0.disposition == .completed) }
            .sorted { lhs, rhs in
                let l = rank(lhs), r = rank(rhs)
                return l == r ? lhs.updatedAt > rhs.updatedAt : l < r
            }
            .prefix(maxLines)
            .map(line(for:))
        guard !done.isEmpty else { return nil }
        return LiveActivitySnapshot(
            runningCount: 0, needsYouCount: 0, approvalCount: 0,
            filesChanged: 0, linesAdded: 0, linesRemoved: 0, lines: Array(done))
    }

    /// "Needs you" vs background work — mirrors the phone's `SessionStatus.needsYou(_:)`.
    private static func sessionNeedsYou(_ s: SessionSummary) -> Bool {
        switch s.status {
        case .awaitingApproval: return true
        case .awaitingInput: return s.disposition != .completed
        default: return false
        }
    }

    /// Attention-first ordering — mirrors the phone's `StatusStyle.sortRank`.
    private static func rank(_ s: SessionSummary) -> Int {
        switch s.status {
        case .awaitingApproval: return 0
        case .awaitingInput: return s.disposition == .completed ? 4 : 1
        case .running, .provisioning: return 2
        case .idle: return 3
        case .finished, .interrupted, .error: return 5
        }
    }

    private static func line(for s: SessionSummary) -> Line {
        // Carry the approval id/risk only for a session awaiting a *tool approval* — the row the
        // glance renders inline Allow/Deny on. An `AskUserQuestion` block (pendingQuestionCount set)
        // is excluded: it needs an answer selection the glance can't collect, so it deep-links to the
        // app's picker card, exactly like the notification path. Other states leave it nil too.
        let approvalID = (s.status == .awaitingApproval && s.pendingQuestionCount == nil)
            ? s.firstApprovalID?.rawValue : nil
        return Line(
            id: s.sessionID.rawValue,
            title: s.title.isEmpty ? s.projectName : s.title,
            project: s.projectName,
            backend: backendTag(s.backend),
            kind: kind(for: s),
            detail: detail(for: s),
            approvalID: approvalID,
            approvalIsHighRisk: approvalID == nil ? nil : s.firstApprovalIsHighRisk)
    }

    private static func kind(for s: SessionSummary) -> String {
        switch s.status {
        case .awaitingApproval: return "approval"
        case .running: return "running"
        case .provisioning: return "provisioning"
        case .awaitingInput: return s.disposition == .completed ? "done" : "needsInput"
        case .idle: return "idle"
        case .finished: return "done"
        case .interrupted, .error: return "error"
        }
    }

    private static func backendTag(_ id: BackendID) -> String {
        switch id {
        case .claudeCode: return "claude"
        case .codex, .codexExec: return "codex"
        case .grok: return "grok"
        }
    }

    private static func detail(for s: SessionSummary) -> String {
        if s.status == .awaitingApproval || s.pendingApprovalCount > 0 {
            let n = max(s.pendingApprovalCount, 1)
            return "\(n) to approve"
        }
        if s.status == .awaitingInput, s.disposition != .completed {
            return "Waiting on you"
        }
        if let d = s.diffStat, d.filesChanged > 0 {
            return "\(d.filesChanged) file\(d.filesChanged == 1 ? "" : "s")  +\(d.added) −\(d.removed)"
        }
        switch s.status {
        case .provisioning: return "Starting…"
        case .running: return "Working…"
        case .awaitingInput: return "Done"
        default: return s.status.displayName
        }
    }
}
