import { assert } from "../../../../core/assert.js";
import Signal from "../../../../core/events/signal/Signal.js";
import { Transport } from "../Transport.js";

/**
 * Transport adapter over a browser `WebTransport` connection, using the
 * unreliable/unordered **datagram** channel — the moral equivalent of
 * UDP, exposed over HTTP/3.
 *
 * Properties:
 *   - Authenticated by TLS (https:// origin required by the API).
 *   - No head-of-line blocking, unlike WebSocket-over-TCP.
 *   - Datagrams have a per-implementation MTU (typically 1200 bytes
 *     after framing); larger payloads should go through the engine's
 *     fragment-send machinery as with any UDP-style transport.
 *
 * For reliable / ordered traffic (chat, room state, file transfer)
 * the same `WebTransport` exposes bidirectional streams. This adapter
 * does not surface them; build a separate stream-mode transport if
 * needed, or use {@link WebSocketTransport} for that traffic.
 *
 * The `WebTransport` global is browser-only and requires a relatively
 * recent runtime (Chromium 97+, Firefox 114+). Server-side, an HTTP/3
 * server is required (e.g. `quiche-py`, `aioquic`, or a dedicated
 * WebTransport library — out of scope for the engine).
 *
 * Either pass a pre-constructed `wt` instance (useful for tests via
 * a mock) or pass `url` + optional `options` and let the adapter
 * construct it.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class WebTransportTransport extends Transport {
    #stats;
    #writer;
    #reader;
    #read_loop_running;
    #close_listened;

    /**
     * @param {{ wt?: object, url?: string, options?: object }} params
     */
    constructor({ wt, url, options } = {}) {
        super();
        if (wt === undefined && typeof url !== "string") {
            throw new Error("WebTransportTransport: either `wt` or `url` is required");
        }

        // Datagram channel: UDP-like — neither delivery nor ordering
        // are guaranteed. Inherits the base's defaults; stated here
        // for clarity, and so a future stream-mode subclass can flip
        // them without ambiguity.
        this.reliable = false;
        this.ordered = false;

        /**
         * The underlying `WebTransport` instance. Exposed for callers
         * that need to access stream-mode APIs or attach lifecycle
         * listeners (`.closed`, `.draining`).
         * @type {object}
         */
        this.wt = wt !== undefined ? wt : new WebTransport(url, options);

        assert.ok(this.wt.datagrams !== undefined,
            'WebTransport instance must expose .datagrams (browser support / version)');

        /** @private */
        this.#stats = { bytes_in: 0, bytes_out: 0, packets_in: 0, packets_out: 0 };

        /** @private */
        this.#writer = null;
        /** @private */
        this.#reader = null;
        /** @private */
        this.#read_loop_running = false;
        /** @private */
        this.#close_listened = false;

        /**
         * Fires when `writer.write()` rejects but the underlying
         * connection is still open — typically transient backpressure
         * (writer queue full / slow consumer). Distinct from
         * `onDisconnect`, which fires only on the actual `wt.closed`
         * settlement (clean close or abrupt error). Callers can
         * subscribe to throttle their send rate, drop optional traffic,
         * or surface a warning.
         *
         * If you don't care, ignore — sends remain fire-and-forget and
         * a dropped packet is no different from a UDP datagram lost on
         * the network.
         *
         * @type {Signal}
         */
        this.onSendDropped = new Signal();
    }

    /**
     * Await `wt.ready`, acquire the datagram writer and reader, and
     * start the receive loop. Idempotent.
     *
     * @returns {Promise<void>}
     */
    async connect() {
        if (this.#writer !== null) return;
        await this.wt.ready;
        this.#writer = this.wt.datagrams.writable.getWriter();
        this.#reader = this.wt.datagrams.readable.getReader();
        this.#start_read_loop();
        this.#listen_for_close();
    }

    /**
     * @param {Uint8Array} bytes
     * @param {number} length
     */
    send(bytes, length) {
        assert.isNonNegativeInteger(length, 'length');
        if (length === 0) return;
        if (this.#writer === null) {
            // Pre-`connect()` writes are dropped. Higher layers should
            // gate sends on the `connect()` promise.
            return;
        }
        const payload = (length === bytes.byteLength) ? bytes : bytes.subarray(0, length);
        // Fire-and-forget at the call site, but distinguish two
        // rejection sources: backpressure / writer-queue-full (transient,
        // recoverable) vs. the transport actually closing (terminal —
        // surfaced via the `.closed` listener below as `onDisconnect`).
        // Backpressure fires `onSendDropped` so callers can throttle.
        this.#writer.write(payload).catch((err) => {
            // If the underlying connection has already entered close,
            // `.closed` will surface the failure as onDisconnect — no
            // need to double-report via onSendDropped.
            if (this.#writer === null) return;
            const reason = err && err.message ? err.message : 'write_rejected';
            this.onSendDropped.send1(reason);
        });
        this.#stats.bytes_out += length;
        this.#stats.packets_out += 1;
    }

    /**
     * Cancel the read loop and close the WebTransport.
     */
    disconnect() {
        if (this.#reader !== null) {
            try { this.#reader.cancel(); } catch (_) { /* swallow */ }
            this.#reader = null;
        }
        if (this.#writer !== null) {
            try { this.#writer.close(); } catch (_) { /* swallow */ }
            this.#writer = null;
        }
        try { this.wt.close(); } catch (_) { /* swallow */ }
    }

    /**
     * @returns {{bytes_in: number, bytes_out: number, packets_in: number, packets_out: number}}
     */
    getStats() {
        return {
            bytes_in: this.#stats.bytes_in,
            bytes_out: this.#stats.bytes_out,
            packets_in: this.#stats.packets_in,
            packets_out: this.#stats.packets_out,
        };
    }

    /** @private */
    #start_read_loop() {
        if (this.#read_loop_running) return;
        this.#read_loop_running = true;
        const pump = async () => {
            try {
                while (this.#reader !== null) {
                    const { value, done } = await this.#reader.read();
                    if (done) break;
                    if (!(value instanceof Uint8Array)) continue;
                    this.#stats.bytes_in += value.length;
                    this.#stats.packets_in += 1;
                    this.onReceive.send2(value, value.length);
                }
            } catch (_) {
                // Stream errored out (e.g. transport closed). The
                // `.closed` listener below dispatches `onDisconnect`.
            } finally {
                this.#read_loop_running = false;
            }
        };
        pump();
    }

    /** @private */
    #listen_for_close() {
        if (this.#close_listened) return;
        this.#close_listened = true;
        // `wt.closed` resolves on clean close, rejects on abrupt close.
        // Either way the transport is dead — surface it via `onDisconnect`.
        this.wt.closed
            .then((info) => {
                const reason = (info && info.reason) ? info.reason : 'closed';
                this.onDisconnect.send1(reason);
            })
            .catch((err) => {
                const reason = err && err.message ? err.message : 'errored';
                this.onDisconnect.send1(reason);
            });
    }
}
