package wang.yaojia.webterm.session

import wang.yaojia.webterm.wire.GateKind

/**
 * Pure reducer that folds `status` frames' `(pending, gate, detail)` into the
 * current [GateState]? (A6; ports iOS `SessionCore.GateTracker`). 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 == null` while pending → treated as a tool gate, because an
 *   unrecognized gate value decodes as null ([wang.yaojia.webterm.wire.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, A14) feeds
 * frames in and emits a gate change event.
 */
public class GateTracker private constructor(
    /** The gate currently held server-side, or `null` when none. */
    public val current: GateState?,
    /**
     * Highest epoch ever issued; NOT reset on falling edges so decisions against a
     * resolved gate can never match a later one.
     */
    private val lastEpoch: Int,
) {
    /** Feeds one `status` frame's gate-relevant fields; returns the next snapshot. */
    public fun reduce(pending: Boolean, gate: GateKind?, detail: String?): GateTracker {
        if (!pending) {
            // Falling edge (or still idle): gate lifted, epoch counter retained.
            return GateTracker(current = null, lastEpoch = lastEpoch)
        }
        val kind = gate ?: GateKind.TOOL
        val held = current
        if (held != null) {
            // Sustained pending: same epoch, refresh kind/detail from the frame.
            val 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).
        val nextEpoch = lastEpoch + 1
        val 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 — the two-line stale-guard).
     */
    public fun canDecide(epoch: Int): Boolean {
        val held = current ?: return false
        return held.epoch == epoch
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is GateTracker) return false
        return current == other.current && lastEpoch == other.lastEpoch
    }

    override fun hashCode(): Int = 31 * (current?.hashCode() ?: 0) + lastEpoch

    override fun toString(): String = "GateTracker(current=$current, lastEpoch=$lastEpoch)"

    public companion object {
        /** The starting tracker: no gate held, epoch counter at 0. */
        public val INITIAL: GateTracker = GateTracker(current = null, lastEpoch = 0)
    }
}
