/**
 * Pipeline-Modul: Visualisierung der Analysepipeline pro Lage.
 *
 * - Liest Pipeline-Definition + letzten Refresh-Stand vom Backend
 *   (GET /api/incidents/{id}/pipeline)
 * - Hört auf WebSocket-Events vom Typ "pipeline_step" und animiert Live
 *   den jeweils aktiven Schritt
 * - Bei Lagen-Wechsel wird die Visualisierung an die neue Lage neu gebunden
 *
 * Stilkonzept:
 * - Blöcke = Karten mit Icon + Titel + Zahl
 * - Verbindungspfeile als SVG zwischen den Blöcken
 * - Aktiver Block: pulsierender Glow (CSS-Klasse .is-active)
 * - Fertiger Block: Häkchen + dezente Outline (.is-done)
 * - Übersprungener Block: ausgeblendet (laut Anforderung)
 * - Multi-Pass (Research): am letzten Block leuchtet ein Schleifen-Pfeil auf
 */
const Pipeline = {
    _incidentId: null,
    _definition: null,        // PIPELINE_STEPS vom Backend
    _stateByKey: {},          // step_key -> {status, count_value, count_secondary, pass_number}
    _snapshotState: null,     // deep-copy von _stateByKey vor Refresh-Start (fuer Cancel-Restore)
    _isResearch: false,
    _passTotal: 1,
    _lastRefreshHeader: null,
    _hoverTooltipEl: null,
    _isLoading: false,
    _wsBound: false,
    _icons: {
        search: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>',
        rss: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1.5"/></svg>',
        'copy-x': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="13" height="13" rx="2"/><path d="M8 21h11a2 2 0 0 0 2-2V8"/><path d="M11 11l4 4M15 11l-4 4"/></svg>',
        scale: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18"/><path d="M5 8h14"/><path d="M5 8l-3 7h6z"/><path d="M19 8l-3 7h6z"/></svg>',
        'map-pin': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s7-7 7-13a7 7 0 0 0-14 0c0 6 7 13 7 13z"/><circle cx="12" cy="9" r="2.5"/></svg>',
        'file-text': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/><path d="M8 13h8M8 17h8M8 9h2"/></svg>',
        shield: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l8 4v6c0 5-3.5 9-8 10-4.5-1-8-5-8-10V6z"/><path d="M9 12l2 2 4-4"/></svg>',
        'check-circle': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 12l3 3 5-6"/></svg>',
        bell: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10 21a2 2 0 0 0 4 0"/></svg>',
    },

    /** Wird einmal beim Seitenstart aufgerufen, hängt sich an WebSocket. */
    init() {
        if (this._wsBound) return;
        if (typeof WS !== 'undefined' && WS.on) {
            WS.on('pipeline_step', (msg) => this._onWsStep(msg));
            // Erfolg: API-State neu laden (finaler Stand sichtbar)
            WS.on('refresh_complete', (msg) => this._onRefreshDoneSuccess(msg));
            // Cancel/Error: vor-Refresh-Snapshot zurueckspielen, damit Pipeline nicht im Mix-Zustand stehen bleibt
            WS.on('refresh_cancelled', (msg) => this._onRefreshDoneCancel(msg));
            WS.on('refresh_error', (msg) => this._onRefreshDoneError(msg));
            this._wsBound = true;
        }
        // Hover-Tooltip-Element vorbereiten
        if (!this._hoverTooltipEl) {
            const t = document.createElement('div');
            t.className = 'pipeline-tooltip';
            t.setAttribute('role', 'tooltip');
            document.body.appendChild(t);
            this._hoverTooltipEl = t;
        }
        // Klick auf Body schliesst Tooltip-Popup
        document.addEventListener('click', (e) => {
            if (!e.target.closest('.pipeline-block') && !e.target.closest('.pipeline-popup')) {
                this._closePopup();
            }
        });
    },

    /** Bindet die Pipeline an eine Lage. Lädt Daten und rendert. */
    async bindToIncident(incidentId) {
        this._incidentId = incidentId;
        this._stateByKey = {};
        this._snapshotState = null;  // Snapshot ist immer lagen-spezifisch
        this._isResearch = false;
        this._passTotal = 1;
        this._lastRefreshHeader = null;
        this._renderEmpty('Lade...');
        if (incidentId == null) return;

        this._isLoading = true;
        try {
            const data = await API.getPipeline(incidentId);
            // Lagen-Wechsel waehrend Request: alte Antwort verwerfen
            if (this._incidentId !== incidentId) return;

            this._definition = data.steps_definition || [];
            this._isResearch = !!data.is_research;
            this._lastRefreshHeader = data.last_refresh || null;
            this._passTotal = (data.last_refresh && data.last_refresh.pass_total) || 1;

            // Letzten Stand pro step_key konsolidieren (bei Multi-Pass: letzter Pass-Eintrag gewinnt)
            (data.steps || []).forEach(s => {
                const key = s.step_key;
                const prev = this._stateByKey[key];
                if (!prev || (s.pass_number || 1) >= (prev.pass_number || 1)) {
                    this._stateByKey[key] = {
                        status: s.status,
                        count_value: s.count_value,
                        count_secondary: s.count_secondary,
                        pass_number: s.pass_number || 1,
                    };
                }
            });

            this._render();
            this._renderMini();

            // Edge-Case: Lage ist gerade in Queue (z.B. via Lagen-Wechsel beim
            // Klick in der Sidebar). API liefert den LETZTEN gespeicherten Stand
            // (alles done = gruen), aber tatsaechlich wartet ein neuer Refresh.
            // -> beginQueue() selbst ausloesen, damit Icons grau zeigen.
            try {
                if (typeof App !== 'undefined' && App._refreshingIncidents
                    && App._refreshingIncidents.has(incidentId)
                    && typeof UI !== 'undefined' && UI._progressState
                    && UI._progressState[incidentId]
                    && UI._progressState[incidentId].step === 'queued') {
                    this.beginQueue(incidentId);
                }
            } catch (e) { /* tolerant */ }
        } catch (e) {
            console.warn('Pipeline laden fehlgeschlagen:', e);
            this._renderEmpty('Pipeline-Daten konnten nicht geladen werden.');
        } finally {
            this._isLoading = false;
        }
    },

    /** WebSocket: einzelner Pipeline-Schritt-Status. */
    _onWsStep(msg) {
        if (!msg || !msg.data) return;
        if (this._incidentId == null || msg.incident_id !== this._incidentId) return;

        const d = msg.data;
        const key = d.step_key;
        if (!key) return;

        // State aktualisieren, letzter Pass gewinnt
        const prev = this._stateByKey[key];
        const passNr = d.pass_number || 1;
        if (!prev || passNr >= (prev.pass_number || 1)) {
            this._stateByKey[key] = {
                status: d.status,
                count_value: d.count_value !== undefined ? d.count_value : (prev ? prev.count_value : null),
                count_secondary: d.count_secondary !== undefined ? d.count_secondary : (prev ? prev.count_secondary : null),
                pass_number: passNr,
            };
        }

        // Multi-Pass-Erkennung: pass_number > _passTotal -> erweitern + Loop-Animation triggern
        if (passNr > this._passTotal) {
            this._passTotal = passNr;
            // Schleifen-Pfeil aufflackern
            const stage = document.getElementById('pipeline-stage');
            if (stage) {
                stage.classList.add('is-looping');
                setTimeout(() => stage.classList.remove('is-looping'), 1500);
            }
        }

        // Wenn der ERSTE Schritt (sources_review) auf "active" geht, beginnt ein neuer
        // Refresh oder ein neuer Multi-Pass-Durchlauf — alle nachfolgenden Schritte auf
        // "pending" (grau) zuruecksetzen, damit der User sieht: das ist neu und
        // noch nicht durchlaufen. Sonst stehen sie als "done" vom letzten Mal da.
        let didReset = false;
        if (d.status === 'active' && this._definition && this._definition.length
            && key === this._definition[0].key) {
            this._definition.forEach(s => {
                if (s.key !== key && this._stateByKey[s.key]) {
                    this._stateByKey[s.key].status = 'pending';
                    didReset = true;
                }
            });
        }

        if (didReset) {
            // Beim Reset alle Bloecke neu zeichnen, nicht nur den aktuellen
            this._render();
            this._renderMini();
        } else {
            this._patchBlock(key);
            this._patchMiniBlock(key);
        }
    },

    /**
     * Wird vom Frontend gerufen, wenn ein Refresh angestossen wurde (queued).
     * Macht einen Snapshot des aktuellen Pipeline-Stands (zur spaeteren Wiederherstellung
     * bei Cancel/Error) und setzt dann alle Steps auf "pending" - damit der User sieht:
     * "neuer Refresh laeuft an, alte gruene Haekchen sind nicht mehr aktuell".
     */
    beginQueue(incidentId) {
        if (this._incidentId !== incidentId) return;          // andere Lage offen
        if (!this._definition) return;                        // noch keine Pipeline-Definition geladen
        // Aktuellen Stand sichern (deep-copy). Bei Mehrfach-Refresh ohne Cancel
        // dazwischen wird der Snapshot bewusst ueberschrieben - er soll immer
        // der "Stand kurz vor diesem Refresh" sein.
        this._snapshotState = JSON.parse(JSON.stringify(this._stateByKey));
        // Alle Steps auf pending setzen
        this._definition.forEach(s => {
            if (this._stateByKey[s.key]) {
                this._stateByKey[s.key].status = 'pending';
            } else {
                this._stateByKey[s.key] = { status: 'pending', count_value: null, count_secondary: null, pass_number: 1 };
            }
        });
        this._render();
        this._renderMini();
    },

    /** Restauriert den letzten Snapshot. Rueckgabe: true bei Erfolg, false wenn keiner da war. */
    _restoreSnapshot() {
        if (!this._snapshotState) return false;
        this._stateByKey = this._snapshotState;
        this._snapshotState = null;
        this._render();
        this._renderMini();
        return true;
    },

    _onRefreshDoneSuccess(msg) {
        if (this._incidentId == null || (msg && msg.incident_id !== this._incidentId)) return;
        this._snapshotState = null;  // verworfen, neuer Stand wird vom API geladen
        // Daten frisch nachladen, damit Header (Dauer) und finale Zahlen passen
        setTimeout(() => {
            if (this._incidentId != null) this.bindToIncident(this._incidentId);
        }, 600);
    },

    _onRefreshDoneCancel(msg) {
        if (this._incidentId == null || (msg && msg.incident_id !== this._incidentId)) return;
        if (!this._restoreSnapshot()) {
            // Kein Snapshot vorhanden (z.B. Page-Reload mitten im Refresh) -> wie bisher API-Reload
            setTimeout(() => {
                if (this._incidentId != null) this.bindToIncident(this._incidentId);
            }, 600);
        }
    },

    _onRefreshDoneError(msg) {
        // Wie Cancel: vorheriger Stand zurueck (nicht im Mix-Zustand stehenbleiben)
        this._onRefreshDoneCancel(msg);
    },

    /** Vollbild-Pipeline (Tab "Analysepipeline") als 3x3-Snake rendern. */
    _render() {
        const stage = document.getElementById('pipeline-stage');
        const meta = document.getElementById('pipeline-header-meta');
        const sidenote = document.getElementById('pipeline-sidenote');
        if (!stage) return;

        if (meta) meta.textContent = this._formatHeader();
        if (sidenote) sidenote.hidden = !this._isResearch;

        // Brandneue Lage ohne Refresh
        if (!this._lastRefreshHeader) {
            const _t = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
            this._renderEmpty(_t('pipeline.empty', 'Noch nie aktualisiert. Starte den ersten Refresh.'));
            return;
        }

        // Sichtbare Blöcke (skipped komplett ausgeblendet, Anforderung 4b)
        const visible = (this._definition || []).filter(s => {
            const st = this._stateByKey[s.key];
            return !st || st.status !== 'skipped';
        });

        // In Dreier-Reihen aufteilen, Snake-Direction abwechselnd
        const ROW_SIZE = 3;
        const rows = [];
        for (let i = 0; i < visible.length; i += ROW_SIZE) {
            rows.push({
                steps: visible.slice(i, i + ROW_SIZE),
                direction: (rows.length % 2 === 0) ? 'ltr' : 'rtl',
            });
        }

        let trackHtml = '';
        rows.forEach((row, rowIdx) => {
            const isLastRow = rowIdx === rows.length - 1;
            let rowHtml = `<div class="pipeline-row" data-direction="${row.direction}">`;
            row.steps.forEach((s, i) => {
                const isLastBlockOverall = isLastRow && i === row.steps.length - 1;
                rowHtml += this._renderBlock(s, isLastBlockOverall);
                // Inner-Pfeil zwischen Blöcken einer Reihe (nicht hinter dem letzten)
                if (i < row.steps.length - 1) {
                    rowHtml += `<div class="pipeline-arrow" data-from="${s.key}" data-arrow-type="inner"></div>`;
                }
            });
            rowHtml += '</div>';
            trackHtml += rowHtml;

            // U-Turn-Pfeil zwischen dieser und der nächsten Reihe
            if (!isLastRow) {
                const lastInRow = row.steps[row.steps.length - 1];
                const side = row.direction === 'ltr' ? 'right' : 'left';
                trackHtml += this._renderUturn(side, lastInRow.key);
            }
        });

        stage.innerHTML = `<div class="pipeline-track">${trackHtml}</div>`;
        this._bindBlockEvents(stage);
    },

    _renderBlock(stepDef, isLastOverall) {
        const st = this._stateByKey[stepDef.key];
        const status = (st && st.status) || 'pending';
        const cv = st ? st.count_value : null;
        const cs = st ? st.count_secondary : null;
        const loopMark = isLastOverall && this._isResearch
            ? `<div class="pipeline-loop" title="Mehrfach-Durchlauf"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 4v5h-5"/></svg></div>`
            : '';
        const icon = this._icons[stepDef.icon] || this._icons.search;
        return `
            <div class="pipeline-block status-${status}" data-step-key="${stepDef.key}" tabindex="0" aria-label="${this._escape(stepDef.label)}">
                <div class="pipeline-block-icon">${icon}</div>
                <div class="pipeline-block-title">${this._escape(stepDef.label)}</div>
                <div class="pipeline-block-count">${this._formatCount(stepDef.key, cv, cs, status)}</div>
                <div class="pipeline-block-check" aria-hidden="true">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5 9-11"/></svg>
                </div>
                ${loopMark}
            </div>
        `;
    },

    /** Kompakter Reihenwechsel-Pfeil: kurzer ↓ direkt unter dem letzten Block der oberen Reihe. */
    _renderUturn(side, fromKey) {
        const arrowSvg = `
            <div class="uturn-arrow">
                <svg viewBox="0 0 24 32" preserveAspectRatio="xMidYMid meet">
                    <path d="M 12 2 L 12 24" class="pipeline-uturn-path"/>
                    <polyline points="6,18 12,24 18,18" class="pipeline-uturn-head"/>
                </svg>
            </div>`;
        const spacers = '<span class="uturn-spacer"></span><span class="uturn-spacer"></span>';
        const inner = side === 'right' ? (spacers + arrowSvg) : (arrowSvg + spacers);
        return `
            <div class="pipeline-uturn" data-side="${side}" data-from="${fromKey}" data-arrow-type="uturn" aria-hidden="true">
                ${inner}
            </div>
        `;
    },

    /** Einzelnen Block neu zeichnen (ohne kompletten Re-Render). */
    _patchBlock(stepKey) {
        const stage = document.getElementById('pipeline-stage');
        if (!stage) return;
        const def = (this._definition || []).find(s => s.key === stepKey);
        if (!def) return;
        const st = this._stateByKey[stepKey];
        const status = (st && st.status) || 'pending';

        // Übersprungene komplett ausblenden -> kompletter Re-Render
        if (status === 'skipped') {
            this._render();
            return;
        }

        const block = stage.querySelector(`.pipeline-block[data-step-key="${stepKey}"]`);
        if (!block) {
            // Block fehlt im DOM (z.B. vorher skipped): kompletter Re-Render
            this._render();
            return;
        }
        block.className = `pipeline-block status-${status}`;
        block.setAttribute('tabindex', '0');
        const cv = st ? st.count_value : null;
        const cs = st ? st.count_secondary : null;
        const cEl = block.querySelector('.pipeline-block-count');
        if (cEl) cEl.innerHTML = this._formatCount(stepKey, cv, cs, status);

        // Aktiven Pfeil/U-Turn zum nächsten Block markieren (alles mit data-from)
        stage.querySelectorAll('.pipeline-arrow, .pipeline-uturn')
             .forEach(a => a.classList.remove('is-flowing'));
        if (status === 'done') {
            const next = stage.querySelector(`[data-from="${stepKey}"]`);
            if (next) next.classList.add('is-flowing');
        }
    },

    _bindBlockEvents(stage) {
        stage.querySelectorAll('.pipeline-block').forEach(block => {
            const key = block.getAttribute('data-step-key');
            const def = (this._definition || []).find(s => s.key === key);
            if (!def) return;

            block.addEventListener('mouseenter', (e) => this._showTooltip(e, def));
            block.addEventListener('mouseleave', () => this._hideTooltip());
            block.addEventListener('focus', (e) => this._showTooltip(e, def));
            block.addEventListener('blur', () => this._hideTooltip());
            block.addEventListener('click', (e) => {
                e.stopPropagation();
                this._openPopup(def);
            });
            block.addEventListener('keydown', (e) => {
                if (e.key === 'Enter' || e.key === ' ') {
                    e.preventDefault();
                    this._openPopup(def);
                }
            });
        });
    },

    _showTooltip(evt, def) {
        if (!this._hoverTooltipEl) return;
        this._hoverTooltipEl.textContent = def.tooltip || def.label;
        this._hoverTooltipEl.classList.add('visible');
        const rect = evt.currentTarget.getBoundingClientRect();
        const tipW = 280;
        let left = rect.left + rect.width / 2 - tipW / 2;
        if (left < 8) left = 8;
        if (left + tipW > window.innerWidth - 8) left = window.innerWidth - tipW - 8;
        this._hoverTooltipEl.style.left = left + 'px';
        this._hoverTooltipEl.style.top = (rect.top - 8) + 'px';
        this._hoverTooltipEl.style.transform = 'translateY(-100%)';
    },

    _hideTooltip() {
        if (!this._hoverTooltipEl) return;
        this._hoverTooltipEl.classList.remove('visible');
    },

    _openPopup(def) {
        this._closePopup();
        const popup = document.createElement('div');
        popup.className = 'pipeline-popup';
        popup.setAttribute('role', 'dialog');
        popup.innerHTML = `
            <div class="pipeline-popup-inner">
                <div class="pipeline-popup-title">${this._escape(def.label)}</div>
                <div class="pipeline-popup-text">${this._escape(def.tooltip || '')}</div>
                <button class="pipeline-popup-close" aria-label="Schliessen">&times;</button>
            </div>
        `;
        popup.querySelector('.pipeline-popup-close').addEventListener('click', () => this._closePopup());
        document.body.appendChild(popup);
        // ESC schliesst
        this._escListener = (e) => { if (e.key === 'Escape') this._closePopup(); };
        document.addEventListener('keydown', this._escListener);
    },

    _closePopup() {
        const existing = document.querySelector('.pipeline-popup');
        if (existing) existing.remove();
        if (this._escListener) {
            document.removeEventListener('keydown', this._escListener);
            this._escListener = null;
        }
    },

    /** Mini-Variante (Refresh-Popup): Icons + Status, keine Zahlen, keine Tooltips. */
    _renderMini() {
        const mini = document.getElementById('progress-pipeline-mini');
        if (!mini) return;
        if (!this._definition || !this._definition.length) {
            mini.innerHTML = '';
            return;
        }
        const visible = this._definition.filter(s => {
            const st = this._stateByKey[s.key];
            return !st || st.status !== 'skipped';
        });
        const html = visible.map((s, i) => {
            const st = this._stateByKey[s.key];
            const status = (st && st.status) || 'pending';
            const icon = this._icons[s.icon] || this._icons.search;
            const sep = (i < visible.length - 1) ? '<span class="pipeline-mini-sep" aria-hidden="true"></span>' : '';
            return `<span class="pipeline-mini-block status-${status}" data-step-key="${s.key}" title="${this._escape(s.label)}">${icon}</span>${sep}`;
        }).join('');
        mini.innerHTML = html;
    },

    _patchMiniBlock(stepKey) {
        const mini = document.getElementById('progress-pipeline-mini');
        if (!mini) return;
        const st = this._stateByKey[stepKey];
        const status = (st && st.status) || 'pending';
        if (status === 'skipped') {
            this._renderMini();
            return;
        }
        const el = mini.querySelector(`.pipeline-mini-block[data-step-key="${stepKey}"]`);
        if (!el) {
            this._renderMini();
            return;
        }
        el.className = `pipeline-mini-block status-${status}`;
    },

    _renderEmpty(msg) {
        const stage = document.getElementById('pipeline-stage');
        const meta = document.getElementById('pipeline-header-meta');
        const sidenote = document.getElementById('pipeline-sidenote');
        if (meta) meta.textContent = '';
        if (sidenote) sidenote.hidden = true;
        if (stage) stage.innerHTML = `<div class="pipeline-empty">${msg}</div>`;
        // Mini im Refresh-Popup zuruecksetzen
        const mini = document.getElementById('progress-pipeline-mini');
        if (mini) mini.innerHTML = '';
    },

    _formatHeader() {
        const r = this._lastRefreshHeader;
        if (!r) return '';
        const _t = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
        const lastLabel = _t('pipeline.last_refresh', 'Letzter Refresh');
        let parts = [];
        if (r.started_at) {
            const rel = this._relativeTime(r.started_at);
            parts.push(rel ? `${lastLabel}: ${rel}` : `${lastLabel}: ${r.started_at}`);
        }
        if (r.duration_sec != null) {
            parts.push(`${_t('pipeline.duration_prefix', 'Dauer:')} ${r.duration_sec} s`);
        }
        if (r.status === 'running') {
            parts = [_t('pipeline.running', 'Aktualisierung läuft...')];
        } else if (r.status === 'cancelled') {
            parts.push(_t('pipeline.cancelled', 'abgebrochen'));
        } else if (r.status === 'error') {
            parts.push(_t('pipeline.with_errors', 'mit Fehler beendet'));
        }
        return parts.join(' · ');
    },

    _relativeTime(dbStr) {
        try {
            // dbStr ist lokal "YYYY-MM-DD HH:MM:SS"
            const d = new Date(dbStr.replace(' ', 'T'));
            if (isNaN(d.getTime())) return '';
            const diffMs = Date.now() - d.getTime();
            const min = Math.floor(diffMs / 60000);
            const _t = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
            if (min < 1) return _t('time.just_now', 'gerade eben');
            if (min < 60) return _t('time.minutes_ago', 'vor {n} Min').replace('{n}', min);
            const h = Math.floor(min / 60);
            if (h < 24) return _t('time.hours_ago', 'vor {n} Std').replace('{n}', h);
            const days = Math.floor(h / 24);
            if (days === 1) return _t('time.day_ago', 'vor 1 Tag');
            return _t('time.days_ago', 'vor {n} Tagen').replace('{n}', days);
        } catch (e) {
            return '';
        }
    },

    _formatCount(stepKey, cv, cs, status) {
        const _t = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
        const sDone = _t('pipeline.status.done', 'erledigt');
        const sRun = _t('pipeline.status.running', 'läuft...');
        const sErr = _t('pipeline.status.error', 'Fehler');
        // Qualitaetscheck: KEINE Zahlen, nur Status (Anforderung 3 vom User)
        if (stepKey === 'qc' || stepKey === 'summary') {
            if (status === 'done') return `<span class="count-status">${sDone}</span>`;
            if (status === 'active') return `<span class="count-status">${sRun}</span>`;
            if (status === 'error') return `<span class="count-status">${sErr}</span>`;
            return '<span class="count-status">-</span>';
        }
        if (status === 'pending') return '<span class="count-status">-</span>';
        if (status === 'active') return `<span class="count-status">${sRun}</span>`;
        if (status === 'error') return `<span class="count-status">${sErr}</span>`;
        if (cv == null) return '<span class="count-status">-</span>';

        switch (stepKey) {
            case 'sources_review':
                return `${cv} Quellen geprüft`;
            case 'collect':
                return cs != null
                    ? `${cv} Meldungen<small> aus ${cs} Quellen</small>`
                    : `${cv} Meldungen`;
            case 'dedup':
                return cs != null
                    ? `${cv} Duplikate<small> (${cs} verbleiben)</small>`
                    : `${cv} Duplikate`;
            case 'relevance':
                return cs != null && cs > 0
                    ? `${cv} relevant<small> von ${cs}</small>`
                    : `${cv} relevant`;
            case 'geoparsing':
                return cs != null
                    ? `${cv} Orte<small> aus ${cs} Meldungen</small>`
                    : `${cv} Orte erkannt`;
            case 'factcheck':
                return cs != null
                    ? `${cv} neue Fakten<small> (${cs} gesamt)</small>`
                    : `${cv} Fakten geprüft`;
            case 'notify':
                return cv === 0 ? 'keine versendet' : `${cv} Hinweis${cv === 1 ? '' : 'e'} versendet`;
            default:
                return `${cv}`;
        }
    },

    _escape(s) {
        if (s == null) return '';
        return String(s).replace(/[&<>"']/g, c => ({
            '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
        }[c]));
    },
};

document.addEventListener('DOMContentLoaded', () => Pipeline.init());
