/*
 * Das Terminal im eigenen Fenster.
 *
 * Eigener Einstiegspunkt, nicht Teil von app.js: nicht weil diese Seite ohne
 * Livewire liefe — sie ist eine Vollseiten-Livewire-Komponente und lädt
 * `<x-shell.head>`, das app.js über bare.blade.php ohnehin mitzieht —,
 * sondern damit der Terminalcode (xterm.js) nicht in app.js landet und die
 * übrigen Konsolenseiten nicht mitschleppen, und umgekehrt app.js nicht in
 * dieses schlanke Fenster.
 */
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import '@xterm/xterm/css/xterm.css'

const root = document.querySelector('[data-terminal]')
const stage = root?.querySelector('[data-terminal-stage]')

// Der Knopf gehört zur Bühne, nicht zur Sitzung, und wird deshalb VOR der
// Weiche unten verdrahtet: bei einem Grund, den schon der Server kennt, gibt es
// gar kein Ticket — und der Knopf steht trotzdem da und muss tun.
//
// Neu laden statt neu verbinden, weil ein Ticket dreißig Sekunden gilt und genau
// einmal: derselbe Wert ein zweites Mal wäre kein zweiter Versuch.
stage?.querySelector('[data-stage-retry-button]')?.addEventListener('click', () => location.reload())

// Ohne Ticket gibt es nichts zu verbinden: die Seite hat dann schon
// hingeschrieben, woran es liegt (HostTerminal::mount, `$problem`), und ein
// Socket, den wir trotzdem aufmachten, überschriebe diesen Satz mit einem
// zweiten, ungenaueren.
if (root && root.dataset.ticket) {
    const screen = root.querySelector('[data-terminal-screen]')
    const stageTitle = stage.querySelector('[data-stage-title]')
    const stageNote = stage.querySelector('[data-stage-note]')
    const stageRetry = stage.querySelector('[data-stage-retry]')
    // Der Hostname steht nirgends im Skript: er ist schon in die Meldungen
    // eingesetzt, die der Server mitgegeben hat. Tunneladresse und Schlüssel
    // sieht dieses Fenster ohnehin nie.
    const messages = JSON.parse(stage.dataset.stageMessages)

    /**
     * Die Bühne zeigen. `which` ist einer der Schlüssel aus
     * `data-stage-messages`; `offerRetry` entscheidet über den Knopf — er kommt
     * nur, wo ein zweiter Anlauf überhaupt etwas ändern kann.
     */
    const showStage = (which, offerRetry) => {
        stageTitle.textContent = messages[which].title
        stageNote.textContent = messages[which].note
        stageRetry.classList.toggle('hidden', !offerRetry)
        stage.classList.remove('hidden')
    }

    const term = new Terminal({ convertEol: true, fontFamily: 'ui-monospace, monospace', fontSize: 13 })
    const fit = new FitAddon()
    term.loadAddon(fit)

    const scheme = location.protocol === 'https:' ? 'wss' : 'ws'
    // Das Ticket reist als Unterprotokoll, NICHT in der Adresszeile: die Adresse
    // eines Upgrade-Antrags schreibt jeder Reverse Proxy auf der Strecke mit
    // (nginx hier, Caddy davor), ein Kopffeld nicht. Die Brücke spiegelt den
    // Wert zurück, sonst bricht der Browser den Handschlag ab.
    //
    // `/terminal/ws` steht fest an der Wurzel und folgt AdminArea::prefix()
    // bewusst nicht: die Stelle wird von nginx durchgereicht und sieht PHP nie,
    // also muss sie ein fester Text sein — in docker/nginx/default.conf steht
    // derselbe, mit derselben Begründung.
    const socket = new WebSocket(`${scheme}://${location.host}/terminal/ws`, [root.dataset.ticket])
    socket.binaryType = 'arraybuffer'

    // xterm hängt sofort im Schirm, nicht erst beim ersten Byte: das Element hat
    // von Anfang an seine volle Größe (die Bühne liegt darüber, nicht daneben),
    // und `fit()` rechnet nur an einem Element richtig, das schon Maße hat.
    term.open(screen)
    fit.fit()

    // Die Bühne weicht beim ERSTEN BYTE, nicht bei `onopen`: ein stehender
    // Socket sagt noch nichts darüber, ob am anderen Ende eine Sitzung entstanden
    // ist, und der Unterschied fiele sonst erst auf, wenn jemand ins Leere tippt.
    let received = false
    const reveal = () => {
        if (received) return
        received = true
        stage.classList.add('hidden')
        fit.fit()
    }

    socket.onmessage = (event) => {
        reveal()
        // Kommt vom Container ein Textrahmen statt eines Binärrahmens, ist
        // `new Uint8Array(event.data)` bei einem String ein leeres Array —
        // `binaryType = 'arraybuffer'` oben regelt nur Binärrahmen.
        //
        // Die Brücke hat sich inzwischen festgelegt: sie sendet ausschließlich
        // Binärrahmen (docker/terminal/bridge.py, to_browser). Diese Verzweigung
        // bleibt trotzdem stehen — sie kostet nichts und ist die einzige Stelle,
        // an der ein Rückfall nicht als stumme leere Ausgabe ankäme.
        term.write(typeof event.data === 'string' ? event.data : new Uint8Array(event.data))
    }

    term.onData((data) => socket.readyState === WebSocket.OPEN && socket.send(data))

    // Die Schließcodes kommen aus docker/terminal/bridge.py und sind die einzige
    // Auskunft, die die Brücke über den Grund gibt — absichtlich: was genau am
    // Host schiefging, steht in ihrem Log, nicht in diesem Fenster.
    //
    //   4401  Ticket unbekannt, abgelaufen oder schon eingelöst
    //   4502  kein SSH zum Host (keine Antwort, oder Fingerabdruck weicht ab)
    //
    // Alles andere ist ein gewöhnliches Ende: die Gegenseite hat aufgelegt.
    // Das kann die Sitzung selbst sein (`exit`), der Leerlaufwächter der Brücke
    // oder eine Leitung, die abgerissen ist — für den Betreiber ist der
    // Unterschied derselbe: hier läuft nichts mehr, und ein neuer Anlauf hilft.
    socket.onclose = (event) => {
        if (event.code === 4401) return showStage('rejected', true)
        if (event.code === 4502) return showStage('unreachable', true)
        if (received) return showStage('ended', true)

        // Nie eine Sitzung gehabt — und genau hier wusste das Fenster bisher
        // nichts und riet.
        //
        // Scheitert ein WebSocket schon am Handschlag, bekommt die Seite laut
        // Norm KEINEN HTTP-Status: `event.code` ist 1006, sonst nichts. Das ist
        // Absicht (sonst wäre ein Socket ein Portscanner), macht aber genau die
        // Auskunft unmöglich, die ein Betreiber hier braucht — läuft die Brücke
        // nicht, oder ist die Leitung weg? Beides sah gleich aus.
        //
        // Eine gewöhnliche Anfrage an dieselbe Stelle darf den Status sehen.
        // nginx antwortet dort mit 502, solange die Brücke nicht erreichbar ist
        // (`resolver` + Variable, docker/nginx/default.conf), mit 404 auf einem
        // öffentlichen Namen, und sonst mit dem, was die Brücke selbst sagt —
        // ein Upgrade-Fehler, aber eben von ihr. Das reicht zum Unterscheiden.
        showStage('lost', true)

        fetch('/terminal/ws', { cache: 'no-store' })
            .then((probe) => {
                if ([502, 503, 504].includes(probe.status)) showStage('service_down', true)
                else if (probe.status === 404) showStage('not_here', false)
            })
            .catch(() => {
                // Die Anfrage kam nicht einmal los: dann ist wirklich die
                // Leitung weg, und „Keine Verbindung" von oben stimmt.
            })
    }

    // Kein `onerror`: ein Fehler zieht immer ein `close` nach sich, und dort oben
    // steht die Meldung — mit einem Code, der etwas aussagt. Zwei Handler
    // schrieben zwei Sätze über denselben Vorgang, und genau das stand vorher da.

    addEventListener('resize', () => fit.fit())
}
