"use strict";

/**
 * AtdtDosMaint - the SysOp maintenance runner (SPEC-DOSDOORS §13).
 *
 * Boots the same FreeDOS machine callers get, on the same live game
 * drive, but pointed the other way: the machine's own text screen and
 * keyboard face the SysOp (v86's native screen adapter in the page,
 * colors and all), and DOOR.BAT holds whatever command they typed.
 * That is how a door's config program or an installer runs against
 * D:\ with no DOSBox in sight, and why the serial line goes almost
 * unused here: config tools talk to the local screen, and the only
 * serial words are the end-of-run sentinels.
 *
 * Everything that knows DOS lives in dosdoor.js (AtdtDosDoor) and is
 * reused whole: DoorFs, the EtherDFS server, the grouped prefetch.
 * Only the transport differs: ops ride the console's own login and
 * CSRF token through admin/dosmaint.php, because a SysOp at the
 * console is not logged into the board itself.
 */
(function () {
    if (typeof document === "undefined" || typeof window === "undefined") { return; }
    var cfg = window.ATDT_MAINT;
    var D = window.AtdtDosDoor;
    if (!cfg || !D) { return; }

    var startBtn = document.getElementById("maint_start");
    var stopBtn = document.getElementById("maint_stop");
    var cmdBox = document.getElementById("maint_cmd");
    var screenEl = document.getElementById("maint_screen");
    var statusEl = document.getElementById("maint_status");

    var status = function (text, warn) {
        statusEl.textContent = text;
        statusEl.className = warn ? "warn" : "hint";
    };

    /** One console ajax call. The payload rides as JSON in 'body' so the
     *  central CSRF check sees a normal form POST. Resolves the parsed
     *  reply, or null on any network or login trouble. The last failed
     *  HTTP status is kept so a dead op can NAME its killer: a 403 is a
     *  security rule, a 413 a body-size cap, a 503 a throttle, and each
     *  points at a different line in the hosting control panel. */
    var lastFail = { status: 0, at: 0 };
    var call = function (which, payload) {
        return fetch("dosmaint.php?ajax=" + which, {
            method: "POST",
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            body: "csrf=" + encodeURIComponent(cfg.csrf)
                + "&body=" + encodeURIComponent(JSON.stringify(payload || {}))
        }).then(function (r) {
            if (!r.ok) { lastFail = { status: r.status, at: Date.now() }; }
            return r.json();
        }).catch(function () { return null; });
    };

    var running = null;

    var start = function () {
        if (running) { return; }
        startBtn.disabled = true;
        stopBtn.disabled = false;
        var dead = false;
        var emulator = null;
        var token = null;
        var fsv = null, srv = null;
        var pingTimer = null, sleepTimer = null;
        var quietSince = Date.now();
        var wakeHandler = null, visHandler = null, unloadHandler = null;
        var navHandler = null;
        var fetchWarning = "";

        /** Everything stops here, exactly once. */
        var teardown = function (why, keepStatus) {
            if (dead) { return; }
            dead = true;
            clearInterval(pingTimer);
            clearInterval(sleepTimer);
            if (wakeHandler) {
                document.removeEventListener("keydown", wakeHandler, true);
                document.removeEventListener("mousedown", wakeHandler, true);
            }
            if (navHandler) {
                document.removeEventListener("keydown", navHandler, true);
                document.removeEventListener("keyup", navHandler, true);
            }
            if (visHandler) { document.removeEventListener("visibilitychange", visHandler); }
            if (unloadHandler) { window.removeEventListener("pagehide", unloadHandler); }
            if (emulator) {
                try { emulator.destroy(); } catch (e) { /* already down */ }
                emulator = null;
            }
            // "gone" means the server already dropped the seat; every
            // other way out returns it politely - after the
            // write-behind queue has landed what the session wrote
            // (a Reset's last records may still be flushing when the
            // SysOp ends the session).
            if (token && why !== "gone") {
                var tok = token;
                var sendClose = function () { call("maint_close", { t: tok }); };
                if (fsv) { fsv.drainTimeout(120000).then(sendClose); } else { sendClose(); }
            }
            token = null;
            running = null;
            startBtn.disabled = false;
            stopBtn.disabled = true;
            if (!keepStatus) {
                status("Session ended. The seat is freed, and the live game keeps every change you made.");
            }
        };
        running = { stop: function () { teardown("stop"); } };

        // ---- 1. the seat ----
        status("Opening the maintenance seat...");
        call("maint_open", { slug: cfg.slug, command: cmdBox.value || "" }).then(function (res) {
            if (dead) { return; }
            if (!res || !res.ok || !res.session) {
                teardown("gone", true);
                status("Could not open the seat"
                    + (res && res.error ? " (" + res.error + ")" : "")
                    + ". Check the SysOp log.", true);
                return;
            }
            var s = res.session;
            token = s.token;
            // The io keeps its own copy: teardown nulls `token` while
            // the write-behind queue may still be flushing against the
            // live seat, right up to the maint_close it delays.
            var seatToken = s.token;
            var manifest = s.manifest || {};

            // ---- 2. the filesystem view, same parts as a caller's door ----
            // Same retry ladder a caller's door climbs: a shared host
            // eating requests, or throttling in bursts that last whole
            // seconds, used to hand the editor a hard DOS error mid
            // Reset for a file that is fine, and a torn data file
            // stayed behind. Ops replay safely, so a failed transport
            // gets about seven seconds of retries before reporting.
            var FS_WAITS = [350, 900, 2200, 4000];
            var failSaid = 0;
            var fsTry = function (body, left) {
                return call("maint_fs", body).then(function (r) {
                    if (r !== null || dead) { return r; }
                    if (left <= 0) {
                        // The op is dead for real; put its obituary on the
                        // status line so the SysOp learns WHAT the host
                        // refused, not just that a program complained.
                        if (Date.now() - failSaid > 8000) {
                            failSaid = Date.now();
                            var what = body.op || (body.ops && body.ops[0] && body.ops[0].op) || "op";
                            var where = body.path || (body.ops && body.ops[0] && body.ops[0].path) || "";
                            status("The host refused a file operation after 5 tries: "
                                + what + " " + where
                                + (lastFail.status && Date.now() - lastFail.at < 60000
                                    ? " (HTTP " + lastFail.status + ")" : " (no HTTP reply)")
                                + ". The program will show an error; note this line for the hosting checklist.", true);
                        }
                        return r;
                    }
                    return new Promise(function (r2) {
                        setTimeout(r2, FS_WAITS[FS_WAITS.length - left] || 900);
                    }).then(function () { return fsTry(body, left - 1); });
                });
            };
            var io = {
                op: function (params) {
                    var body = { t: seatToken };
                    Object.keys(params).forEach(function (k) { body[k] = params[k]; });
                    return fsTry(body, FS_WAITS.length);
                },
                batch: function (ops) { return fsTry({ t: seatToken, ops: ops }, FS_WAITS.length); }
            };
            fsv = new D.DoorFs(io, {
                statics: manifest.static || [],
                sessions: manifest.session || [],
                gen: s.gen
            });
            var enc = function (txt) {
                var b = new Uint8Array(txt.length);
                for (var i = 0; i < txt.length; i++) { b[i] = txt.charCodeAt(i) & 0xFF; }
                return b;
            };
            fsv.plant(s.dropfile.name, enc(s.dropfile.text));
            fsv.plant("DOOR.BAT", enc(s.doorbat));
            srv = new D.EtherSrv(fsv);

            var trips = 0;
            status("Fetching the game files... (a big door can take half a minute)");
            D.prefetchAll(fsv, function () {
                trips++;
                status("Fetching the game files... " + trips
                    + (trips > 3 ? " (a big door can take half a minute)" : ""));
            }).then(function (pf) {
                if (dead) { return; }
                if (pf && pf.failed && pf.failed.length > 0) {
                    fetchWarning = "Part of the game would not transfer ("
                        + pf.failed.slice(0, 3).join(", ")
                        + (pf.failed.length > 3 ? ", ..." : "")
                        + "). A hosting filter may be cutting large replies; programs may not load.";
                }
                boot(s);
            });
        });

        // ---- 3. the machine, screen and keyboard facing the SysOp ----
        var boot = function (s) {
            var V86Ctor = (typeof V86 !== "undefined") ? V86 : window.V86;
            if (!V86Ctor) {
                teardown("gone", true);
                status("The v86 runtime did not load. Reload the page and boot again.", true);
                return;
            }
            if (fetchWarning !== "") {
                status(fetchWarning, true);
            } else {
                status("Booting... Keys typed on this page go to the machine (the command box stays yours).");
            }
            var rtv = "?v=" + (s.rtv || 0);
            emulator = new V86Ctor({
                wasm_path: cfg.base + "client/v86.wasm" + rtv,
                memory_size: 16 * 1024 * 1024,
                vga_memory_size: 2 * 1024 * 1024,
                bios: { url: cfg.base + "client/seabios.bin" + rtv },
                vga_bios: { url: cfg.base + "client/vgabios.bin" + rtv },
                fda: { url: cfg.base + "boot/freedos.img" + rtv },
                boot_order: 0x321,
                autostart: true,
                screen_container: screenEl,
                disable_mouse: true,
                disable_speaker: true,
                // Same per-door "jit": "off" switch the caller road honors:
                // maintenance must run on the same CPU the callers get, or
                // a Turbo Pascal door that needs the interpreter would hang
                // only here. (1.7.4)
                disable_jit: !!(s.manifest && s.manifest.jit === "off"),
                net_device: { type: "ne2k" }
            });

            // The wire: the same EDF5 server a caller's tab runs. Network
            // traffic counts as activity for the idle sleep, exactly as in
            // the door runner: a machine mid-file-operation is busy even
            // when its screen is still.
            emulator.add_listener("net0-send", function (data) {
                quietSince = Date.now();
                srv.handleFrame(data).then(function (reply) {
                    if (reply && emulator) {
                        emulator.bus.send("net0-receive", reply);
                        // A slow reply (retry ladder mid-climb) can land on
                        // a machine the idle sleep already stopped; wake it
                        // so the answer is read. run() is a no-op when the
                        // machine is already running.
                        quietSince = Date.now();
                        try { emulator.run(); } catch (e) { /* coming up */ }
                    }
                });
            });

            // Carrier up: a door run by hand in here still sees a live
            // line. And the hardware clock is shifted once so the DOS
            // date and time are the BOARD's, not UTC (config tools stamp
            // files, and LORD's day is the DOS day).
            var rtcSet = false;
            emulator.add_listener("emulator-started", function () {
                if (!rtcSet) {
                    rtcSet = true;
                    try {
                        emulator.v86.cpu.devices.rtc.rtc_time += (s.tzoffmin || 0) * 60000;
                    } catch (e) { /* an emulator without the knob keeps UTC */ }
                }
                ["serial0-carrier-detect-input", "serial0-data-set-ready-input",
                    "serial0-clear-to-send-input"].forEach(function (ev) {
                    if (emulator) { emulator.bus.send(ev, true); }
                });
            });

            // The batch file's only planned serial words are the sentinels;
            // watch the raw stream for them and translate to plain English.
            var tail = "";
            emulator.add_listener("serial0-output-byte", function (b) {
                quietSince = Date.now();
                tail += String.fromCharCode(b & 0xFF);
                if (tail.length > 64) { tail = tail.slice(-64); }
                if (tail.indexOf("ATDT-DOOR-EOF") !== -1) {
                    tail = "";
                    status("The command finished. The machine is at a DOS prompt on D:\\ now: "
                        + "click the screen and keep typing, or End session.");
                } else if (tail.indexOf("ATDT-DOOR-NOSRV") !== -1) {
                    tail = "";
                    status("The machine could not reach the game drive. End the session and check the SysOp log.", true);
                }
            });

            // The gray arrow and navigation keys reach DOS as E0-prefixed
            // scancodes, and Borland-era config tools (Usurper's editor,
            // and friends) read that prefix as a keystroke of its own: one
            // press of Down moves two menu entries. The numeric keypad's
            // codes carry no prefix and mean the same keys while NumLock
            // is off, which is how the boot image comes up - so the ten
            // navigation keys are sent as their plain codes instead, and
            // v86's own translation is stopped from double-speaking.
            var NAVKEYS = {
                ArrowUp: 0x48, ArrowDown: 0x50, ArrowLeft: 0x4B, ArrowRight: 0x4D,
                Home: 0x47, End: 0x4F, PageUp: 0x49, PageDown: 0x51,
                Insert: 0x52, Delete: 0x53
            };
            navHandler = function (e) {
                if (!emulator || e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) { return; }
                var sc = NAVKEYS[e.code];
                if (!sc) { return; }
                e.preventDefault();
                e.stopPropagation();
                try {
                    emulator.keyboard_send_scancodes([e.type === "keydown" ? sc : sc | 0x80]);
                } catch (err) { /* without the knob the plain adapter path stands */ }
            };
            document.addEventListener("keydown", navHandler, true);
            document.addEventListener("keyup", navHandler, true);

            // ---- the idle sleep (§8.4) ----
            // A machine parked at a DOS prompt polls the keyboard flat out.
            // Same rule as a caller's door: a quiet minute (ten seconds in
            // a hidden tab) stops it whole, and any key or click wakes it
            // with an UNCONDITIONAL run() - never ask "is it running?"
            // first, that race is how a machine stays down for good.
            var wake = function () {
                quietSince = Date.now();
                if (emulator) {
                    try { emulator.run(); } catch (e) { /* already coming up */ }
                }
            };
            wakeHandler = function () { wake(); };
            document.addEventListener("keydown", wakeHandler, true);
            document.addEventListener("mousedown", wakeHandler, true);
            visHandler = function () {
                if (!document.hidden) { wake(); }
            };
            document.addEventListener("visibilitychange", visHandler);
            // Freeze-aware, like the caller runner: a tick arriving
            // long after its schedule means the page thread (and the
            // machine with it) was stalled, not that the door went
            // quiet. Stopping on that tick parked a machine that had
            // just been handed a reply and owed the drive its next
            // query - mid world reset, with no wake left in a hidden
            // tab. Grace and an unconditional run() instead.
            var lastSleepTick = Date.now();
            sleepTimer = setInterval(function () {
                if (!emulator || dead) { return; }
                var now = Date.now();
                var stalled = now - lastSleepTick > 5000;
                lastSleepTick = now;
                if (stalled) {
                    quietSince = now;
                    try { emulator.run(); } catch (e) { /* coming up */ }
                    return;
                }
                var limit = document.hidden ? 10000 : 60000;
                if (now - quietSince > limit) {
                    try { emulator.stop(); } catch (e) { /* already down */ }
                }
            }, 1000);

            // ---- the heartbeat ----
            var misses = 0;
            pingTimer = setInterval(function () {
                call("maint_ping", { t: token }).then(function (r) {
                    if (dead) { return; }
                    if (r === null) {
                        misses++;
                        if (misses >= 3) {
                            teardown("gone", true);
                            status("Lost the console connection, so the machine is stopped. "
                                + "Log in again if needed, reload, and boot again.", true);
                        }
                        return;
                    }
                    misses = 0;
                    if (r.error) {
                        teardown("gone", true);
                        status("The seat was closed (disconnected from the console, or swept). "
                            + "The machine is stopped; boot again to continue.", true);
                        return;
                    }
                    if (typeof r.minutes === "number" && r.minutes <= 0) {
                        teardown("stop", true);
                        status("Eight hours in one maintenance seat is the cap. Boot again to continue.", true);
                        return;
                    }
                    // A Reset or another session moving the generation
                    // counter flushes this machine's file caches too.
                    fsv.sawGen(r.gen, false);
                });
            }, 30000);

            // A closed tab still returns the seat (best effort; the sweep
            // catches whatever the beacon misses).
            unloadHandler = function () {
                if (!token || !navigator.sendBeacon) { return; }
                var fd = new FormData();
                fd.append("csrf", cfg.csrf);
                fd.append("body", JSON.stringify({ t: token }));
                navigator.sendBeacon("dosmaint.php?ajax=maint_close", fd);
            };
            window.addEventListener("pagehide", unloadHandler);
        };
    };

    startBtn.addEventListener("click", start);
    stopBtn.addEventListener("click", function () {
        if (running) { running.stop(); }
    });
})();
