import WireProtocol

/// One held permission gate as the UI should render it (T-iOS-6, plan §3.2 —
/// frozen shape). Produced by `GateTracker` from `status` frames; `nil` gate
/// (via `SessionEvent.gate(nil)`) means "gate lifted".
///
/// `epoch` is the stale-decision guard (mirrors the web client's
/// `pendingEpochValue`, public/terminal-session.ts:98-102): it increments only
/// on a pending false→true rising edge, and every approve/reject carries the
/// epoch it was tapped against — a stale epoch is dropped so a slow tap can
/// never approve the NEXT gate (防误批新 gate).
public struct GateState: Sendable, Equatable {
    /// `plan` → three-way affordances; `tool` → two-way (public/tabs.ts:334-350).
    public let kind: GateKind
    /// Server-supplied context (tool name for tool gates); untrusted display text.
    public let detail: String?
    /// Rising-edge counter; see type doc.
    public let epoch: Int

    public init(kind: GateKind, detail: String?, epoch: Int) {
        self.kind = kind
        self.detail = detail
        self.epoch = epoch
    }
}

extension GateState {
    /// One user-facing gate action. Pure affordance DATA — display strings live
    /// in the App layer; the wire mapping mirrors public/tabs.ts:334-350.
    public enum Affordance: Sendable, Equatable {
        /// Plan gate: "Approve + Auto" → `approve(mode: .acceptEdits)`.
        case approveAuto
        /// Plan gate: "Approve + Review" → `approve(mode: .default)`.
        case approveReview
        /// Plan gate: "Keep Planning" → `reject` (stays in plan mode).
        case keepPlanning
        /// Tool gate: "Approve" → `approve(mode: nil)`.
        case approve
        /// Tool gate: "Reject" → `reject`.
        case reject

        /// The exact wire message the web client sends for this affordance
        /// (public/tabs.ts:345-347: plan three-way only ever sends
        /// acceptEdits / default / reject — never raw `auto`, plan §3.1 note).
        public var clientMessage: ClientMessage {
            switch self {
            case .approveAuto: return .approve(mode: .acceptEdits)
            case .approveReview: return .approve(mode: .default)
            case .keepPlanning: return .reject
            case .approve: return .approve(mode: nil)
            case .reject: return .reject
            }
        }
    }

    /// The action set for this gate: plan → three-way, tool → two-way.
    public var affordances: [Affordance] {
        switch kind {
        case .plan: return [.approveAuto, .approveReview, .keepPlanning]
        case .tool: return [.approve, .reject]
        }
    }
}

/// Pure reducer that folds `status` frames' `(pending, gate, detail)` into the
/// current `GateState?` (T-iOS-6). Mirrors public/terminal-session.ts:306-311:
///
/// - pending false→true rising edge → epoch +1, new gate held;
/// - sustained pending → SAME epoch, kind/detail refresh from the latest frame
///   (the server is the source of truth; reattach re-sync stays correct);
/// - pending true→false falling edge → gate cleared, epoch counter retained;
/// - `gate == nil` while pending → treated as a tool gate, because an
///   unrecognized gate value decodes as nil (ServerMessage) and the pending
///   signal must never be lost (web: `gate ?? null` → tool buttons).
///
/// PURE state machine: `reduce` never mutates the receiver — it returns a new
/// immutable snapshot. No I/O, no clock; the caller (SessionEngine, T-iOS-10)
/// feeds frames in and emits `SessionEvent.gate(current)` on change.
public struct GateTracker: Sendable, Equatable {
    public static let initial = GateTracker(current: nil, lastEpoch: 0)

    /// The gate currently held server-side, or `nil` when none.
    public let current: GateState?

    /// Highest epoch ever issued; NOT reset on falling edges so decisions
    /// against a resolved gate can never match a later one.
    private let lastEpoch: Int

    private init(current: GateState?, lastEpoch: Int) {
        self.current = current
        self.lastEpoch = lastEpoch
    }

    /// Feeds one `status` frame's gate-relevant fields; returns the next snapshot.
    public func reduce(pending: Bool, gate: GateKind?, detail: String?) -> GateTracker {
        guard pending else {
            // Falling edge (or still idle): gate lifted, epoch counter retained.
            return GateTracker(current: nil, lastEpoch: lastEpoch)
        }
        let kind = gate ?? .tool
        if let held = current {
            // Sustained pending: same epoch, refresh kind/detail from the frame.
            let refreshed = GateState(kind: kind, detail: detail, epoch: held.epoch)
            return GateTracker(current: refreshed, lastEpoch: lastEpoch)
        }
        // Rising edge: mint the next epoch (the ONLY place it increments).
        let nextEpoch = lastEpoch + 1
        let risen = GateState(kind: kind, detail: detail, epoch: nextEpoch)
        return GateTracker(current: risen, lastEpoch: nextEpoch)
    }

    /// True iff a decision tapped against `epoch` may be sent NOW: a gate must
    /// be held and its epoch must match. Stale epoch or no gate → drop the
    /// decision (防误批新 gate).
    public func canDecide(epoch: Int) -> Bool {
        guard let held = current else { return false }
        return held.epoch == epoch
    }
}
