/**
 * live_terminal.js — Terminal de salida en vivo, batched + buffered.
 *
 * Reemplaza el flujo legacy donde cada evento SSE producia un appendChild +
 * scrollTop + posible removeChild (reflow por linea, O(n^2) acumulado por los
 * innerHTML += dispersos). Esta clase:
 *
 *   - Acumula eventos en un array `lines` (single source of truth).
 *   - Cada evento programa un flush con setTimeout(60ms) + requestAnimationFrame.
 *   - El flush vuelca el batch al DOM en un solo DocumentFragment + un solo
 *     scroll + un solo trim. N reflows -> 1 reflow por batch.
 *   - El boton "Copiar" lee del array, no del DOM.
 *
 * API (expuesta como `window.LiveTerminal`):
 *
 *   const term = new LiveTerminal(screenEl, {
 *       maxLines: 3000,            // cap visible. Lo que sobra se descarta
 *                                   // del DOM y del array (truncated++).
 *       onError: (text) => { ... }, // callback para lineas clasificadas como
 *                                   // error y que no son [SISTEMA] (para
 *                                   // alimentar la pestania Errores).
 *       classifier: (text) => ({    // opcional. Por default usa el clasificador
 *           lineClass, downloadUrl   // de scripts. Para Playwright puedes pasar
 *       }),                          // uno distinto.
 *       onStats: (stats) => { ... }, // recibe { total, visible, truncated }
 *                                    // tras cada flush para pintar contadores.
 *       onEof: () => { ... },        // se invoca al recibir [EOF].
 *       onStreamError: (err) => {},  // se invoca cuando EventSource.onerror
 *                                    // se dispara (para que el caller decida
 *                                    // reconexion / cierre / etc).
 *   });
 *
 *   term.attachStream(taskId);   // abre el EventSource y conecta el flujo
 *   term.appendSystem(text, "info" | "error" | "success");  // mensajes [SISTEMA]
 *   term.getCopyText();          // string completo del buffer
 *   term.getStats();             // { total, visible, truncated, sinceLastFlush }
 *   term.reset();                // limpia DOM + array, mantiene el EventSource
 *   term.detach();               // cierra el EventSource (mantiene el buffer)
 *   term.destroy();              // detach + reset + remueve listeners
 *
 * Cualquier marcador "[DOWNLOAD] /api/exports/<file>" se renderiza como un
 * link de descarga (preserva el comportamiento original).
 */
(function () {
    "use strict";

    // -----------------------------------------------------------------------
    // Clasificador default — replica la logica del flujo legacy de scripts.
    // -----------------------------------------------------------------------
    function defaultClassifier(line) {
        const downloadMatch = line.match(/^\[DOWNLOAD\]\s+(\/api\/exports\/[^\s]+)\s*$/);
        if (downloadMatch) {
            return { lineClass: "download", downloadUrl: downloadMatch[1] };
        }
        if (line.indexOf("ERROR") !== -1 ||
            line.indexOf("Exception") !== -1 ||
            line.indexOf("Failed") !== -1) {
            return { lineClass: "error" };
        }
        if (line.indexOf("COMPLETADA CON ÉXITO") !== -1 ||
            line.indexOf("exitosa") !== -1) {
            return { lineClass: "success" };
        }
        if (line.indexOf("===") !== -1 || line.indexOf("INICIANDO") !== -1) {
            return { lineClass: "info" };
        }
        return { lineClass: "" };
    }

    // -----------------------------------------------------------------------
    // Construye un nodo DOM para un entry, SIEMPRE con createElement+textContent.
    // Cero innerHTML. Devuelve un <div class="terminal-line ...">.
    // -----------------------------------------------------------------------
    function renderLine(entry) {
        const div = document.createElement("div");
        let cls = "terminal-line";
        if (entry.lineClass) cls += " " + entry.lineClass;
        div.className = cls;

        if (entry.downloadUrl) {
            const url = entry.downloadUrl;
            const filename = decodeURIComponent(url.split("/").pop() || "archivo");
            const a = document.createElement("a");
            a.href = url;
            a.download = filename;
            a.target = "_blank";
            a.rel = "noopener";
            a.className = "terminal-download-link";
            a.textContent = "📥 Descargar " + filename;
            div.appendChild(a);
        } else {
            div.textContent = entry.text;
        }
        return div;
    }

    // -----------------------------------------------------------------------
    // LiveTerminal — instanciable, reusable entre la terminal de scripts y la
    // de Playwright.
    // -----------------------------------------------------------------------
    function LiveTerminal(screenEl, opts) {
        opts = opts || {};
        if (!screenEl) {
            throw new Error("LiveTerminal: screenEl es requerido.");
        }
        this.screen = screenEl;
        this.maxLines = opts.maxLines || 3000;
        this.onError = typeof opts.onError === "function" ? opts.onError : null;
        this.onStats = typeof opts.onStats === "function" ? opts.onStats : null;
        this.onEof = typeof opts.onEof === "function" ? opts.onEof : null;
        this.onStreamError = typeof opts.onStreamError === "function" ? opts.onStreamError : null;
        this.classifier = typeof opts.classifier === "function" ? opts.classifier : defaultClassifier;

        this.lines = [];        // [{ text, lineClass, downloadUrl?, system?, ts }]
        this.pending = [];      // batch entrante sin pintar
        this.truncated = 0;     // contador acumulado de descartadas
        this.rafScheduled = false;
        this.userScrolledUp = false;
        this.eventSource = null;
        this.taskId = null;
        this.lastByte = 0;      // para reconexion por tail

        // Listener para detectar si el usuario se aleja del fondo
        this._scrollHandler = this._onScroll.bind(this);
        this.screen.addEventListener("scroll", this._scrollHandler, { passive: true });
    }

    LiveTerminal.prototype._onScroll = function () {
        const s = this.screen;
        const atBottom = s.scrollTop + s.clientHeight >= s.scrollHeight - 40;
        this.userScrolledUp = !atBottom;
    };

    // Empuja una entry al pending y agenda flush si no esta agendado.
    LiveTerminal.prototype._enqueue = function (entry) {
        this.pending.push(entry);
        // Onerror sincrono: el callback se llama AQUI, no en el flush, para que
        // el badge de errores y el panel se actualicen al instante.
        if (entry.lineClass === "error" && !entry.system && this.onError) {
            try { this.onError(entry.text); } catch (e) { /* swallow */ }
        }
        if (!this.rafScheduled) {
            this.rafScheduled = true;
            const self = this;
            setTimeout(function () {
                if (window.requestAnimationFrame) {
                    requestAnimationFrame(function () { self._flush(); });
                } else {
                    self._flush();
                }
            }, 60);
        }
    };

    // Vacia el batch al DOM en un solo fragment + un solo trim + un solo scroll.
    LiveTerminal.prototype._flush = function () {
        this.rafScheduled = false;
        if (!this.pending.length) return;

        // 1. Merge al array maestro
        const incoming = this.pending;
        this.pending = [];
        for (let i = 0; i < incoming.length; i++) {
            this.lines.push(incoming[i]);
        }

        // 2. Trim del array maestro (un solo splice)
        let trimmedThisFlush = 0;
        if (this.lines.length > this.maxLines) {
            trimmedThisFlush = this.lines.length - this.maxLines;
            this.lines.splice(0, trimmedThisFlush);
            this.truncated += trimmedThisFlush;
        }

        // 3. Limpiar placeholder inicial si todavia esta
        const placeholder = this.screen.querySelector(".terminal-placeholder");
        if (placeholder) placeholder.remove();

        // 4. Render del batch en un solo DocumentFragment
        const frag = document.createDocumentFragment();
        for (let j = 0; j < incoming.length; j++) {
            frag.appendChild(renderLine(incoming[j]));
        }
        this.screen.appendChild(frag);

        // 5. Trim del DOM. Si el usuario tiene texto seleccionado dentro del
        //    screen, posponemos el trim al siguiente flush (no perder seleccion).
        const sel = window.getSelection ? window.getSelection() : null;
        const selInScreen = sel && sel.toString() && sel.anchorNode &&
                            this.screen.contains(sel.anchorNode);
        if (!selInScreen) {
            const excess = this.screen.childElementCount - this.maxLines;
            if (excess > 0) {
                for (let k = 0; k < excess; k++) {
                    const first = this.screen.firstElementChild;
                    if (!first) break;
                    first.remove();
                }
            }
        }

        // 6. Auto-scroll una sola vez por flush
        if (!this.userScrolledUp) {
            this.screen.scrollTop = this.screen.scrollHeight;
        }

        // 7. Notificar stats
        if (this.onStats) {
            try {
                this.onStats({
                    total: this.lines.length + this.truncated,
                    visible: this.lines.length,
                    truncated: this.truncated,
                    flushed: incoming.length,
                });
            } catch (e) { /* swallow */ }
        }
    };

    // Procesa una linea cruda del stream (con [EOF] / [DOWNLOAD] / clasificacion).
    LiveTerminal.prototype._handleStreamLine = function (rawLine) {
        if (rawLine === "[EOF]") {
            // Forzar flush inmediato antes de avisar al caller.
            this._flush();
            if (this.onEof) {
                try { this.onEof(); } catch (e) { /* swallow */ }
            }
            return;
        }
        const cls = this.classifier(rawLine) || {};
        this._enqueue({
            text: rawLine,
            lineClass: cls.lineClass || "",
            downloadUrl: cls.downloadUrl || null,
            ts: Date.now(),
        });
    };

    // ---- API publica ------------------------------------------------------

    LiveTerminal.prototype.appendSystem = function (text, lineClass) {
        this._enqueue({
            text: text,
            lineClass: lineClass || "info",
            system: true,
            ts: Date.now(),
        });
    };

    LiveTerminal.prototype.attachStream = function (taskId, opts) {
        opts = opts || {};
        // Si ya hay un EventSource abierto, cerrarlo.
        this.detach();
        this.taskId = taskId;
        this.lastByte = 0;
        const url = opts.url || ("/api/scripts/stream/" + encodeURIComponent(taskId));
        const es = new EventSource(url);
        this.eventSource = es;
        const self = this;
        es.onmessage = function (ev) {
            self._handleStreamLine(ev.data);
        };
        es.onerror = function (e) {
            if (self.onStreamError) {
                try { self.onStreamError(e, es); } catch (err) { /* swallow */ }
            }
        };
        return es;
    };

    LiveTerminal.prototype.detach = function () {
        if (this.eventSource) {
            try { this.eventSource.close(); } catch (e) { /* swallow */ }
            this.eventSource = null;
        }
    };

    LiveTerminal.prototype.reset = function () {
        this.pending = [];
        this.lines = [];
        this.truncated = 0;
        this.rafScheduled = false;
        this.userScrolledUp = false;
        // Limpiar DOM sin innerHTML para mantener la consistencia.
        while (this.screen.firstChild) this.screen.removeChild(this.screen.firstChild);
        if (this.onStats) {
            try {
                this.onStats({ total: 0, visible: 0, truncated: 0, flushed: 0 });
            } catch (e) { /* swallow */ }
        }
    };

    LiveTerminal.prototype.destroy = function () {
        this.detach();
        this.screen.removeEventListener("scroll", this._scrollHandler);
        this.reset();
    };

    LiveTerminal.prototype.getCopyText = function () {
        // Lee del array, no del DOM. Cero force-layout.
        const out = [];
        for (let i = 0; i < this.lines.length; i++) {
            out.push(this.lines[i].text);
        }
        return out.join("\n");
    };

    LiveTerminal.prototype.getStats = function () {
        return {
            total: this.lines.length + this.truncated,
            visible: this.lines.length,
            truncated: this.truncated,
            pending: this.pending.length,
        };
    };

    LiveTerminal.prototype.scrollToBottom = function () {
        this.userScrolledUp = false;
        this.screen.scrollTop = this.screen.scrollHeight;
    };

    // Expone tambien el clasificador default por si alguien quiere extenderlo.
    LiveTerminal.defaultClassifier = defaultClassifier;

    window.LiveTerminal = LiveTerminal;
})();
