/* event-modal.js — shared event-detail modal.
 *
 * Used by:
 *   - /sfm (admin Events tab)
 *   - /projects/{p}/nrl/{l}    (project-location Events tab)
 *   - /unit/{id}               (unit-detail SFM Events table)
 *
 * Pages must include partials/event_detail_modal.html in the body
 * before this script is loaded.
 *
 * Public API:
 *   showEventDetail(eventId)
 *       Open the modal and fetch /api/sfm/db/events/{id}/sidecar to
 *       populate the rich BW report fields (peaks, ZC freq, sensor
 *       self-check, device info, etc.) into a tabbed/sectioned view.
 *
 *   closeEventDetailModal()
 *       Close the modal.
 *
 * Notes:
 *   - Fetches sidecar live from SFM via terra-view's /api/sfm proxy.
 *   - Renders gracefully when the sidecar lacks a bw_report block
 *     (older events forwarded before the _ASCII.TXT pairing fix).
 *   - All functions are global on window so inline onclick handlers
 *     can reach them across all three host pages.
 */

(function () {
    const MODAL_ID = 'event-detail-modal';

    // ── Chart.js constants (ported from sfm_webapp.html:2555-2880) ──
    const _CHANNEL_COLORS = {
        MicL: '#e066ff',  // purple — distinct from the geo channels
        Long: '#3b82f6',  // blue
        Vert: '#22c55e',  // green
        Tran: '#ef4444',  // red
    };
    const _CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];

    // dB(L) reference pressure — 20 µPa expressed in psi (Instantel native unit).
    const DBL_REF = 2.9e-9;
    // Mic display floor — sound-pressure AC samples sit at the digitisation
    // noise floor most of the time (1-2 ADC counts ≈ 20-40 dBL).  Without
    // a floor, the chart looks like a sparse pattern of "moments when sound
    // briefly exceeded the Y-axis bottom" instead of an SPL-vs-time curve.
    const MIC_DBL_FLOOR = 60;

    let _charts = {};                // ch → Chart instance
    let _micUnitPref = 'psi';        // refreshed via fetch on first chart render
    let _micUnitPrefLoaded = false;  // one-shot fetch guard

    function _esc(s) {
        if (s == null) return '';
        return String(s).replace(/&/g, '&amp;')
                        .replace(/</g, '&lt;')
                        .replace(/>/g, '&gt;')
                        .replace(/"/g, '&quot;');
    }

    function _fmt(v, digits = 4, suffix = '') {
        if (v == null || (typeof v === 'number' && Number.isNaN(v))) return '—';
        if (typeof v === 'number') {
            return v.toFixed(digits) + (suffix ? ` ${suffix}` : '');
        }
        return _esc(v) + (suffix ? ` ${suffix}` : '');
    }

    function _ppvClass(v) {
        if (v == null) return 'text-gray-400';
        if (v < 0.5) return 'text-green-600 dark:text-green-400';
        if (v < 2.0) return 'text-amber-600 dark:text-amber-400';
        return 'text-red-600 dark:text-red-400 font-semibold';
    }

    function _kvCard(label, value, options = {}) {
        // Single key-value tile.  `value` is pre-rendered HTML (or text).
        const colorCls = options.colorCls || '';
        const valCls   = `font-mono font-semibold ${colorCls}`;
        return `<div class="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-3">
            <div class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">${_esc(label)}</div>
            <div class="${valCls} mt-1">${value}</div>
            ${options.sub ? `<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">${options.sub}</div>` : ''}
        </div>`;
    }

    function _deriveRecordType(filename, fallback) {
        // SFM currently hardcodes record_type="Waveform" for every event.
        // The actual type is encoded in the LAST character of the Blastware
        // filename's extension (e.g. "O121LL5E.IS0H" → "H" → Histogram).
        // We derive it client-side until SFM is fixed; if the suffix isn't
        // a known code we fall back to whatever SFM reported.
        if (!filename) return fallback || '—';
        const dotIdx = filename.lastIndexOf('.');
        if (dotIdx < 0 || dotIdx === filename.length - 1) return fallback || '—';
        const ext = filename.slice(dotIdx + 1);
        const lastChar = ext.slice(-1).toUpperCase();
        const typeMap = {
            'H': 'Histogram',
            'W': 'Waveform',
            'M': 'Manual',
            'E': 'Event',
            'C': 'Combo',
        };
        return typeMap[lastChar] || (fallback || '—');
    }

    function _sectionHeader(title, sub) {
        return `<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3 mt-5 first:mt-0">
            ${_esc(title)}${sub ? ` <span class="text-xs text-gray-400 normal-case font-normal ml-2">${_esc(sub)}</span>` : ''}
        </h4>`;
    }

    // ── Section renderers ────────────────────────────────────────────

    function _renderEventHeader(s) {
        const ev = s.event || {};
        const bw = s.blastware || {};
        const ts = ev.timestamp ? ev.timestamp.replace('T', ' ').slice(0, 19) : '—';
        const recType = _deriveRecordType(bw.filename || ev.blastware_filename, ev.record_type);
        return `<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-6 gap-y-2 text-sm">
            <div><span class="text-gray-500">Serial</span> <span class="font-mono font-semibold text-seismo-orange ml-1">${_esc(ev.serial)}</span></div>
            <div><span class="text-gray-500">Timestamp</span> <span class="font-medium ml-1">${ts}</span></div>
            <div><span class="text-gray-500">Record Type</span> <span class="font-medium ml-1">${_esc(recType)}</span></div>
            <div><span class="text-gray-500">Sample Rate</span> <span class="font-medium ml-1">${ev.sample_rate ?? '—'} sps</span></div>
            <div><span class="text-gray-500">Rec Time</span> <span class="font-medium ml-1">${ev.rectime_seconds != null ? ev.rectime_seconds + ' s' : '—'}</span></div>
            <div><span class="text-gray-500">Waveform Key</span> <span class="font-mono text-xs ml-1">${_esc(ev.waveform_key || '—')}</span></div>
        </div>`;
    }

    function _renderUserNotes(s) {
        // The "user notes" metadata the operator typed into the BW device.
        // These are the strings the future metadata-driven parser will use.
        // NOTE: SFM's sidecar JSON still names this block `project_info` —
        // we render it as "User Notes" (the actual BW term) but read the
        // field by its SFM-API name.  Rename in SFM is a future cleanup.
        const p = s.project_info || {};
        return `<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-sm">
            <div><span class="text-gray-500">Project</span> <span class="font-medium ml-1">${_esc(p.project || '—')}</span></div>
            <div><span class="text-gray-500">Client</span> <span class="font-medium ml-1">${_esc(p.client || '—')}</span></div>
            <div><span class="text-gray-500">Operator</span> <span class="font-medium ml-1">${_esc(p.operator || '—')}</span></div>
            <div><span class="text-gray-500">Sensor Location</span> <span class="font-medium ml-1">${_esc(p.sensor_location || '—')}</span></div>
        </div>
        <p class="text-xs text-gray-500 dark:text-gray-400 mt-2 italic">
            Values are as typed into the seismograph at session start — not the terra-view project/location assignment.
        </p>`;
    }

    function _renderPeakValues(s) {
        // Prefer bw_report.peaks for richer per-channel data; fall back to peak_values.
        const bwPeaks = (s.bw_report && s.bw_report.peaks) || null;
        const pv = s.peak_values || {};

        const tran  = bwPeaks ? bwPeaks.tran?.ppv_ips : pv.transverse;
        const vert  = bwPeaks ? bwPeaks.vert?.ppv_ips : pv.vertical;
        const lng   = bwPeaks ? bwPeaks.long?.ppv_ips : pv.longitudinal;
        const pvs   = bwPeaks ? bwPeaks.vector_sum?.ips : pv.vector_sum;
        const pvsAt = bwPeaks ? bwPeaks.vector_sum?.time_s : null;

        return `<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
            ${_kvCard('Transverse', `<span class="${_ppvClass(tran)}">${_fmt(tran, 4)}</span>`, { sub: 'in/s' })}
            ${_kvCard('Vertical',   `<span class="${_ppvClass(vert)}">${_fmt(vert, 4)}</span>`, { sub: 'in/s' })}
            ${_kvCard('Longitudinal', `<span class="${_ppvClass(lng)}">${_fmt(lng, 4)}</span>`, { sub: 'in/s' })}
            ${_kvCard('Peak Vector Sum', `<span class="${_ppvClass(pvs)} text-base">${_fmt(pvs, 4)}</span>`, {
                sub: pvsAt != null ? `in/s @ t=${_fmt(pvsAt, 2)}s` : 'in/s',
            })}
        </div>`;
    }

    function _renderMic(s) {
        // Operators only care about dB(L); PSI tile was dropped 2026-05.
        // We still render the row if any mic data is present so ZC freq /
        // time-of-peak stay visible even when bw_report.mic is missing.
        const mic = (s.bw_report && s.bw_report.mic) || null;
        const pv = s.peak_values || {};

        if (!mic && pv.mic_psi == null) return '';

        const dbl  = mic?.pspl_dbl;
        const zcHz = mic?.zc_freq_hz;
        const tPk  = mic?.time_of_peak_s;
        const wt   = mic?.weighting;

        return `<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
            ${_kvCard('Peak Mic dB(L)', _fmt(dbl, 1), { sub: wt || '' })}
            ${_kvCard('ZC Frequency', _fmt(zcHz, 1, 'Hz'))}
            ${_kvCard('Time of Peak', tPk != null ? _fmt(tPk, 2, 's') : '—')}
        </div>`;
    }

    function _sensorRow(label, ch) {
        if (!ch) {
            return `<tr><td class="px-3 py-2 text-sm font-medium">${_esc(label)}</td>
                <td class="px-3 py-2 text-sm text-gray-400" colspan="3">—</td></tr>`;
        }
        const result = ch.result || '—';
        const resultCls = result === 'Passed'
            ? 'text-green-600 dark:text-green-400'
            : (result === 'Failed' ? 'text-red-600 dark:text-red-400 font-semibold' : 'text-gray-500');

        // Geo channels have freq + ratio; mic has freq + amplitude.
        const rightCol = (ch.amplitude_mv != null)
            ? `<td class="px-3 py-2 text-sm font-mono">${_fmt(ch.amplitude_mv, 1, 'mV')}</td>`
            : `<td class="px-3 py-2 text-sm font-mono">${ch.ratio != null ? ch.ratio.toFixed(1) + ' ratio' : '—'}</td>`;

        return `<tr>
            <td class="px-3 py-2 text-sm font-medium">${_esc(label)}</td>
            <td class="px-3 py-2 text-sm font-mono">${_fmt(ch.freq_hz, 1, 'Hz')}</td>
            ${rightCol}
            <td class="px-3 py-2 text-sm ${resultCls}">${_esc(result)}</td>
        </tr>`;
    }

    function _renderSensorCheck(s) {
        const sc = s.bw_report && s.bw_report.sensor_check;
        if (!sc) return '';
        return `<table class="w-full text-left rounded overflow-hidden border border-gray-200 dark:border-gray-700">
            <thead class="bg-gray-50 dark:bg-slate-700">
                <tr>
                    <th class="px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">Channel</th>
                    <th class="px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">Frequency</th>
                    <th class="px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">Amplitude/Ratio</th>
                    <th class="px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">Result</th>
                </tr>
            </thead>
            <tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-slate-800">
                ${_sensorRow('Transverse',   sc.tran)}
                ${_sensorRow('Vertical',     sc.vert)}
                ${_sensorRow('Longitudinal', sc.long)}
                ${_sensorRow('Microphone',   sc.mic)}
            </tbody>
        </table>`;
    }

    function _renderDeviceMetadata(s) {
        const bw  = s.bw_report || {};
        const dev = bw.device || {};
        const rec = bw.recording || {};
        return `<div class="grid grid-cols-2 sm:grid-cols-3 gap-x-6 gap-y-2 text-sm">
            <div><span class="text-gray-500">Firmware</span> <span class="font-mono text-xs ml-1">${_esc(bw.version || '—')}</span></div>
            <div><span class="text-gray-500">Battery</span> <span class="font-medium ml-1">${dev.battery_volts != null ? dev.battery_volts.toFixed(2) + ' V' : '—'}</span></div>
            <div><span class="text-gray-500">Calibrated</span> <span class="font-medium ml-1">${_esc(dev.calibration_date || '—')}${dev.calibration_by ? ' (' + _esc(dev.calibration_by) + ')' : ''}</span></div>
            <div><span class="text-gray-500">Geo Range</span> <span class="font-medium ml-1">${rec.geo_range_ips != null ? rec.geo_range_ips + ' in/s' : '—'}</span></div>
            <div><span class="text-gray-500">Stop Mode</span> <span class="font-medium ml-1">${_esc(rec.stop_mode || '—')}</span></div>
            <div><span class="text-gray-500">Units</span> <span class="font-medium ml-1">${_esc(rec.units || '—')}</span></div>
        </div>`;
    }

    function _renderReview(s, eventId) {
        const rev = s.review || {};
        const ft = !!rev.false_trigger;
        const reviewer = rev.reviewer || '';
        const notes    = rev.notes || '';
        const reviewedAt = rev.reviewed_at
            ? rev.reviewed_at.replace('T', ' ').slice(0, 19)
            : null;
        return `<div class="bg-gray-50 dark:bg-slate-900/40 border border-gray-200 dark:border-slate-700 rounded-lg p-4">
            <div class="flex flex-wrap items-center gap-x-6 gap-y-3">
                <label class="inline-flex items-center gap-2 text-sm cursor-pointer">
                    <input type="checkbox" id="event-review-ft" ${ft ? 'checked' : ''}
                           class="w-4 h-4 rounded text-seismo-orange focus:ring-seismo-orange">
                    <span class="font-medium">Flag as false trigger</span>
                </label>
                <div class="flex items-center gap-2 text-sm flex-1 min-w-[180px]">
                    <label for="event-review-reviewer" class="text-gray-500">Reviewer</label>
                    <input type="text" id="event-review-reviewer" value="${_esc(reviewer)}"
                           placeholder="Initials or name"
                           class="flex-1 px-2 py-1 text-sm bg-white dark:bg-slate-800 border border-gray-300 dark:border-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-seismo-orange">
                </div>
            </div>
            <div class="mt-3">
                <label for="event-review-notes" class="block text-xs text-gray-500 mb-1">Notes</label>
                <textarea id="event-review-notes" rows="2"
                          placeholder="Optional context — what caused the FT, follow-up actions, etc."
                          class="w-full px-2 py-1 text-sm bg-white dark:bg-slate-800 border border-gray-300 dark:border-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-seismo-orange">${_esc(notes)}</textarea>
            </div>
            <div class="flex items-center justify-between gap-3 mt-3">
                <span id="event-review-status" class="text-xs text-gray-500 dark:text-gray-400">
                    ${reviewedAt ? `Last reviewed ${reviewedAt}` : 'Not yet reviewed.'}
                </span>
                <button type="button"
                        onclick="window.saveEventReview('${_esc(eventId)}')"
                        class="px-4 py-1.5 bg-seismo-orange hover:bg-seismo-navy text-white rounded-lg text-sm font-medium transition-colors">
                    Save
                </button>
            </div>
        </div>`;
    }

    // ── Waveform / histogram chart helpers ──────────────────────────

    async function _loadMicUnitPref() {
        if (_micUnitPrefLoaded) return _micUnitPref;
        try {
            const r = await fetch('/api/settings/preferences');
            if (r.ok) {
                const prefs = await r.json();
                _micUnitPref = prefs.mic_unit_pref === 'dBL' ? 'dBL' : 'psi';
            }
        } catch (e) {
            // Network error → silent fall back to default 'psi'.
        }
        _micUnitPrefLoaded = true;
        return _micUnitPref;
    }

    function _psiToDbl(psi) {
        if (psi == null || !(psi > 0)) return null;
        return 20 * Math.log10(psi / DBL_REF);
    }

    // Rectifying psi→dBL converter for per-sample values — see comments in
    // sfm_webapp.html:2592-2607 for the floor rationale.
    function _psiToDblForChart(psi) {
        if (psi == null) return MIC_DBL_FLOOR;
        const a = Math.abs(psi);
        if (a === 0) return MIC_DBL_FLOOR;
        const dbl = 20 * Math.log10(a / DBL_REF);
        return dbl > MIC_DBL_FLOOR ? dbl : MIC_DBL_FLOOR;
    }

    // Adaptive decimal formatter — sensible precision in the normal range,
    // scientific notation only at the extremes.
    function _fmtPeak(v, unit) {
        if (v == null || (typeof v === 'number' && !isFinite(v))) return '';
        if (typeof v !== 'number') return String(v) + (unit ? ' ' + unit : '');
        if (v === 0) return '0' + (unit ? ' ' + unit : '');
        const a = Math.abs(v);
        const u = unit ? ' ' + unit : '';
        if (a >= 0.0001 && a < 10000) {
            const d = a >= 100 ? 1 : a >= 10 ? 2 : a >= 1 ? 3 : a >= 0.1 ? 4 : 5;
            return v.toFixed(d) + u;
        }
        return v.toExponential(2) + u;
    }

    function _destroyCharts() {
        Object.values(_charts).forEach(c => { try { c.destroy(); } catch (e) { /* noop */ } });
        _charts = {};
    }

    // Returns true when Tailwind dark mode is active (the `dark` class is
    // toggled on <html> by Terra-View's theme handler).  Drives chart grid
    // + tick colors so they have contrast on both backgrounds.
    function _isDark() {
        return document.documentElement.classList.contains('dark');
    }

    function _renderWaveformInto(containerId, data, micUnit) {
        const container = document.getElementById(containerId);
        if (!container) return;
        container.innerHTML = '';
        _destroyCharts();

        const channels = data.channels || {};
        const ta       = data.time_axis || {};
        const sr       = ta.sample_rate || 1024;
        const dtMs     = ta.dt_ms || (1000.0 / sr);
        const t0Ms     = ta.t0_ms != null ? ta.t0_ms : 0;
        const isHistogram = String(data.record_type || '').toLowerCase().includes('histogram');

        const withData = _CHANNEL_ORDER.filter(ch =>
            channels[ch] && (channels[ch].values || []).length > 0
        );
        const lastCh = withData[withData.length - 1];

        // Theme-aware chart colors.  Tailwind dark uses bg-slate-800 (~#1e293b);
        // light is white.  Grids + ticks need contrast on both.
        const dark = _isDark();
        const gridColor = dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)';
        const tickColor = dark ? '#94a3b8' : '#64748b';

        if (withData.length === 0) {
            container.innerHTML = `<div class="text-sm text-gray-500 dark:text-gray-400 italic py-6 text-center">
                No waveform samples decoded — codec walker returned 0 valid blocks for this event.
            </div>`;
            return;
        }

        for (const ch of _CHANNEL_ORDER) {
            const chData = channels[ch];
            if (!chData) continue;
            let values = chData.values || [];
            let chUnit = chData.unit || '';
            let chPeak = chData.peak;

            // Mic: convert psi → dBL when the user pref is dBL (default).
            if (ch === 'MicL' && chUnit === 'psi' && micUnit === 'dBL') {
                values = values.map(_psiToDblForChart);
                chPeak = _psiToDbl(chPeak);
                chUnit = 'dB(L)';
            }

            const wrap = document.createElement('div');
            wrap.className = 'bg-gray-50 dark:bg-slate-900/40 border border-gray-200 dark:border-slate-700 rounded-md px-3 pr-8 pb-1 pt-1 mb-1';

            const lbl = document.createElement('div');
            lbl.className = 'text-[10px] font-semibold uppercase tracking-wider mb-0.5 flex justify-between items-baseline';
            lbl.style.color = _CHANNEL_COLORS[ch];
            const peakStr = chPeak != null ? `peak ${_fmtPeak(chPeak, chUnit)}` : '';
            lbl.innerHTML = `<span>${ch}</span><span class="text-gray-500 dark:text-gray-400 font-normal">${peakStr}</span>`;
            wrap.appendChild(lbl);

            if (values.length === 0) {
                const e = document.createElement('div');
                e.className = 'h-20 flex items-center justify-center text-xs text-gray-400 italic';
                e.textContent = 'no samples decoded';
                wrap.appendChild(e);
                container.appendChild(wrap);
                continue;
            }

            const canvasWrap = document.createElement('div');
            canvasWrap.className = 'relative';
            canvasWrap.style.height = '100px';
            const canvas = document.createElement('canvas');
            canvasWrap.appendChild(canvas);
            wrap.appendChild(canvasWrap);
            container.appendChild(wrap);

            // X-axis: waveforms use ms-relative-to-trigger; histograms use
            // the BW-reported interval timestamps (HH:MM:SS) when the server
            // aggregated to BW intervals, else interval index.
            let times;
            if (isHistogram) {
                const intervalTimes = ta.interval_times || [];
                times = (intervalTimes.length === values.length)
                    ? intervalTimes
                    : values.map((_, i) => i + 1);
            } else {
                times = values.map((_, i) => t0Ms + i * dtMs);
            }

            // Downsample for rendering when very long.
            const MAX = 3000;
            let rT = times, rV = values;
            if (values.length > MAX) {
                const step = Math.ceil(values.length / MAX);
                rT = times.filter((_, i) => i % step === 0);
                rV = values.filter((_, i) => i % step === 0);
            }
            const showX = (ch === lastCh);

            const xAxisLabel = isHistogram ? '' : ' ms';
            const fmtTick = i => {
                const v = rT[i];
                if (typeof v === 'number') {
                    const s = Number.isInteger(v) ? String(v) : v.toFixed(1);
                    return s + xAxisLabel;
                }
                return String(v) + xAxisLabel;
            };

            // Y-axis bounds — see sfm_webapp.html:2744-2786 for the rationale.
            let yBounds = {};
            const isGeo = ch !== 'MicL';
            if (isGeo && !isHistogram) {
                let absMax = 0;
                for (const v of values) {
                    const a = Math.abs(v);
                    if (a > absMax) absMax = a;
                }
                const padded = (absMax || 1) * 1.10;
                yBounds = { min: -padded, max: padded };
            } else if (isGeo && isHistogram) {
                const HIST_GEO_MIN_INS = 0.05;
                let peak = 0;
                for (const v of values) { const a = Math.abs(v); if (a > peak) peak = a; }
                yBounds = { min: 0, max: Math.max(peak * 1.10, HIST_GEO_MIN_INS) };
            } else if (ch === 'MicL' && micUnit === 'dBL') {
                const peakDbl = (typeof chPeak === 'number' && isFinite(chPeak))
                    ? chPeak + 5 : 100;
                yBounds = { min: MIC_DBL_FLOOR, max: Math.max(peakDbl, MIC_DBL_FLOOR + 20) };
            } else if (ch === 'MicL' && isHistogram && micUnit === 'psi') {
                const HIST_MIC_MIN_PSI = 0.001;
                let peak = 0;
                for (const v of values) { const a = Math.abs(v); if (a > peak) peak = a; }
                yBounds = { min: 0, max: Math.max(peak * 1.10, HIST_MIC_MIN_PSI) };
            }

            _charts[ch] = new Chart(canvas, {
                type: isHistogram ? 'bar' : 'line',
                data: {
                    labels: rT.map(t => (typeof t === 'number' ? (Number.isInteger(t) ? String(t) : t.toFixed(2)) : t)),
                    datasets: isHistogram ? [{
                        data: rV,
                        backgroundColor: _CHANNEL_COLORS[ch],
                        borderWidth: 0,
                        barPercentage: 1.0,
                        categoryPercentage: 1.0,
                    }] : [{
                        data: rV,
                        borderColor: _CHANNEL_COLORS[ch],
                        borderWidth: 1,
                        pointRadius: 0,
                        tension: 0,
                    }],
                },
                options: {
                    animation: false, responsive: true, maintainAspectRatio: false,
                    plugins: {
                        legend: { display: false },
                        tooltip: {
                            mode: 'index', intersect: false,
                            callbacks: {
                                title: items => isHistogram
                                    ? `interval ${items[0].label}`
                                    : `t = ${items[0].label} ms`,
                                label: item => `${ch}: ${_fmtPeak(item.raw, chUnit)}`,
                            },
                        },
                    },
                    scales: {
                        x: {
                            type: 'category', display: showX,
                            ticks: { color: tickColor, maxTicksLimit: 8, maxRotation: 0, callback: (v, i) => fmtTick(i) },
                            grid:  { color: gridColor, drawTicks: showX },
                        },
                        y: {
                            ...yBounds,
                            ticks: { color: tickColor, maxTicksLimit: 4 },
                            grid:  { color: gridColor },
                            title: { display: true, text: chUnit, color: tickColor, font: { size: 9 } },
                        },
                    },
                },
                plugins: isHistogram ? [] : [{
                    id: 'overlays',
                    afterDraw(chart) {
                        const ctx = chart.ctx, x = chart.scales.x, y = chart.scales.y;
                        const zi = rT.findIndex(t => parseFloat(t) >= 0);
                        if (zi >= 0) {
                            const px = x.getPixelForValue(zi);
                            ctx.save();
                            ctx.beginPath(); ctx.moveTo(px, y.top); ctx.lineTo(px, y.bottom);
                            ctx.strokeStyle = 'rgba(239,68,68,0.8)'; ctx.lineWidth = 1.2;
                            ctx.setLineDash([4, 3]); ctx.stroke(); ctx.restore();
                            ctx.save();
                            ctx.fillStyle = '#ef4444';
                            ctx.beginPath();
                            ctx.moveTo(px - 4, y.top - 7); ctx.lineTo(px + 4, y.top - 7); ctx.lineTo(px, y.top - 1);
                            ctx.closePath(); ctx.fill();
                            ctx.beginPath();
                            ctx.moveTo(px - 4, y.bottom + 7); ctx.lineTo(px + 4, y.bottom + 7); ctx.lineTo(px, y.bottom + 1);
                            ctx.closePath(); ctx.fill();
                            ctx.restore();
                        }
                        const zy = y.getPixelForValue(0);
                        if (zy >= y.top && zy <= y.bottom) {
                            ctx.save();
                            ctx.strokeStyle = gridColor; ctx.lineWidth = 0.8;
                            ctx.setLineDash([2, 2]);
                            ctx.beginPath(); ctx.moveTo(x.left, zy); ctx.lineTo(x.right, zy); ctx.stroke();
                            ctx.restore();
                            ctx.save();
                            ctx.fillStyle = tickColor; ctx.font = '10px monospace';
                            ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
                            ctx.fillText('0.0', x.right + 6, zy);
                            ctx.restore();
                        }
                    },
                }],
            });
        }
    }

    function _renderFileInfo(s, eventId) {
        const bw  = s.blastware || {};
        const src = s.source || {};
        const sizeKb = bw.filesize ? (bw.filesize / 1024).toFixed(1) : null;
        const canDownloadBinary = !!(bw.available && bw.filename && eventId);
        const txtFilename = src && src.txt_filename;
        const reportPdfUrl  = `/api/sfm/db/events/${encodeURIComponent(eventId)}/report.pdf`;
        const reportTxtUrl  = `/api/sfm/db/events/${encodeURIComponent(eventId)}/ascii_report.txt`;

        const downloadButtons = `
            <div class="flex flex-wrap gap-2 mb-4">
                <button type="button"
                        onclick="window.toggleEventPdfPreview()"
                        class="inline-flex items-center gap-2 px-4 py-2 bg-seismo-orange hover:bg-seismo-navy text-white rounded-lg text-sm font-medium transition-colors">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
                    </svg>
                    <span id="event-pdf-toggle-label">Show Event Report PDF</span>
                </button>
                <a href="${reportPdfUrl}" download
                   class="inline-flex items-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg text-sm transition-colors">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
                    </svg>
                    Download PDF
                </a>
                ${canDownloadBinary ? `
                    <a href="/api/sfm/db/events/${encodeURIComponent(eventId)}/blastware_file"
                       download="${_esc(bw.filename)}"
                       class="inline-flex items-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg text-sm transition-colors">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
                        </svg>
                        Blastware binary
                        <span class="text-xs opacity-60 ml-1">${sizeKb ? `(${sizeKb} KB)` : ''}</span>
                    </a>
                ` : ''}
                ${txtFilename ? `
                    <a href="${reportTxtUrl}" download="${_esc(txtFilename)}"
                       class="inline-flex items-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg text-sm transition-colors">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
                        </svg>
                        Original .TXT report
                    </a>
                ` : ''}
                <button type="button"
                        onclick="window.toggleEventJsonViewer()"
                        class="inline-flex items-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg text-sm transition-colors">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
                    </svg>
                    <span id="event-json-toggle-label">View JSON</span>
                </button>
                <a href="/api/sfm/db/events/${encodeURIComponent(eventId)}/sidecar"
                   download="${_esc((bw.filename || 'event') + '.sfm.json')}"
                   class="inline-flex items-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg text-sm transition-colors">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
                    </svg>
                    Download sidecar JSON
                </a>
            </div>
            <div id="event-pdf-preview" class="hidden mb-4 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-gray-50 dark:bg-slate-900">
                <iframe id="event-pdf-iframe" title="Event Report PDF preview"
                        class="w-full" style="height:80vh; min-height:600px; border:0;"
                        data-pdf-url="${reportPdfUrl}"></iframe>
            </div>
            <div id="event-json-viewer" class="hidden mb-4">
                <div class="flex items-center justify-between mb-2">
                    <span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Sidecar JSON</span>
                    <button type="button" onclick="window.copyEventJson()"
                            class="text-xs text-seismo-orange hover:text-seismo-navy">
                        <span id="event-json-copy-label">Copy</span>
                    </button>
                </div>
                <pre id="event-json-pre" class="bg-gray-900 dark:bg-black text-gray-200 font-mono text-xs p-4 rounded-lg max-h-96 overflow-auto whitespace-pre">${_esc(JSON.stringify(s, null, 2))}</pre>
            </div>
        `;

        return `${downloadButtons}
        <div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-sm">
            <div class="sm:col-span-2"><span class="text-gray-500">Blastware file</span> <span class="font-mono text-xs ml-1">${_esc(bw.filename || '—')}</span> ${sizeKb ? `<span class="text-xs text-gray-500 ml-2">(${sizeKb} KB)</span>` : ''}</div>
            <div class="sm:col-span-2"><span class="text-gray-500">SHA-256</span> <span class="font-mono text-xs ml-1 break-all">${_esc(bw.sha256 || '—')}</span></div>
            <div title="When SFM received and stored this event — NOT the unit-local trigger time (see Timestamp at the top of the modal for that).">
                <span class="text-gray-500">Time received</span> <span class="font-medium ml-1">${_esc(src.captured_at ? src.captured_at.slice(0, 19).replace('T', ' ') : '—')}</span>
            </div>
            <div><span class="text-gray-500">Tool version</span> <span class="font-mono text-xs ml-1">${_esc(src.tool_version || '—')}</span></div>
        </div>`;
    }

    // ── Public API ───────────────────────────────────────────────────

    window.showEventDetail = async function (eventId) {
        const modal = document.getElementById(MODAL_ID);
        if (!modal) {
            console.warn('event-modal: include event_detail_modal.html partial on this page.');
            return;
        }
        modal.classList.remove('hidden');
        document.getElementById(MODAL_ID + '-title').textContent = 'Event Detail';
        document.getElementById(MODAL_ID + '-content').innerHTML = `
            <div class="text-center py-12 text-gray-500 dark:text-gray-400">
                <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-seismo-orange mx-auto mb-3"></div>
                Loading event detail…
            </div>`;

        let s;
        try {
            const r = await fetch(`/api/sfm/db/events/${encodeURIComponent(eventId)}/sidecar`);
            if (!r.ok) {
                throw new Error('HTTP ' + r.status + ' fetching sidecar');
            }
            s = await r.json();
        } catch (e) {
            document.getElementById(MODAL_ID + '-content').innerHTML = `
                <div class="text-center py-8 text-red-500 text-sm">
                    Failed to load event detail: ${_esc(e.message)}
                </div>`;
            return;
        }

        const ev = s.event || {};
        const ts = ev.timestamp ? ev.timestamp.replace('T', ' ').slice(0, 19) : '';
        document.getElementById(MODAL_ID + '-title').textContent =
            `Event — ${ev.serial || '?'} @ ${ts}`;

        const hasReport = !!s.bw_report;
        const reportNote = hasReport
            ? ''
            : `<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3 text-sm text-amber-800 dark:text-amber-300 mb-4">
                   <strong>No BW ASCII report paired with this event.</strong>
                   Older events forwarded before the watcher's <code class="font-mono text-xs">_ASCII.TXT</code> pairing fix landed lack this data.
                   PPV is still available from the binary event file.
               </div>`;

        document.getElementById(MODAL_ID + '-content').innerHTML = `
            ${reportNote}

            ${_sectionHeader('Event')}
            ${_renderEventHeader(s)}

            ${_sectionHeader('User Notes')}
            ${_renderUserNotes(s)}

            ${_sectionHeader('Peak Particle Velocity')}
            ${_renderPeakValues(s)}

            ${_sectionHeader('Waveform')}
            <div id="event-waveform-status" class="text-xs text-gray-500 dark:text-gray-400 italic mb-2">Loading waveform…</div>
            <div id="event-waveform-charts" class="space-y-0.5"></div>

            ${(s.bw_report && (s.bw_report.mic || s.peak_values?.mic_psi != null)) ? `
                ${_sectionHeader('Microphone')}
                ${_renderMic(s)}
            ` : ''}

            ${hasReport ? `
                ${_sectionHeader('Sensor Self-Check')}
                ${_renderSensorCheck(s)}

                ${_sectionHeader('Device & Recording Metadata')}
                ${_renderDeviceMetadata(s)}
            ` : ''}

            ${_sectionHeader('Review')}
            ${_renderReview(s, eventId)}

            ${_sectionHeader('Source File')}
            ${_renderFileInfo(s, eventId)}
        `;

        // Waveform load runs after the sidecar content is in the DOM, in
        // parallel with the mic-unit-pref fetch.  Either may complete first.
        try {
            const [wfRes, micUnit] = await Promise.all([
                fetch(`/api/sfm/db/events/${encodeURIComponent(eventId)}/waveform.json`),
                _loadMicUnitPref(),
            ]);
            if (wfRes.status === 404) {
                document.getElementById('event-waveform-status').textContent =
                    'No waveform data — codec returned 0 valid blocks for this event.';
                return;
            }
            if (!wfRes.ok) {
                document.getElementById('event-waveform-status').textContent =
                    'Failed to load waveform: HTTP ' + wfRes.status;
                return;
            }
            const wfData = await wfRes.json();
            document.getElementById('event-waveform-status').textContent = '';
            _renderWaveformInto('event-waveform-charts', wfData, micUnit);
        } catch (e) {
            const st = document.getElementById('event-waveform-status');
            if (st) st.textContent = 'Waveform fetch failed: ' + _esc(e.message);
        }
    };

    window.closeEventDetailModal = function () {
        const modal = document.getElementById(MODAL_ID);
        if (modal) modal.classList.add('hidden');
        _destroyCharts();
    };

    window.toggleEventJsonViewer = function () {
        const viewer = document.getElementById('event-json-viewer');
        const label = document.getElementById('event-json-toggle-label');
        if (!viewer) return;
        const isHidden = viewer.classList.toggle('hidden');
        if (label) label.textContent = isHidden ? 'View JSON' : 'Hide JSON';
    };

    window.toggleEventPdfPreview = function () {
        const preview = document.getElementById('event-pdf-preview');
        const iframe  = document.getElementById('event-pdf-iframe');
        const label   = document.getElementById('event-pdf-toggle-label');
        if (!preview || !iframe) return;
        const isHidden = preview.classList.toggle('hidden');
        // Lazy-load the PDF: only set the iframe src on first reveal, so
        // closing the event modal without opening the PDF never spends
        // bandwidth on it.
        if (!isHidden && !iframe.src) {
            iframe.src = iframe.dataset.pdfUrl || '';
        }
        if (label) label.textContent = isHidden ? 'Show Event Report PDF' : 'Hide Event Report PDF';
        // Scroll the iframe into view on first reveal so the operator
        // doesn't have to hunt for it after clicking.
        if (!isHidden) {
            preview.scrollIntoView({ behavior: 'smooth', block: 'start' });
        }
    };

    window.saveEventReview = async function (eventId) {
        const ft       = document.getElementById('event-review-ft');
        const reviewer = document.getElementById('event-review-reviewer');
        const notes    = document.getElementById('event-review-notes');
        const status   = document.getElementById('event-review-status');
        if (!ft || !reviewer || !notes) return;

        const payload = {
            review: {
                false_trigger: ft.checked,
                reviewer:      reviewer.value.trim() || null,
                notes:         notes.value.trim() || null,
            }
        };
        if (status) {
            status.textContent = 'Saving…';
            status.className = 'text-xs text-gray-500 dark:text-gray-400';
        }
        try {
            const r = await fetch(`/api/sfm/db/events/${encodeURIComponent(eventId)}/sidecar`, {
                method: 'PATCH',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload),
            });
            if (!r.ok) {
                const t = await r.text().catch(() => '');
                throw new Error('HTTP ' + r.status + (t ? ` — ${t.slice(0, 120)}` : ''));
            }
            if (status) {
                status.textContent = 'Saved.';
                status.className = 'text-xs text-green-600 dark:text-green-400';
            }
            // Notify the host page so its event-list FT badge / row state
            // can refresh.  Pages opt in by listening for this event.
            window.dispatchEvent(new CustomEvent('sfm-event-review-saved', {
                detail: { eventId, review: payload.review },
            }));
        } catch (e) {
            if (status) {
                status.textContent = 'Save failed: ' + e.message;
                status.className = 'text-xs text-red-600 dark:text-red-400';
            }
        }
    };

    window.copyEventJson = function () {
        const pre = document.getElementById('event-json-pre');
        const label = document.getElementById('event-json-copy-label');
        if (!pre) return;
        navigator.clipboard.writeText(pre.textContent).then(() => {
            if (label) {
                label.textContent = 'Copied!';
                setTimeout(() => { label.textContent = 'Copy'; }, 1500);
            }
        }).catch(err => {
            console.error('clipboard write failed', err);
            if (label) {
                label.textContent = 'Failed';
                setTimeout(() => { label.textContent = 'Copy'; }, 1500);
            }
        });
    };

    // Close on Escape.
    document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape') window.closeEventDetailModal();
    });
})();
