#!/usr/bin/env python3
"""
Transcription Review Dashboard v2
===================================

Features:
  - Audio player + waveform visualizer per segment (wavesurfer.js)
  - Segment numbering (SEG_0000, SEG_0001, ...)
  - Filter by status (accept/review/retry/reject)
  - Detailed view: large waveform + start/end trim sliders + timeline
  - Re-transcribe trimmed audio with history (old vs new diff)
  - Keyboard navigation (J/K/Space)

Usage:
    cd /home/ubuntu/maya3_transcribe && source venv/bin/activate
    python bin/dashboard.py [--port 8765] [--data consistency_test/full_run.json]
"""
import io
import os
import sys
import json
import argparse
import mimetypes
import tempfile
from pathlib import Path
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs

PROJECT_ROOT = Path(__file__).parent.parent
DEFAULT_SEG_DIR = "/tmp/maya3_transcribe/pF_BQpHaIdU/extracted/pF_BQpHaIdU/segments"

# Lazy-load heavy imports only when re-transcription is requested
_transcriber = None
_config = None


def _get_transcriber():
    global _transcriber, _config
    if _transcriber is None:
        sys.path.insert(0, str(PROJECT_ROOT))
        from src.backend.gemini_transcriber import GeminiTranscriber, TranscriptionConfig
        from src.validators import validate_transcription
        _transcriber = GeminiTranscriber()
        _config = TranscriptionConfig(
            model="gemini-3-flash-preview", thinking_level="low",
            temperature=0.0, language="Telugu"
        )
    return _transcriber, _config


def load_data(data_path: str, seg_dir: str) -> dict:
    """Load transcription data from JSON."""
    with open(data_path, "r", encoding="utf-8") as f:
        raw = json.load(f)

    segments = []

    if "segments" in raw and isinstance(raw["segments"], list):
        # full_run.py format: {meta, segments: [{seg_num, ...}]}
        for i, s in enumerate(raw["segments"]):
            seg_name = s.get("original_file", s.get("id", f"seg_{i}"))
            audio_file = s.get("audio_file", seg_name)
            # Resolve audio path: try polished dir first, then original
            audio_path = s.get("audio_path", "")
            if not audio_path or not os.path.exists(audio_path):
                audio_path = os.path.join(seg_dir, audio_file)
            if not os.path.exists(audio_path):
                audio_path = os.path.join(seg_dir, seg_name)

            segments.append({
                "idx": i,
                "seg_num": s.get("seg_num", f"SEG_{i:04d}"),
                "id": seg_name,
                "audio_file": audio_file,
                "audio_path": audio_path,
                "audio_exists": os.path.exists(audio_path),
                "audio_url": f"/audio?path={audio_path}",
                "duration_sec": s.get("duration_sec", 0),
                "chunk_index": s.get("chunk_index", 0),
                "total_chunks": s.get("total_chunks", 1),
                "transcription": s.get("transcription", ""),
                "tagged": s.get("tagged", ""),
                "romanized": s.get("romanized", ""),
                "uroman": s.get("uroman", ""),
                "detected_language": s.get("detected_language", ""),
                "speaker": s.get("speaker", {}),
                "native_ctc": s.get("native_ctc", 0),
                "roman_mms": s.get("roman_mms", 0),
                "combined": s.get("combined", 0),
                "status": s.get("status", "unknown"),
                "error": s.get("error", ""),
                "history": [],
            })

    elif "runs" in raw:
        # consistency test format
        runs = raw["runs"]
        all_segs = set()
        for run in runs:
            all_segs.update(run.keys())
        for i, seg_name in enumerate(sorted(all_segs)):
            r0 = runs[0].get(seg_name, {})
            audio_path = os.path.join(seg_dir, seg_name)
            segments.append({
                "idx": i,
                "seg_num": f"SEG_{i:04d}",
                "id": seg_name,
                "audio_file": seg_name,
                "audio_path": audio_path,
                "audio_exists": os.path.exists(audio_path),
                "audio_url": f"/audio?path={audio_path}",
                "duration_sec": 0,
                "chunk_index": 0, "total_chunks": 1,
                "transcription": r0.get("transcription", ""),
                "tagged": "", "romanized": r0.get("romanized", ""),
                "uroman": "", "detected_language": r0.get("detected_language", ""),
                "speaker": {},
                "native_ctc": r0.get("native_ctc", 0),
                "roman_mms": r0.get("roman_mms", 0),
                "combined": r0.get("combined", 0),
                "status": r0.get("status", "unknown"),
                "error": "",
                "history": [],
                "runs": [runs[ri].get(seg_name, {}) for ri in range(len(runs))],
            })

    return {
        "segments": segments,
        "meta": raw.get("meta", {}),
        "source": str(data_path),
    }


def retranscribe_trimmed(audio_path: str, start_sec: float, end_sec: float,
                         language: str = "Telugu") -> dict:
    """Trim audio to [start_sec, end_sec] and re-transcribe."""
    import soundfile as sf
    import numpy as np

    audio, sr = sf.read(audio_path)
    if len(audio.shape) > 1:
        audio = audio.mean(axis=1)

    ss = int(start_sec * sr)
    es = int(end_sec * sr)
    trimmed = audio[max(0, ss):min(len(audio), es)]

    # Write to temp file
    tmp = tempfile.NamedTemporaryFile(suffix=".flac", delete=False)
    sf.write(tmp.name, trimmed, sr)
    tmp.close()

    try:
        transcriber, config = _get_transcriber()
        raw = transcriber.transcribe_audio(tmp.name, config)

        if raw.get("error"):
            return {"error": raw["error"]}

        native = raw.get("transcription", "")
        lang_code_map = {"Telugu": "te", "Hindi": "hi", "Tamil": "ta", "English": "en"}
        lang_code = lang_code_map.get(language, "te")

        from src.validators import validate_transcription
        val = validate_transcription(tmp.name, native, language=lang_code,
                                     duration_sec=end_sec - start_sec)

        return {
            "transcription": native,
            "tagged": raw.get("tagged", ""),
            "romanized": raw.get("romanized", ""),
            "detected_language": raw.get("detected_language", ""),
            "native_ctc": val.native_ctc_score,
            "roman_mms": val.roman_mms_score,
            "combined": val.combined_score,
            "status": val.status,
            "trim_start": start_sec,
            "trim_end": end_sec,
            "trim_duration": round(end_sec - start_sec, 2),
        }
    finally:
        os.unlink(tmp.name)


class DashboardHandler(BaseHTTPRequestHandler):
    data = {}
    seg_dir = ""
    data_path = ""

    def do_GET(self):
        parsed = urlparse(self.path)
        path = parsed.path
        params = parse_qs(parsed.query)

        if path == "/" or path == "/index.html":
            self._serve_html()
        elif path == "/api/data":
            self._json_response(self.data)
        elif path == "/audio":
            audio_path = params.get("path", [""])[0]
            self._serve_audio(audio_path)
        elif path == "/api/reload":
            self.__class__.data = load_data(self.data_path, self.seg_dir)
            self._json_response({"ok": True, "segments": len(self.data["segments"])})
        else:
            self.send_error(404)

    def do_POST(self):
        parsed = urlparse(self.path)
        if parsed.path == "/api/retranscribe":
            length = int(self.headers.get("Content-Length", 0))
            body = json.loads(self.rfile.read(length))
            audio_path = body.get("audio_path", "")
            start = body.get("start_sec", 0)
            end = body.get("end_sec", 0)
            lang = body.get("language", "Telugu")

            if not audio_path or not os.path.exists(audio_path):
                self._json_response({"error": "Audio not found"}, status=404)
                return

            result = retranscribe_trimmed(audio_path, start, end, lang)
            # Store in history
            for seg in self.data.get("segments", []):
                if seg.get("audio_path") == audio_path:
                    seg.setdefault("history", []).append(result)
                    break
            self._json_response(result)
        else:
            self.send_error(404)

    def _serve_html(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.end_headers()
        self.wfile.write(DASHBOARD_HTML.encode("utf-8"))

    def _json_response(self, data, status=200):
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))

    def _serve_audio(self, audio_path):
        if not audio_path or not os.path.exists(audio_path):
            self.send_error(404, "Audio not found")
            return
        mime = mimetypes.guess_type(audio_path)[0] or "audio/flac"
        size = os.path.getsize(audio_path)
        self.send_response(200)
        self.send_header("Content-Type", mime)
        self.send_header("Content-Length", str(size))
        self.send_header("Accept-Ranges", "bytes")
        self.end_headers()
        with open(audio_path, "rb") as f:
            self.wfile.write(f.read())

    def log_message(self, format, *args):
        msg = str(args[0]) if args else ""
        if "/api/" not in msg and "/audio" not in msg:
            super().log_message(format, *args)


DASHBOARD_HTML = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcription Review Dashboard</title>
<script src="https://unpkg.com/wavesurfer.js@7"></script>
<style>
:root {
  --bg: #0f1117; --surface: #1a1d27; --surface2: #242836; --surface3: #2d3145;
  --border: #2e3348; --text: #e1e4ed; --text2: #8b90a5; --text3: #5a5f78;
  --accept: #22c55e; --review: #f59e0b; --retry: #3b82f6; --reject: #ef4444;
  --accent: #818cf8; --accent2: #6366f1;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family: -apple-system, 'Segoe UI', system-ui, sans-serif;
       background: var(--bg); color: var(--text); line-height: 1.5; font-size: 14px; }

.header { padding: 16px 24px; border-bottom: 1px solid var(--border);
          display: flex; justify-content: space-between; align-items: center;
          position: sticky; top: 0; background: var(--bg); z-index: 100; }
.header h1 { font-size: 15px; font-weight: 600; letter-spacing: -0.3px; }
.stats { display: flex; gap: 10px; font-size: 12px; color: var(--text2); align-items: center; }
.stat-pill { padding: 3px 10px; border-radius: 10px; background: var(--surface2); }
.reload-btn { padding: 4px 12px; border-radius: 6px; border: 1px solid var(--border);
              background: var(--surface); color: var(--accent); cursor: pointer;
              font-size: 11px; font-family: inherit; }
.reload-btn:hover { background: var(--surface2); }

.filters { padding: 10px 24px; border-bottom: 1px solid var(--border);
           display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.filter-btn { padding: 4px 12px; border-radius: 5px; border: 1px solid var(--border);
              background: var(--surface); color: var(--text2); cursor: pointer;
              font-size: 12px; font-family: inherit; transition: all 0.12s; }
.filter-btn:hover { border-color: var(--accent); color: var(--text); }
.filter-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.search { padding: 5px 12px; border-radius: 5px; border: 1px solid var(--border);
          background: var(--surface); color: var(--text); font-size: 12px;
          font-family: inherit; width: 200px; margin-left: auto; outline: none; }
.search:focus { border-color: var(--accent); }
.kbd { font-size: 10px; color: var(--text3); margin-left: 8px; }
kbd { padding: 1px 5px; background: var(--surface2); border: 1px solid var(--border);
      border-radius: 3px; font-size: 10px; }

.segments { padding: 12px 24px; display: flex; flex-direction: column; gap: 8px; }

.seg { background: var(--surface); border: 1px solid var(--border);
       border-radius: 8px; overflow: hidden; transition: border-color 0.12s; }
.seg:hover { border-color: #3d4260; }
.seg.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

.seg-top { padding: 10px 14px; display: flex; align-items: center; gap: 12px;
           cursor: pointer; user-select: none; }
.seg-num { font-size: 11px; font-weight: 700; color: var(--accent); min-width: 65px;
           font-family: 'SF Mono', 'Fira Code', monospace; }
.seg-name { font-size: 11px; color: var(--text3); flex: 1; overflow: hidden;
            text-overflow: ellipsis; white-space: nowrap; }
.seg-dur { font-size: 11px; color: var(--text2); min-width: 40px; text-align: right; }
.seg-scores { display: flex; gap: 8px; align-items: center; }
.score-chip { font-size: 11px; font-weight: 600; padding: 1px 6px; border-radius: 3px; }
.badge { padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 700;
         text-transform: uppercase; letter-spacing: 0.5px; }
.badge-accept { background: rgba(34,197,94,0.15); color: var(--accept); }
.badge-review { background: rgba(245,158,11,0.15); color: var(--review); }
.badge-retry { background: rgba(59,130,246,0.15); color: var(--retry); }
.badge-reject { background: rgba(239,68,68,0.15); color: var(--reject); }
.badge-error { background: rgba(239,68,68,0.15); color: var(--reject); }
.badge-unknown { background: var(--surface2); color: var(--text3); }
.badge-unvalidated { background: var(--surface2); color: var(--text3); }

.seg-expanded { display: none; border-top: 1px solid var(--border); }
.seg.open .seg-expanded { display: block; }

.seg-body { padding: 14px; display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 1000px) { .seg-body { grid-template-columns: 1fr; } }

.waveform-container { background: var(--surface2); border-radius: 6px; padding: 8px;
                      min-height: 80px; position: relative; }
.waveform-mini { height: 64px; }
.waveform-time { position: absolute; bottom: 2px; right: 8px; font-size: 10px;
                 color: var(--text3); font-family: monospace; }

.scores-row { display: flex; gap: 16px; margin-top: 8px; }
.score-block { text-align: center; }
.score-val { font-size: 20px; font-weight: 700; }
.score-lbl { font-size: 9px; text-transform: uppercase; color: var(--text3);
             letter-spacing: 0.5px; }
.score-bar { width: 50px; height: 3px; background: var(--surface3); border-radius: 2px;
             margin: 3px auto 0; overflow: hidden; }
.score-fill { height: 100%; border-radius: 2px; }

.tx-section { margin-top: 8px; }
.tx-label { font-size: 10px; text-transform: uppercase; color: var(--text3);
            letter-spacing: 0.5px; margin-bottom: 3px; }
.tx-block { background: var(--surface2); padding: 10px 12px; border-radius: 6px;
            line-height: 1.8; }
.tx-native { font-size: 15px; }
.tx-secondary { font-size: 12px; color: var(--text2); margin-top: 6px; }
.tx-lang { font-size: 11px; color: var(--accent); }
.tx-speaker { font-size: 11px; color: var(--text3); }

/* Detail panel */
.detail-panel { border-top: 1px solid var(--border); padding: 14px; background: var(--bg); }
.detail-wave { height: 120px; background: var(--surface2); border-radius: 6px;
               padding: 4px; margin-bottom: 10px; }
.trim-controls { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.trim-controls label { font-size: 11px; color: var(--text2); }
.trim-controls input[type=range] { flex: 1; accent-color: var(--accent); }
.trim-controls .time-display { font-size: 12px; font-family: monospace; color: var(--text);
                                min-width: 60px; }
.regen-btn { padding: 6px 16px; border-radius: 6px; border: none;
             background: var(--accent); color: #fff; cursor: pointer;
             font-size: 12px; font-weight: 600; font-family: inherit; }
.regen-btn:hover { background: var(--accent2); }
.regen-btn:disabled { opacity: 0.5; cursor: not-allowed; }

.history { margin-top: 12px; }
.history-item { background: var(--surface2); border-radius: 6px; padding: 10px 12px;
                margin-bottom: 8px; border-left: 3px solid var(--border); }
.history-item.current { border-left-color: var(--accent); }
.history-item.old { opacity: 0.7; }
.history-meta { font-size: 10px; color: var(--text3); margin-bottom: 4px; }
.history-text { font-size: 14px; line-height: 1.7; }
.history-scores { font-size: 11px; color: var(--text2); margin-top: 4px; }

.detail-toggle { padding: 4px 10px; border-radius: 4px; border: 1px solid var(--border);
                 background: var(--surface); color: var(--text2); cursor: pointer;
                 font-size: 11px; font-family: inherit; margin-top: 8px; }
.detail-toggle:hover { border-color: var(--accent); color: var(--text); }

.empty { text-align: center; padding: 40px 24px; color: var(--text3); }
.loading { color: var(--accent); }
</style>
</head>
<body>

<div class="header">
  <h1>Transcription Review</h1>
  <div class="stats">
    <span class="stat-pill" id="stat-total">loading...</span>
    <span class="stat-pill" id="stat-verdicts"></span>
    <span class="stat-pill" id="stat-avg"></span>
    <button class="reload-btn" onclick="reloadData()">Reload Data</button>
    <span class="kbd"><kbd>J</kbd>/<kbd>K</kbd> nav <kbd>Space</kbd> play <kbd>Enter</kbd> expand</span>
  </div>
</div>

<div class="filters">
  <button class="filter-btn active" data-filter="all">All</button>
  <button class="filter-btn" data-filter="accept">Accept</button>
  <button class="filter-btn" data-filter="review">Review</button>
  <button class="filter-btn" data-filter="retry">Retry</button>
  <button class="filter-btn" data-filter="reject">Reject</button>
  <button class="filter-btn" data-filter="error">Error</button>
  <input class="search" type="text" placeholder="Search transcription..." id="searchBox">
</div>

<div class="segments" id="segments"></div>

<script>
let DATA = null;
let currentFilter = 'all';
let searchQuery = '';
let activeIdx = -1;
let wavesurfers = {};
let detailWs = null;

function scoreColor(v) {
  if (v >= 0.80) return 'var(--accept)';
  if (v >= 0.65) return 'var(--review)';
  if (v >= 0.55) return 'var(--retry)';
  return 'var(--reject)';
}

function escHtml(s) {
  return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}

function fmtDur(s) {
  if (!s) return '--';
  return s < 60 ? `${s.toFixed(1)}s` : `${Math.floor(s/60)}m${(s%60).toFixed(0)}s`;
}

async function init() {
  const resp = await fetch('/api/data');
  DATA = await resp.json();
  renderStats();
  renderSegments();
  setupFilters();
  setupKeyboard();
}

async function reloadData() {
  await fetch('/api/reload');
  const resp = await fetch('/api/data');
  DATA = await resp.json();
  renderStats();
  renderSegments();
}

function renderStats() {
  const segs = DATA.segments;
  const st = {};
  let ts = 0, cs = 0;
  segs.forEach(s => {
    st[s.status] = (st[s.status]||0) + 1;
    if (s.combined > 0) { ts += s.combined; cs++; }
  });
  document.getElementById('stat-total').textContent = `${segs.length} segments`;
  document.getElementById('stat-verdicts').textContent =
    `${st.accept||0}A ${st.review||0}R ${st.retry||0}T ${st.reject||0}X ${st.error||0}E`;
  document.getElementById('stat-avg').textContent = `avg S: ${cs ? (ts/cs).toFixed(3) : '--'}`;
}

function getFiltered() {
  return DATA.segments.filter(s => {
    if (currentFilter !== 'all' && s.status !== currentFilter) return false;
    if (searchQuery && !(s.transcription||'').toLowerCase().includes(searchQuery.toLowerCase())
        && !(s.seg_num||'').toLowerCase().includes(searchQuery.toLowerCase())
        && !(s.id||'').toLowerCase().includes(searchQuery.toLowerCase())) return false;
    return true;
  });
}

function renderSegments() {
  Object.values(wavesurfers).forEach(ws => ws.destroy());
  wavesurfers = {};

  const container = document.getElementById('segments');
  const filtered = getFiltered();

  if (!filtered.length) {
    container.innerHTML = '<div class="empty">No segments match filter</div>';
    return;
  }

  container.innerHTML = filtered.map((s, i) => {
    const shortId = (s.id||'').replace(/SPEAKER_\d+_/,'').replace('.flac','');
    const bc = `badge-${s.status||'unknown'}`;
    const spk = s.speaker || {};
    const spkTxt = [spk.emotion, spk.speaking_style, spk.pace].filter(Boolean).join(' / ');

    return `
    <div class="seg" data-idx="${s.idx}" data-status="${s.status}" id="seg-${s.idx}">
      <div class="seg-top" onclick="toggleSeg(${s.idx})">
        <span class="seg-num">${s.seg_num}</span>
        <span class="seg-name">${shortId}${s.total_chunks>1 ? ` [${s.chunk_index+1}/${s.total_chunks}]` : ''}</span>
        <span class="seg-dur">${fmtDur(s.duration_sec)}</span>
        <div class="seg-scores">
          <span class="score-chip" style="color:${scoreColor(s.combined)}">${s.combined.toFixed(2)}</span>
          <span class="badge ${bc}">${s.status}</span>
        </div>
      </div>
      <div class="seg-expanded">
        <div class="seg-body">
          <div>
            <div class="waveform-container">
              <div id="wave-${s.idx}" class="waveform-mini"></div>
              <span class="waveform-time" id="wavetime-${s.idx}">0:00 / ${fmtDur(s.duration_sec)}</span>
            </div>
            <div class="scores-row">
              ${['combined','native_ctc','roman_mms'].map(k => {
                const v = s[k] || 0;
                const lbl = k === 'combined' ? 'S' : k === 'native_ctc' ? 'CTC' : 'MMS';
                return `<div class="score-block">
                  <div class="score-val" style="color:${scoreColor(v)}">${v.toFixed(3)}</div>
                  <div class="score-lbl">${lbl}</div>
                  <div class="score-bar"><div class="score-fill" style="width:${v*100}%;background:${scoreColor(v)}"></div></div>
                </div>`;
              }).join('')}
            </div>
            <div style="margin-top:8px">
              <span class="tx-lang">${s.detected_language||'?'}</span>
              ${spkTxt ? `<span class="tx-speaker"> &middot; ${spkTxt}</span>` : ''}
            </div>
            <button class="detail-toggle" onclick="openDetail(${s.idx})">Detailed View + Trim</button>
          </div>
          <div>
            <div class="tx-section">
              <div class="tx-label">Transcription</div>
              <div class="tx-block tx-native">${escHtml(s.transcription)}</div>
            </div>
            ${s.tagged ? `<div class="tx-section">
              <div class="tx-label">Tagged</div>
              <div class="tx-block tx-secondary">${escHtml(s.tagged)}</div>
            </div>` : ''}
            ${(s.uroman || s.romanized) ? `<div class="tx-section">
              <div class="tx-label">Romanized (uroman)</div>
              <div class="tx-block tx-secondary">${escHtml(s.uroman || s.romanized)}</div>
            </div>` : ''}
            ${s.error ? `<div class="tx-section">
              <div class="tx-label" style="color:var(--reject)">Error</div>
              <div class="tx-block tx-secondary" style="color:var(--reject)">${escHtml(s.error)}</div>
            </div>` : ''}
          </div>
        </div>
        <div id="detail-${s.idx}" class="detail-panel" style="display:none">
          <div class="tx-label">Detailed Waveform + Trim</div>
          <div id="detailwave-${s.idx}" class="detail-wave"></div>
          <div class="trim-controls">
            <label>Start</label>
            <span class="time-display" id="trim-start-val-${s.idx}">0.00s</span>
            <input type="range" id="trim-start-${s.idx}" min="0" max="${(s.duration_sec||5)*1000}"
                   value="0" step="10" oninput="updateTrim(${s.idx})">
            <input type="range" id="trim-end-${s.idx}" min="0" max="${(s.duration_sec||5)*1000}"
                   value="${(s.duration_sec||5)*1000}" step="10" oninput="updateTrim(${s.idx})">
            <span class="time-display" id="trim-end-val-${s.idx}">${(s.duration_sec||0).toFixed(2)}s</span>
            <label>End</label>
          </div>
          <div style="display:flex;gap:8px;align-items:center">
            <button class="regen-btn" id="regen-btn-${s.idx}" onclick="retranscribe(${s.idx})">
              Regenerate Transcription
            </button>
            <span id="regen-status-${s.idx}" style="font-size:11px;color:var(--text3)"></span>
          </div>
          <div class="history" id="history-${s.idx}"></div>
        </div>
      </div>
    </div>`;
  }).join('');
}

function toggleSeg(idx) {
  const el = document.getElementById(`seg-${idx}`);
  const wasOpen = el.classList.contains('open');
  // Close all
  document.querySelectorAll('.seg.open').forEach(s => {
    s.classList.remove('open');
  });
  if (!wasOpen) {
    el.classList.add('open');
    activeIdx = idx;
    initWaveform(idx);
    el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  }
}

function initWaveform(idx) {
  if (wavesurfers[idx]) return;
  const seg = DATA.segments.find(s => s.idx === idx);
  if (!seg || !seg.audio_exists) return;

  const ws = WaveSurfer.create({
    container: `#wave-${idx}`,
    height: 64,
    waveColor: '#4a5080',
    progressColor: '#818cf8',
    cursorColor: '#818cf8',
    barWidth: 2,
    barGap: 1,
    barRadius: 2,
    url: seg.audio_url,
  });

  ws.on('timeupdate', (t) => {
    const total = ws.getDuration();
    const el = document.getElementById(`wavetime-${idx}`);
    if (el) el.textContent = `${t.toFixed(1)}s / ${total.toFixed(1)}s`;
  });

  wavesurfers[idx] = ws;
}

function openDetail(idx) {
  const panel = document.getElementById(`detail-${idx}`);
  const isOpen = panel.style.display !== 'none';
  panel.style.display = isOpen ? 'none' : 'block';

  if (!isOpen) {
    initDetailWaveform(idx);
    renderHistory(idx);
  }
}

function initDetailWaveform(idx) {
  const containerId = `detailwave-${idx}`;
  const container = document.getElementById(containerId);
  if (!container || container.dataset.init) return;
  container.dataset.init = 'true';

  const seg = DATA.segments.find(s => s.idx === idx);
  if (!seg || !seg.audio_exists) return;

  const ws = WaveSurfer.create({
    container: `#${containerId}`,
    height: 112,
    waveColor: '#4a5080',
    progressColor: '#818cf8',
    cursorColor: '#f59e0b',
    barWidth: 2,
    barGap: 1,
    barRadius: 2,
    url: seg.audio_url,
  });

  ws.on('ready', () => {
    const dur = ws.getDuration();
    document.getElementById(`trim-start-${idx}`).max = dur * 1000;
    document.getElementById(`trim-end-${idx}`).max = dur * 1000;
    document.getElementById(`trim-end-${idx}`).value = dur * 1000;
    document.getElementById(`trim-end-val-${idx}`).textContent = `${dur.toFixed(2)}s`;
  });
}

function updateTrim(idx) {
  const startSlider = document.getElementById(`trim-start-${idx}`);
  const endSlider = document.getElementById(`trim-end-${idx}`);
  let startMs = parseInt(startSlider.value);
  let endMs = parseInt(endSlider.value);
  if (startMs >= endMs) { startMs = endMs - 100; startSlider.value = startMs; }
  document.getElementById(`trim-start-val-${idx}`).textContent = `${(startMs/1000).toFixed(2)}s`;
  document.getElementById(`trim-end-val-${idx}`).textContent = `${(endMs/1000).toFixed(2)}s`;
}

async function retranscribe(idx) {
  const seg = DATA.segments.find(s => s.idx === idx);
  if (!seg) return;

  const startMs = parseInt(document.getElementById(`trim-start-${idx}`).value);
  const endMs = parseInt(document.getElementById(`trim-end-${idx}`).value);
  const btn = document.getElementById(`regen-btn-${idx}`);
  const status = document.getElementById(`regen-status-${idx}`);

  btn.disabled = true;
  status.textContent = 'Transcribing...';
  status.className = 'loading';

  try {
    const resp = await fetch('/api/retranscribe', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({
        audio_path: seg.audio_path,
        start_sec: startMs / 1000,
        end_sec: endMs / 1000,
        language: 'Telugu',
      })
    });
    const result = await resp.json();

    if (result.error) {
      status.textContent = `Error: ${result.error}`;
      status.style.color = 'var(--reject)';
    } else {
      status.textContent = `Done! S=${(result.combined||0).toFixed(3)} [${result.status}]`;
      status.style.color = scoreColor(result.combined || 0);
      seg.history = seg.history || [];
      seg.history.push(result);
      renderHistory(idx);
    }
  } catch (e) {
    status.textContent = `Error: ${e.message}`;
    status.style.color = 'var(--reject)';
  }
  btn.disabled = false;
}

function renderHistory(idx) {
  const seg = DATA.segments.find(s => s.idx === idx);
  const container = document.getElementById(`history-${idx}`);
  if (!seg || !container) return;

  const items = [];
  // Current (original) transcription
  items.push(`
    <div class="history-item current">
      <div class="history-meta">Original (full segment)</div>
      <div class="history-text tx-native">${escHtml(seg.transcription)}</div>
      <div class="history-scores">S=${(seg.combined||0).toFixed(3)} CTC=${(seg.native_ctc||0).toFixed(3)} MMS=${(seg.roman_mms||0).toFixed(3)} [${seg.status}]</div>
    </div>
  `);

  // History entries (newest first)
  const hist = (seg.history || []).slice().reverse();
  hist.forEach((h, i) => {
    items.push(`
      <div class="history-item old">
        <div class="history-meta">Re-transcription #${hist.length - i} (${h.trim_start?.toFixed(2)||0}s - ${h.trim_end?.toFixed(2)||0}s = ${h.trim_duration||'?'}s)</div>
        <div class="history-text tx-native">${escHtml(h.transcription)}</div>
        <div class="history-scores">S=${(h.combined||0).toFixed(3)} CTC=${(h.native_ctc||0).toFixed(3)} MMS=${(h.roman_mms||0).toFixed(3)} [${h.status||'?'}]</div>
      </div>
    `);
  });

  container.innerHTML = items.join('');
}

function setupFilters() {
  document.querySelectorAll('.filter-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
      currentFilter = btn.dataset.filter;
      renderSegments();
    });
  });
  document.getElementById('searchBox').addEventListener('input', (e) => {
    searchQuery = e.target.value;
    renderSegments();
  });
}

function setupKeyboard() {
  document.addEventListener('keydown', e => {
    if (e.target.tagName === 'INPUT') return;
    const segs = document.querySelectorAll('.seg');
    if (!segs.length) return;

    const idxList = Array.from(segs).map(s => parseInt(s.dataset.idx));
    const curPos = idxList.indexOf(activeIdx);

    if (e.key === 'j' || e.key === 'ArrowDown') {
      e.preventDefault();
      const next = curPos < idxList.length - 1 ? curPos + 1 : curPos;
      toggleSeg(idxList[next]);
    } else if (e.key === 'k' || e.key === 'ArrowUp') {
      e.preventDefault();
      const prev = curPos > 0 ? curPos - 1 : 0;
      toggleSeg(idxList[prev]);
    } else if (e.key === ' ') {
      e.preventDefault();
      if (activeIdx >= 0 && wavesurfers[activeIdx]) {
        wavesurfers[activeIdx].playPause();
      }
    }
  });
}

init();
</script>
</body>
</html>"""


def main():
    parser = argparse.ArgumentParser(description="Transcription Review Dashboard v2")
    parser.add_argument("--port", "-p", type=int, default=8765)
    parser.add_argument("--data", "-d", default=None,
                        help="Path to JSON data (auto-detects full_run.json or v2_comparison.json)")
    parser.add_argument("--segments", "-s", default=DEFAULT_SEG_DIR)
    args = parser.parse_args()

    # Auto-detect data file
    if args.data is None:
        candidates = [
            PROJECT_ROOT / "consistency_test" / "full_run.json",
            PROJECT_ROOT / "consistency_test" / "v2_comparison.json",
        ]
        for c in candidates:
            if c.exists():
                args.data = str(c)
                break
        if args.data is None:
            print("No data file found. Run full_run.py or consistency test first.")
            sys.exit(1)

    print(f"Loading data from {args.data}...")
    data = load_data(args.data, args.segments)
    print(f"Loaded {len(data['segments'])} segments")

    DashboardHandler.data = data
    DashboardHandler.seg_dir = args.segments
    DashboardHandler.data_path = args.data

    server = HTTPServer(("0.0.0.0", args.port), DashboardHandler)
    print(f"\nDashboard: http://localhost:{args.port}")
    print("Press Ctrl+C to stop\n")

    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nShutting down...")
        server.server_close()


if __name__ == "__main__":
    main()
