import { assert } from "../../../core/assert.js";
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
import { binarySearchHighIndex } from "../../../core/collection/array/binarySearchHighIndex.js";
import { fastArrayEquals } from "../../../core/collection/array/fastArrayEquals.js";
import Signal from "../../../core/events/signal/Signal.js";
import { number_compare_ascending } from "../../../core/primitives/numbers/number_compare_ascending.js";
import { Replicator } from "../replication/Replicator.js";
import { AlwaysRelevantScope } from "../replication/ScopeFilter.js";
import { RewindEngine } from "../sim/RewindEngine.js";
import { NetworkPeer } from "./NetworkPeer.js";

/**
 * Server-authoritative orchestrator with deterministic per-tick rollback.
 *
 * Encapsulates the algorithm previously inlined in
 * `app/src/prototype/network_prototype_prediction.js`:
 *
 *   1. Inbound action packets do NOT execute on arrival. Instead, a deferral
 *      hook on the {@link Replicator} drops them into a per-tick pending log
 *      tagged with `(client_frame, sender_id, type_id, payload_bytes)`.
 *   2. Once per server tick, `tick(current_frame)` consumes the pending log:
 *      - Rejects entries older than the action_log's rewindable window.
 *      - Determines the oldest pending frame `replay_start`.
 *      - Rewinds the server world back to end-of-(replay_start - 1) via the
 *        action_log's prior-state captures (see {@link RewindEngine}).
 *      - Replays forward `[replay_start, current_frame]`. For each frame `f`:
 *        - Read historical actions out of `action_log[f]` BEFORE
 *          `begin_frame(f)` recycles the buffer.
 *        - Append pending entries for `f`.
 *        - Stable-sort the merged list by `sender_id` (so multi-client order
 *          is deterministic across peers — relies on per-record sender_id in
 *          the action log).
 *        - `executor.execute` each in sorted order, then run the user-
 *          supplied local sim via {@link onLocalSim}.
 *      - Clear pending.
 *
 * Net effect: a client action tagged at client tick K lands on the server as
 * if applied against end-of-K-1 server state, regardless of arrival timing
 * or order. Both peers compute identical world states for identical inputs.
 *
 * The user wires this orchestrator up by:
 *   1. Constructing with a world, replication setup, and (typically) an
 *      {@link OwnerAwareScope} filter so client-owned-entity actions are
 *      not echoed back.
 *   2. Calling {@link connect_peer} for each connected client.
 *   3. Subscribing to {@link onLocalSim} for per-frame server-side game logic
 *      (e.g. clamps, physics, collision) and {@link onTickComplete} for
 *      end-of-tick work (e.g. sending AUTH_STATE).
 *   4. Calling {@link tick} once per server frame.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class ServerAuthoritativeServer {
    #pending_frames;
    #pending_senders;
    #pending_type_ids;
    #pending_payloads;
    #pending_referenced_frames;
    #peer_max_received_frame;
    #historical_scratch;
    #sort_scratch;
    #payload_buf;


    /**
     * @param {{
     *   world: EntityComponentDataset,
     *   binary_registry: BinarySerializationRegistry,
     *   replicated_components: Function[],
     *   action_classes: Function[],
     *   scope_filter?: object,
     *   frame_capacity?: number,
     *   initial_buffer_size?: number,
     *   simulation_delay_ticks?: number,
     * }} options
     *
     * `simulation_delay_ticks`: server-side input buffer (Overwatch-style).
     * When > 0, {@link tick}(wall_frame) simulates
     * `sim_frame = wall_frame - simulation_delay_ticks`. Pending actions
     * tagged for `> sim_frame` stay queued and apply when `sim_frame`
     * catches up — most client actions land before the server needs
     * them, so rollback rarely fires. Costs N ticks of perceived
     * input-to-feedback latency (client prediction hides it). Default 0
     * reproduces pre-feature behavior.
     */
    constructor({
        world,
        binary_registry,
        replicated_components,
        action_classes,
        scope_filter = new AlwaysRelevantScope(),
        frame_capacity = 32,
        initial_buffer_size = 1024,
        simulation_delay_ticks = 0,
    }) {
        assert.isNonNegativeInteger(simulation_delay_ticks, 'simulation_delay_ticks');
        /** @type {NetworkPeer} */
        this.peer = new NetworkPeer({
            world,
            binary_registry,
            replicated_components,
            action_classes,
            scope_filter,
            frame_capacity,
            initial_buffer_size,
        });

        /** @type {EntityComponentDataset} */
        this.world = world;
        /** @type {SimActionExecutor} */
        this.executor = this.peer.executor;
        /** @type {ReplicationSlotTable} */
        this.slot_table = this.peer.slot_table;
        /** @type {ActionLog} */
        this.action_log = this.peer.action_log;
        /** @type {SimActionRegistry} */
        this.action_registry = this.peer.action_registry;
        /** @type {ReplicatedComponentRegistry} */
        this.component_registry = this.peer.component_registry;

        /** @type {RewindEngine} */
        this.rewind_engine = new RewindEngine({
            action_log: this.action_log,
            world: this.world,
            component_registry: this.component_registry,
        });

        /**
         * Highest client-tagged frame received (across all peers).
         * RECEIPT semantic — under `simulation_delay_ticks > 0` this
         * may be ahead of {@link current_sim_frame}. Tag AUTH_STATE
         * with `current_sim_frame`, not this.
         * @type {number}
         */
        this.last_client_frame_processed = -1;

        /**
         * Number of ticks the server's simulation lags behind the caller's
         * wall frame. See constructor doc.
         * @type {number}
         */
        this.simulation_delay_ticks = simulation_delay_ticks;

        /**
         * Most recently simulated frame. `-1` during warmup
         * (`wall_frame < simulation_delay_ticks`), advances by 1 per
         * `tick()` after.
         * @type {number}
         */
        this.current_sim_frame = -1;

        // Per-tick pending input log. Parallel arrays (no per-action object
        // alloc) of newly-arrived action records, plus a sorted unique set
        // of the frames they reference.
        this.#pending_frames    = /** @type {number[]} */ ([]);
        this.#pending_senders   = /** @type {number[]} */ ([]);
        this.#pending_type_ids  = /** @type {number[]} */ ([]);
        this.#pending_payloads  = /** @type {Uint8Array[]} */ ([]);
        this.#pending_referenced_frames = /** @type {number[]} */ ([]);

        /**
         * Running watermark per peer: highest client-tagged frame received
         * from each peer. {@link buffer_depth_for_peer} reads this minus
         * {@link current_sim_frame} to drive time-dilation feedback.
         * @private @type {Map<number, number>}
         */
        this.#peer_max_received_frame = new Map();

        // Scratch for the replay loop.
        this.#historical_scratch = /** @type {Array<{type_id:number, sender_id:number, payload:Uint8Array}>} */ ([]);
        this.#sort_scratch = /** @type {Array<{sender_id:number, type_id:number, payload:Uint8Array}>} */ ([]);
        this.#payload_buf = new BinaryBuffer();
        this.#payload_buf.setCapacity(64);

        // Switch the Replicator to deferred-execute mode: incoming actions
        // accumulate in pending until tick() drains them.
        this.peer.replicator.on_pending_action = (peer_id, frame_number, type_id, buf, off, len) => {
            const payload = new Uint8Array(len);
            payload.set(buf.raw_bytes.subarray(off, off + len));
            this.#pending_add(frame_number, peer_id, type_id, payload);
            if (frame_number > this.last_client_frame_processed) {
                this.last_client_frame_processed = frame_number;
            }
            const prev = this.#peer_max_received_frame.get(peer_id);
            if (prev === undefined || frame_number > prev) {
                this.#peer_max_received_frame.set(peer_id, frame_number);
            }
        };

        /**
         * Fires once per frame during the replay loop, AFTER all merged
         * actions for that frame have been applied and BEFORE the action_log
         * frame is closed. Use this for server-authoritative local sim
         * (clamps, collision, AI step, etc.). The current world state
         * reflects all inputs for this frame; the local sim should be
         * idempotent under repeated application (because it may run many
         * times under rollback).
         *
         * Args: `(frame_number)`.
         * @type {Signal}
         */
        this.onLocalSim = new Signal();

        /**
         * Fires after `tick()` completes (pending drained, all frames in the
         * replay window applied). Use for end-of-tick work: sending
         * AUTH_STATE, recording diagnostics, etc.
         *
         * Args: `(current_frame)`.
         * @type {Signal}
         */
        this.onTickComplete = new Signal();

        /**
         * Fired when a tick triggers an actual rewind (i.e. the replay
         * window includes already-committed frames). Args:
         *   `(committed_top, replay_target, depth)` where depth is
         *   `committed_top - replay_target` (number of frames undone).
         *
         * Diagnostic: counts rollbacks and characterizes how deep they
         * went, which directly maps to how late client actions arrived.
         * Tick paths that ran without a rewind (steady-state, no pending
         * for past frames) do NOT fire this.
         * @type {Signal}
         */
        this.onRewind = new Signal();
    }

    /**
     * Connect a client peer over a transport. Subsequent action packets from
     * this peer will be buffered in the pending log via the deferral hook
     * and consumed on the next `tick()`.
     *
     * @param {number} peer_id
     * @param {object} transport
     */
    connect_peer(peer_id, transport) {
        this.peer.connect_peer(peer_id, transport);
    }

    /**
     * Disconnect a previously-connected peer.
     * @param {number} peer_id
     */
    disconnect_peer(peer_id) {
        this.peer.disconnect_peer(peer_id);
    }

    /**
     * Forward to {@link NetworkPeer#send_auth_state}. Typically called from
     * an {@link onTickComplete} subscriber.
     *
     * @param {number} peer_id
     * @param {number} frame_number
     * @param {number} network_id
     * @param {(buf: BinaryBuffer) => void} write_fn
     */
    send_auth_state(peer_id, frame_number, network_id, write_fn) {
        return this.peer.send_auth_state(peer_id, frame_number, network_id, write_fn);
    }

    /**
     * Channel accessor for the user's manual packet plumbing (e.g. sending
     * empty ACK packets in the prototype). Most callers won't need this.
     * @param {number} peer_id
     */
    channel_for(peer_id) {
        return this.peer.channel_for(peer_id);
    }

    /**
     * Send the current buffer-depth observation for one peer to that peer.
     * Convenience wrapper around {@link NetworkPeer#send_time_dilation_feedback}
     * that pulls the depth from {@link buffer_depth_for_peer}. Typically
     * invoked once per tick (or every few ticks) from an
     * {@link onTickComplete} subscriber.
     *
     * @param {number} peer_id
     */
    send_time_dilation_feedback(peer_id) {
        this.peer.send_time_dilation_feedback(peer_id, this.buffer_depth_for_peer(peer_id));
    }

    /**
     * Drive the rollback flow for one tick.
     *
     * With `simulation_delay_ticks = 0` (default), simulates the caller's
     * `wall_frame` and drains pending. With `> 0`, simulates
     * `sim_frame = wall_frame - simulation_delay_ticks` and holds
     * `frame > sim_frame` pending until the sim catches up. Warmup
     * (`sim_frame < 0`) is a no-op; `onTickComplete` does not fire.
     *
     * `onTickComplete` fires with `sim_frame` (not `wall_frame`).
     *
     * @param {number} current_frame the caller's wall frame
     */
    tick(current_frame) {
        assert.isNonNegativeInteger(current_frame, 'current_frame');

        const sim_frame = current_frame - this.simulation_delay_ticks;

        if (sim_frame < 0) return; // warmup

        // 1. Drop pending entries outside the action_log's rewindable window.
        const window_oldest = Math.max(0, sim_frame - this.action_log.frame_capacity + 1);
        if (this.#pending_frames.length > 0 && this.#pending_referenced_frames[0] < window_oldest) {
            this.#pending_trim_to_window(window_oldest);
        }

        // 2. Pick the replay window. `__pending_referenced_frames` is
        //    sorted ascending; entries `> sim_frame` stay buffered.
        let replay_start = sim_frame;
        const refs = this.#pending_referenced_frames;
        if (refs.length > 0 && refs[0] <= sim_frame) {
            replay_start = refs[0];
        }

        // 3. Rewind only if the replay window covers committed frames.
        const committed_top = sim_frame - 1;
        if (replay_start <= committed_top) {
            this.rewind_engine.rewind_to(committed_top, replay_start - 1);
            this.onRewind.send3(committed_top, replay_start - 1, committed_top - (replay_start - 1));
        }

        // 4. Replay [replay_start, sim_frame].
        for (let f = replay_start; f <= sim_frame; f++) {
            this.#replay_frame(f);
        }

        // 5. Keep only `frame > sim_frame` pending (future buffer).
        this.#pending_filter_to_future(sim_frame);

        // 6. Publish sim_frame before flushing, so onTickComplete handlers
        //    reading current_sim_frame see the just-advanced value.
        this.current_sim_frame = sim_frame;
        this.peer.flush_outbound(sim_frame);
        this.onTickComplete.send1(sim_frame);
    }

    /**
     * Current input-buffer depth for a peer: `max_received_frame - current_sim_frame`.
     * Positive ⇒ peer is sending ahead of sim (good); zero/negative ⇒
     * peer is falling behind. Returns 0 if no inputs from this peer.
     * During warmup returns the count of received frames.
     *
     * @param {number} peer_id
     * @returns {number}
     */
    buffer_depth_for_peer(peer_id) {
        const max = this.#peer_max_received_frame.get(peer_id);
        if (max === undefined) return 0;
        if (this.current_sim_frame < 0) return max + 1;
        return max - this.current_sim_frame;
    }

    /** @private */
    #replay_frame(f) {
        this.#sort_scratch.length = 0;

        // Read historical actions out of action_log[f] BEFORE begin_frame(f)
        // recycles the buffer. The sender_id is recorded per-record (see
        // SimActionExecutor), so the stable sort below produces deterministic
        // order regardless of how history was originally interleaved.
        this.#read_historical(f, this.#historical_scratch);
        for (let i = 0; i < this.#historical_scratch.length; i++) {
            const h = this.#historical_scratch[i];
            this.#sort_scratch.push({ sender_id: h.sender_id, type_id: h.type_id, payload: h.payload });
        }

        // Append newly-arrived pending actions for this frame, deduplicating
        // against historical entries. Retransmissions are routine on the
        // wire — `Replicator.pack_for_peer` packs every frame in
        // [last_acked + 1, current_frame] each tick, so an action lives in
        // every outgoing packet for the round-trip duration of its ack.
        // The server receives each retransmission as a fresh pending entry;
        // without dedup, the rollback flow merges it with the historical
        // record left over from the previous apply and executes the action
        // twice (and three times, four times… until the ack arrives).
        //
        // Dedup key: (sender_id, type_id, payload bytes). Two actions are
        // "the same arrival" if all three match. (Same sender_id and
        // type_id but different payload = legitimately distinct actions
        // for the same frame, e.g. fire + move in the same tick.)
        for (let i = 0; i < this.#pending_frames.length; i++) {
            if (this.#pending_frames[i] !== f) continue;
            const p_sender = this.#pending_senders[i];
            const p_type   = this.#pending_type_ids[i];
            const p_payload = this.#pending_payloads[i];
            let is_dup = false;
            for (let j = 0; j < this.#historical_scratch.length; j++) {
                const h = this.#historical_scratch[j];
                if (h.sender_id !== p_sender) continue;
                if (h.type_id !== p_type) continue;
                if (fastArrayEquals(h.payload, p_payload)) { is_dup = true; break; }
            }
            if (is_dup) continue;
            this.#sort_scratch.push({ sender_id: p_sender, type_id: p_type, payload: p_payload });
        }

        // Stable sort by sender. Array.prototype.sort is stable since ES2019;
        // equal sender_ids preserve insertion order, with historical entries
        // appearing before newly-arrived pending entries for the same sender.
        this.#sort_scratch.sort((a, b) => a.sender_id - b.sender_id);

        // Open the frame, execute all, close.
        this.action_log.begin_frame(f);
        for (let i = 0; i < this.#sort_scratch.length; i++) {
            const s = this.#sort_scratch[i];
            this.#apply_payload(s.type_id, s.sender_id, s.payload);
        }
        // Notify user code so server-side local sim runs at the right point
        // in the frame (after inputs applied, before frame closed). Idempotent
        // under repeated application across rollback replays.
        this.onLocalSim.send1(f);
        this.action_log.end_frame();
    }

    /** @private */
    #apply_payload(type_id, sender_id, payload) {
        const klass = this.action_registry.klass_for(type_id);
        if (klass === undefined) return;
        const action = this.action_registry.acquire(klass);
        try {
            if (this.#payload_buf.raw_bytes.length < payload.length) {
                this.#payload_buf.setCapacity(payload.length);
            }
            this.#payload_buf.position = 0;
            this.#payload_buf.writeBytes(payload, 0, payload.length);
            this.#payload_buf.position = 0;
            action.deserialize(this.#payload_buf);
            this.executor.execute(action, sender_id);
        } finally {
            this.action_registry.release(action);
        }
    }

    /** @private */
    #read_historical(frame_number, out_array) {
        out_array.length = 0;
        if (!this.action_log.has_frame(frame_number)) return;
        const buf = this.action_log.buffer_for(frame_number); // sets position = 0
        const end = this.action_log.write_end_for(frame_number);
        while (buf.position < end) {
            const prior_count = buf.readUintVar();
            for (let i = 0; i < prior_count; i++) {
                buf.readUintVar();             // entity_id
                buf.readUint8();               // component_type_id
                const plen = buf.readUint32();
                buf.position += plen;
            }
            const type_id = buf.readUint8();
            const sender_id = buf.readUint8();
            const payload_len = buf.readUint32();
            const payload = new Uint8Array(payload_len);
            payload.set(buf.raw_bytes.subarray(buf.position, buf.position + payload_len));
            buf.position += payload_len;
            out_array.push({ type_id, sender_id, payload });
        }
    }

    /** @private */
    #pending_add(frame_number, sender_id, type_id, payload) {
        // Dedup against other pending entries for the same frame. Multiple
        // retransmission packets routinely arrive in the same c2s.tick
        // window when ack RTT exceeds tick period; each carries the same
        // action records for the same frames. The __replay_frame dedup
        // catches retransmissions across server ticks (where historical
        // already has the record), but the SAME server tick can collect
        // multiple identical pending entries before historical exists. So
        // also dedup here, against everything already in pending for this
        // frame. Match on (sender_id, type_id, payload bytes) — distinct
        // payloads from the same sender at the same frame are legitimately
        // different actions (move + fire) and both flow through.
        for (let i = 0; i < this.#pending_frames.length; i++) {
            if (this.#pending_frames[i] !== frame_number) continue;
            if (this.#pending_senders[i] !== sender_id) continue;
            if (this.#pending_type_ids[i] !== type_id) continue;
            if (fastArrayEquals(this.#pending_payloads[i], payload)) return; // duplicate already in pending; drop
        }

        this.#pending_frames.push(frame_number);
        this.#pending_senders.push(sender_id);
        this.#pending_type_ids.push(type_id);
        this.#pending_payloads.push(payload);
        this.#track_referenced_frame(frame_number);
    }

    /**
     * Insert `frame_number` into the monotonically-sorted referenced-frames
     * index, leaving it untouched if already present. Single source of truth
     * for the three sites (per-add, post-trim, post-filter) that maintain
     * this index, so the binary-search invariant and uniqueness check live
     * in one place.
     * @param {number} frame_number
     * @private
     */
    #track_referenced_frame(frame_number) {
        const arr = this.#pending_referenced_frames;
        const i = binarySearchHighIndex(arr, frame_number, number_compare_ascending);
        if (arr[i] !== frame_number) arr.splice(i, 0, frame_number);
    }

    /** @private */
    #pending_trim_to_window(window_oldest) {
        let kept = 0;
        for (let i = 0; i < this.#pending_frames.length; i++) {
            if (this.#pending_frames[i] >= window_oldest) {
                this.#pending_frames[kept]   = this.#pending_frames[i];
                this.#pending_senders[kept]  = this.#pending_senders[i];
                this.#pending_type_ids[kept] = this.#pending_type_ids[i];
                this.#pending_payloads[kept] = this.#pending_payloads[i];
                kept++;
            }
        }
        this.#pending_frames.length   = kept;
        this.#pending_senders.length  = kept;
        this.#pending_type_ids.length = kept;
        this.#pending_payloads.length = kept;
        this.#pending_referenced_frames.length = 0;
        for (const f of this.#pending_frames) {
            this.#track_referenced_frame(f);
        }
    }

    /** @private */
    #pending_clear() {
        this.#pending_frames.length = 0;
        this.#pending_senders.length = 0;
        this.#pending_type_ids.length = 0;
        this.#pending_payloads.length = 0;
        this.#pending_referenced_frames.length = 0;
    }

    /**
     * Drop pending entries with `frame <= keep_threshold`, keeping the
     * rest. Same structural pattern as {@link __pending_trim_to_window}
     * but with the inequality flipped — used at the end of {@link tick}
     * to retain future-tagged inputs under `simulation_delay_ticks > 0`.
     * @private
     */
    #pending_filter_to_future(keep_threshold) {
        let kept = 0;
        for (let i = 0; i < this.#pending_frames.length; i++) {
            if (this.#pending_frames[i] > keep_threshold) {
                this.#pending_frames[kept]   = this.#pending_frames[i];
                this.#pending_senders[kept]  = this.#pending_senders[i];
                this.#pending_type_ids[kept] = this.#pending_type_ids[i];
                this.#pending_payloads[kept] = this.#pending_payloads[i];
                kept++;
            }
        }
        this.#pending_frames.length   = kept;
        this.#pending_senders.length  = kept;
        this.#pending_type_ids.length = kept;
        this.#pending_payloads.length = kept;
        this.#pending_referenced_frames.length = 0;
        for (const f of this.#pending_frames) {
            this.#track_referenced_frame(f);
        }
    }
}
