import * as db from "./db.js";
import * as learning from "./learning.js";
import * as links from "./links.js";
import { esc } from "./util.js";
import { lyricGroups } from "./lyric_sync.js";
import { loadContribution, allContributions, clearContributions } from "./author.js";
import { mountPanelTiming } from "./panel_timing.js";
import { mountLyricsEditor, loadLyricEdit } from "./lyrics_edit.js";
import * as upload from "./upload.js";
import * as detectClient from "./fp/client.js";
import * as confirms from "./confirms.js";
import * as ytmap from "./ytmap.js";

const DIM_LABELS = {
  valence: "Valence", romantic: "Romantic", melancholic: "Melancholy", passion: "Passion",
  social: "Social", philosophical: "Philosophical", narrative: "Narrative", mythical: "Mythical",
};

const qInput = document.getElementById("q");
const resultsEl = document.getElementById("results");
const detailEl = document.getElementById("detail");
const statusEl = document.getElementById("status");
const detectEl = document.getElementById("detect");
const detectBtn = document.getElementById("detect-btn");
const detectMicBtn = document.getElementById("detect-mic");

let searchTimer = null;
let detectToken = 0;          // ignore results from a dismissed/superseded detect

init();

async function init() {
  try {
    await Promise.all([db.init(), learning.init()]);
  } catch (e) {
    statusEl.textContent = "Could not load the El Eje data bundle. Re-run build_snapshot.py.";
    return;
  }
  showLanding();
  qInput.addEventListener("input", () => {
    clearTimeout(searchTimer);
    searchTimer = setTimeout(() => runSearch(qInput.value), 110);
  });
  qInput.focus();
  watchPending();
  setupShareBar();
  setupContribBar();
  setupConfirmBar();
  setupNowPlaying();

  detectBtn.addEventListener("click", () => runDetect("tab"));
  detectMicBtn.addEventListener("click", () => runDetect("mic"));

  // Deep-link / screenshot support: ?q=<query>&open=1&tanda=1
  const params = new URLSearchParams(location.search);
  const aq = params.get("q");
  if (aq) {
    qInput.value = aq;
    runSearch(aq);
    if (params.get("open")) {
      setTimeout(() => {
        const first = resultsEl.querySelector("li");
        if (!first) return;
        first.click();
        if (params.get("tanda")) {
          setTimeout(() => document.getElementById("suggest-tanda")?.click(), 300);
        }
      }, 220);
    }
  }
}

function show(which) {
  resultsEl.hidden = which !== "results";
  detailEl.hidden = which !== "detail";
  statusEl.hidden = which !== "status";
}

function showLanding() {
  const m = db.meta();
  const c = m && m.counts;
  statusEl.className = "empty-hint";
  statusEl.innerHTML = c
    ? `Search the <b>El&nbsp;Eje</b> library — lyrics, translations, recordings &amp; info, fully offline.
       <div class="counts" style="margin-top:10px">
       ${c.songs.toLocaleString()} songs · ${c.withLyrics.toLocaleString()} with translations ·
       ${c.orchestras} orchestras · ${c.singers} singers</div>`
    : `Search the <b>El&nbsp;Eje</b> library.`;
  show("status");
}

function runSearch(query) {
  endPanelTiming();
  const q = (query || "").trim();
  if (detectEl && !detectEl.hidden) { detectToken++; setDetect(null); detectBusy(false); }
  if (!q) { showLanding(); return; }
  const hits = db.search(q);
  if (!hits.length) {
    statusEl.className = "empty-hint";
    statusEl.innerHTML = `No matches for <b>${esc(q)}</b>.`;
    show("status");
    return;
  }
  renderResults(hits);
}

function subline(s) {
  return [s.o, s.s, s.y].filter(Boolean).map(esc).join(" · ");
}

function renderResults(hits) {
  resultsEl.innerHTML = "";
  for (const s of hits) {
    const li = document.createElement("li");
    const badges =
      (s.l ? `<span class="badge lyr">lyrics</span>` : "") +
      (s.g ? `<span class="badge">${esc(s.g)}</span>` : "");
    li.innerHTML = `<div class="r-title">${esc(s.t)}${badges}</div>
                    <div class="r-sub">${subline(s)}</div>`;
    li.addEventListener("click", () => showDetail(s.id));
    resultsEl.appendChild(li);
  }
  show("results");
  document.getElementById("main").scrollTop = 0;
}

function pips(value) {
  let out = `<span class="pips">`;
  for (let i = 1; i <= 3; i++) out += `<span class="pip${i <= value ? " on" : ""}"></span>`;
  return out + `</span>`;
}

function dimsBlock(d) {
  if (!d) return "";
  const items = Object.keys(DIM_LABELS)
    .filter(k => d[k])
    .map(k => `<span class="dim">${DIM_LABELS[k]} ${pips(d[k])}</span>`)
    .join("");
  return items ? `<div class="dims">${items}</div>` : "";
}

function factsBlock(s) {
  const f = [];
  if (s.g) f.push(`<span class="fact">${esc(s.g)}</span>`);
  if (s.k) f.push(`<span class="fact"><b>Key</b> ${esc(s.k)}</span>`);
  if (s.b) f.push(`<span class="fact"><b>BPM</b> ${esc(s.b)}</span>`);
  return f.length ? `<div class="facts">${f.join("")}</div>` : "";
}

function learnBlock(s) {
  const orc = learning.orchestra(s.o);
  const sng = s.s ? learning.singer(s.s) : null;
  let html = "";
  for (const [label, e] of [["orchestra", orc], ["singer", sng]]) {
    if (!e) continue;
    const fields = (e.fields || []).slice(0, 6)
      .map(f => `<b>${esc(f.label)}:</b> ${esc(f.value)}`).join(" &nbsp;·&nbsp; ");
    const bio = e.bio ? `<div class="l-bio">${esc(e.bio)}</div>` : "";
    html += `<details><summary>ⓘ About ${esc(e.name)}${label === "singer" ? " (singer)" : ""}</summary>
      <div class="learn" style="margin-top:8px">
        ${e.subtitle ? `<div class="l-sub">${esc(e.subtitle)}</div>` : ""}
        ${fields ? `<div class="l-fields">${fields}</div>` : ""}
        ${bio}
      </div></details>`;
  }
  return html;
}

async function showDetail(id, opts = {}) {
  const s = db.getSong(id);
  if (!s) return;
  endPanelTiming();
  setDetect(null);
  show("detail");
  document.getElementById("main").scrollTop = 0;

  const banner = opts.via === "sound"
    ? `<div class="sound-banner">🎵 <span><b>Identified by sound</b> — matched this recording by its audio fingerprint.</span></div>`
    : opts.via === "sound-maybe"
    ? `<div class="sound-banner">🎵 <span><b>Possible sound match</b> — chosen from the closest audio candidates.</span></div>`
    : opts.via === "confirmed"
    ? `<div class="sound-banner">✓ <span><b>Confirmed for this page</b> earlier. <button class="link" id="redetect">Re-detect</button></span></div>`
    : opts.via === "playing"
    ? `<div class="sound-banner">🎵 <span><b>Now playing</b> — lyrics for the track in the video you’re watching.</span></div>`
    : opts.via === "title"
    ? `<div class="sound-banner">≈ <span><b>Matched by page title</b> — the sound didn’t match the library (different transfer, performance, or mid-song). Confirm below if it’s right.</span></div>`
    : opts.via === "mapped"
    ? `<div class="sound-banner">🎬 <span><b>Recognized this video</b> — a known YouTube upload of this recording.</span></div>`
    : "";
  const by = [s.o, s.s, s.y].filter(Boolean).map(esc).join(`<span class="sep">·</span>`);
  detailEl.innerHTML = `
    <button class="back">‹ Back</button>
    ${banner}
    <div class="d-title">${esc(s.t)}</div>
    <div class="d-by">${by}</div>
    ${factsBlock(s)}
    ${dimsBlock(s.d)}
    <div class="actions">
      <a class="btn" target="_blank" rel="noopener" href="${esc(links.youtube(s))}">▶ Search on YouTube</a>
      <a class="btn ghost" target="_blank" rel="noopener" href="${esc(links.spotify(s))}">Search on Spotify</a>
      <a class="btn ghost" target="_blank" rel="noopener" href="${esc(links.web(s))}">Web</a>
    </div>
    <div id="confirm-slot"></div>
    <div id="tanda-slot"></div>
    ${learnBlock(s)}
    <div id="lyrics-slot"></div>
    <div id="recs-slot"></div>`;

  detailEl.querySelector(".back").addEventListener("click", () => {
    if (qInput.value.trim()) runSearch(qInput.value); else showLanding();
  });
  const redet = document.getElementById("redetect");
  if (redet) redet.addEventListener("click", () => runDetect("tab", { skipConfirmed: true }));

  renderConfirmControl(s);  // async, "this is what's playing here" / confirmed state
  setupTanda(s);            // async, shows "Suggest a tanda" if this track is in one
  renderRecs(s, !!opts.via);   // via set ⇒ detected/now-playing context ⇒ display-only
  renderLyrics(s);          // async, fills in when the shard arrives
}

// "Suggest a tanda": if the looked-up track belongs to one of the curated tandas,
// offer to reveal a tanda it's in (random among them if several).
async function setupTanda(s) {
  const slot = document.getElementById("tanda-slot");
  if (!slot) return;
  const tandas = await db.tandasForSong(s.id);
  if (!tandas.length) return;
  slot.innerHTML = `<button class="btn ghost" id="suggest-tanda">Suggest a tanda</button>
                    <div id="tanda-card"></div>`;
  let shown = -1;
  const pick = () => {
    if (tandas.length === 1) return 0;
    let i;
    do { i = Math.floor(Math.random() * tandas.length); } while (i === shown);
    return i;
  };
  const render = () => {
    shown = pick();
    const t = tandas[shown];
    const rows = t.tracks.map(tr => {
      const sub = [tr.o, tr.s, tr.y].filter(Boolean).map(esc).join(" · ");
      const here = tr.id === s.id ? " here" : "";
      return `<li class="${here}" ${tr.id ? `data-id="${esc(tr.id)}"` : ""}>${esc(tr.t)}` +
             `<span class="r-sub">${sub}</span></li>`;
    }).join("");
    document.getElementById("tanda-card").innerHTML = `
      <div class="tanda">
        <div class="tanda-h">${esc(t.label)}
          ${t.genre ? `<span class="badge">${esc(t.genre)}</span>` : ""}
          ${tandas.length > 1 ? `<button class="reshuffle" id="tanda-again" title="Another tanda it's in">↻</button>` : ""}
        </div>
        <ol class="tanda-tracks">${rows}</ol>
      </div>`;
    document.querySelectorAll("#tanda-card .tanda-tracks li[data-id]").forEach(li =>
      li.addEventListener("click", () => showDetail(li.dataset.id)));
    const again = document.getElementById("tanda-again");
    if (again) again.addEventListener("click", render);
  };
  document.getElementById("suggest-tanda").addEventListener("click", render);
}

// `live` = this detail is the currently detected/playing track. There the list is DISPLAY-ONLY:
// a stray click would navigate away from what's playing and risk mis-associating it (noise).
// Clickable only when browsing (reached from search/links, not from a detect/now-playing).
function renderRecs(s, live) {
  const others = db.otherRecordings(s);
  const slot = document.getElementById("recs-slot");
  if (!others.length) return;
  const items = others.map(o =>
    `<li ${live ? "" : `data-id="${esc(o.id)}"`}>${esc(o.o || "Unknown")}${o.s ? " · " + esc(o.s) : ""}
       <span class="r-sub">${[o.y, o.g].filter(Boolean).map(esc).join(" · ")}${o.l ? " · lyrics" : ""}</span></li>`
  ).join("");
  slot.innerHTML = `<div class="section-h">Other recordings of “${esc(s.t)}” (${others.length})</div>
                    <ul class="recs${live ? " static" : ""}">${items}</ul>`;
  if (!live) slot.querySelectorAll("li").forEach(li =>
    li.addEventListener("click", () => showDetail(li.dataset.id)));
}

async function renderLyrics(s) {
  const slot = document.getElementById("lyrics-slot");
  const m = db.meta();
  if (m && m.clean) { slot.innerHTML = ""; return; }   // public build ships no lyric text
  slot.innerHTML = `<div class="section-h">Lyrics &amp; translation</div>
                    <div class="no-lyrics">Loading…</div>`;
  // Prefer the user's saved edit (added/fixed lines) over the bundled lyrics, so edits
  // show AND can be timed. Falls back to the bundled lyrics, then to nothing.
  const base = s.l ? await db.getLyrics(s.id) : null;
  const edit = await loadLyricEdit(s.id);
  // Prefer the user's edited lyrics AND edited Grabado/credits over the bundled ones.
  const ly = edit
    ? { es: edit.es, en: edit.en, by: (edit.by != null ? edit.by : (base && base.by)),
        byBase: edit.byBase, grabaciones: edit.grabaciones }   // so the editor preloads the entry list
    : base;
  const editBtn = `<button class="time-btn" id="edit-lyrics">✎ ${ly && (ly.es || ly.en) ? "Edit" : "Add"} lyrics &amp; credits</button>`;

  if (!ly || (!ly.es && !ly.en)) {
    slot.innerHTML = `<div class="section-h">Lyrics &amp; translation ${editBtn}</div>
                      <div class="no-lyrics">No lyrics in the library for this recording — add them to time along.</div>
                      ${ly && ly.by ? `<div class="contrib">${esc(ly.by)}</div>` : ""}`;
    document.getElementById("edit-lyrics").addEventListener("click", () => enterLyricsEdit(s, ly));
    return;
  }
  // Interleave: each Spanish line with its English translation right below it, and
  // drop dot-only separator lines. Fall back to two columns only if there's no es.
  const groups = lyricGroups(ly);
  const flow = groups.length
    ? `<div class="lyrics-flow">${groups.map(g =>
        `<div class="lg"><div class="es">${esc(g.es)}</div>` +
        `${g.en ? `<div class="en">${esc(g.en)}</div>` : ""}</div>`).join("")}</div>`
    : `<div class="lyrics">
         <div class="lyr-col es"><h4>Español</h4><pre>${esc(ly.es)}</pre></div>
         <div class="lyr-col en"><h4>English</h4><pre>${esc(ly.en)}</pre></div></div>`;
  slot.innerHTML = `<div class="section-h">Lyrics &amp; translation${edit ? ` <span class="edited-tag">edited</span>` : ""}
      <span class="lyr-acts">${editBtn}${groups.length ? `<button class="time-btn" id="add-timing" title="Add line timings for this recording">✎ Add timing</button>` : ""}</span></div>
    ${flow}
    ${ly.by ? `<div class="contrib">${esc(ly.by)}</div>` : ""}`;
  document.getElementById("edit-lyrics").addEventListener("click", () => enterLyricsEdit(s, ly));
  if (groups.length) {
    document.getElementById("add-timing")?.addEventListener("click", () => enterPanelTiming(s, ly));
  }
}

// Edit/add lyrics for this recording (paired es/en rows). On done, re-render — which
// now prefers the saved edit, so the new lines show and become timeable.
function enterLyricsEdit(s, ly) {
  endPanelTiming();
  const slot = document.getElementById("lyrics-slot");
  if (!slot) return;
  mountLyricsEditor({ slot, song: s, ly, onDone: () => renderLyrics(s) });
}

// ---- Click-to-stamp timing in the panel (UI mounted from ./panel_timing.js) ------
let ptHandle = null;           // active mountPanelTiming handle ({ teardown })

function endPanelTiming() { if (ptHandle) { ptHandle.teardown(); ptHandle = null; } }

async function enterPanelTiming(s, ly) {
  const slot = document.getElementById("lyrics-slot");
  if (!slot) return;
  const groups = lyricGroups(ly);
  if (!groups.length) return;
  endPanelTiming();

  // Resume an existing contribution for this song (stored by original es-line index).
  const stored = await loadContribution("song:" + s.id);
  let prior = null;
  if (stored) {
    const posByEs = new Map(groups.map((g, pos) => [g.i, pos]));
    prior = {
      startSec: stored.startSec, endSec: stored.endSec,
      lines: (stored.lines || []).map(l => ({ i: posByEs.get(l.i), t: l.t })).filter(l => Number.isInteger(l.i)),
    };
  }
  ptHandle = mountPanelTiming({ slot, song: s, groups, prior, onDone: () => renderLyrics(s) });
}

// ---- Sharing consent (always visible) -------------------------------------
// El Eje shares anonymous contributions (recordings you confirm, corrections, lyric edits,
// timings, and YouTube video ids — no page URLs, audio, or personal data) to improve the
// shared library. ON by default; one master toggle governs both the feedback path
// (eleje_share_optout, consumed by feedback.js) and the timing path (shareTimings, upload.js),
// so the disclosed behavior is a single, honest setting. el-eje.com is a granted host
// permission, so no per-toggle permission prompt is needed.
const SHARE_OPTOUT_KEY = "eleje_share_optout";   // default false ⇒ sharing ON
const SHARE_TIMINGS_KEY = "shareTimings";         // kept in sync with the master toggle

async function setupShareBar() {
  const box = document.getElementById("share-master");
  if (!box || !(chrome.storage && chrome.storage.local)) return;
  const o = await chrome.storage.local.get(SHARE_OPTOUT_KEY);
  const on = o[SHARE_OPTOUT_KEY] !== true;          // default ON
  box.checked = on;
  await chrome.storage.local.set({ [SHARE_TIMINGS_KEY]: on });   // keep timings in sync
  if (on) upload.submitAll(await allContributions());            // flush anything saved earlier
  box.addEventListener("change", async () => {
    const v = box.checked;
    await chrome.storage.local.set({ [SHARE_OPTOUT_KEY]: !v, [SHARE_TIMINGS_KEY]: v });
    if (v) upload.submitAll(await allContributions());           // background auto-flushes feedback
  });
}

// ---- Crowd-sourced timing contributions: count + export + clear -----------
// Viewers author per-line timings on YouTube (saved to chrome.storage.local); this
// footer lets the owner export them as JSON to fold into the desktop app.
async function setupContribBar() {
  const bar = document.getElementById("contrib-bar");
  if (!bar) return;
  const countEl = document.getElementById("contrib-count");
  const refresh = async () => {
    const all = await allContributions();
    bar.hidden = all.length === 0;
    if (all.length) {
      const lines = all.reduce((n, c) => n + ((c.lines && c.lines.length) || 0), 0);
      countEl.textContent = `${all.length} recording${all.length > 1 ? "s" : ""} timed · ${lines} lines`;
    }
  };
  document.getElementById("contrib-export").addEventListener("click", async () => {
    const all = await allContributions();
    if (!all.length) return;
    const blob = new Blob([JSON.stringify(all, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "eleje-timings-" + new Date().toISOString().slice(0, 10) + ".json";
    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 2000);
  });
  document.getElementById("contrib-clear").addEventListener("click", async () => {
    if (!confirm("Delete all saved timing contributions? Export them first if you want to keep them.")) return;
    await clearContributions();
    refresh();
  });
  // (Sharing consent now lives in #share-bar / setupShareBar — always visible, not gated here.)
  if (chrome.storage && chrome.storage.onChanged) {
    chrome.storage.onChanged.addListener((ch, area) => {
      if (area === "local" && ch.eleje_timings) refresh();
    });
  }
  refresh();
}

// Right-click "Look up in El Eje" hands a selection to the panel via session
// storage; the on-page "Detect tango" button hands over a pendingDetect flag.
function watchPending() {
  if (typeof chrome === "undefined" || !chrome.storage || !chrome.storage.session) return;
  const consume = async () => {
    const { pendingQuery, pendingDetect } = await chrome.storage.session.get(["pendingQuery", "pendingDetect"]);
    if (pendingQuery) {
      await chrome.storage.session.remove("pendingQuery");
      qInput.value = pendingQuery;
      runSearch(pendingQuery);
    }
    if (pendingDetect) {
      await chrome.storage.session.remove("pendingDetect");
      runDetect("tab");          // best-effort; falls back to a Try-again button if a gesture is needed
    }
  };
  consume();
  chrome.storage.onChanged.addListener((changes, area) => {
    if (area !== "session") return;
    if (changes.pendingQuery && changes.pendingQuery.newValue) consume();
    if (changes.pendingDetect && changes.pendingDetect.newValue) consume();
  });
}

// ---- Now playing → auto-show lyrics ---------------------------------------
// The YouTube overlay writes the track it confidently identifies to
// chrome.storage.local (key `eleje_nowplaying`). When the panel is idle (on the
// landing, not mid-search/detail/detect) we surface that track's lyrics here —
// on open for a recently-played track, and live when the overlay matches a new one.
const NOWPLAYING_KEY = "eleje_nowplaying";
let lastNowPlayingAt = 0;

// Only auto-show from the landing, so we never yank the user out of a search,
// an open detail, or an in-flight sound detection.
function panelIsIdle() {
  return detailEl.hidden && resultsEl.hidden && !qInput.value.trim()
      && (!detectEl || detectEl.hidden);
}

async function setupNowPlaying() {
  if (typeof chrome === "undefined" || !chrome.storage || !chrome.storage.local) return;
  const maybeShow = (np, recentOnly) => {
    if (!np || !np.songId || !db.getSong(np.songId)) return;
    if ((np.at || 0) === lastNowPlayingAt) return;       // already surfaced
    if (recentOnly && Date.now() - (np.at || 0) > 4 * 3600 * 1000) return;  // stale on open
    if (!panelIsIdle()) return;
    lastNowPlayingAt = np.at || 0;
    showDetail(np.songId, { via: "playing" });
  };
  try {
    const { [NOWPLAYING_KEY]: np } = await chrome.storage.local.get(NOWPLAYING_KEY);
    maybeShow(np, true);                                  // panel opened over a playing tango
  } catch { /* storage unavailable */ }
  if (chrome.storage.onChanged) {
    chrome.storage.onChanged.addListener((ch, area) => {
      if (area === "local" && ch[NOWPLAYING_KEY]) maybeShow(ch[NOWPLAYING_KEY].newValue, false);
    });
  }
}

// ---- Detect tango by sound (on-device fingerprint) ------------------------

function setDetect(html, cls) {
  if (html == null) { detectEl.hidden = true; detectEl.innerHTML = ""; return; }
  detectEl.className = "detect-panel" + (cls ? " " + cls : "");
  detectEl.innerHTML = html;
  detectEl.hidden = false;
}

function detectBusy(busy) { detectBtn.disabled = busy; detectMicBtn.disabled = busy; }

// ---- Page → track confirmations (fast lookup + export) --------------------
// The URL/title come from the active tab's content script (no "tabs" permission).
async function activePageInfo() {
  try { return (await chrome.runtime.sendMessage({ type: "pageInfo" })) || {}; }
  catch { return {}; }
}
async function activeUrl() { return (await activePageInfo()).url || ""; }
function hostOf(url) { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return "this page"; } }

async function updateConfirmBar() {
  const bar = document.getElementById("confirm-bar");
  const countEl = document.getElementById("confirm-count");
  if (!bar || !countEl) return;
  const all = await confirms.allConfirms();
  bar.hidden = all.length === 0;
  if (all.length) countEl.textContent = `${all.length} page${all.length > 1 ? "s" : ""} → track confirmed`;
}

// On a song's detail: mark it as the track playing on the current page (stored
// for instant lookup next time), or show/undo an existing confirmation.
async function renderConfirmControl(song) {
  const slot = document.getElementById("confirm-slot");
  if (!slot) return;
  const url = await activeUrl();
  if (!url) { slot.innerHTML = ""; return; }        // chrome:// etc. — nothing to key on
  const host = hostOf(url);
  const existing = await confirms.getConfirm(url);
  if (existing && existing.songId === song.id) {
    slot.innerHTML = `<div class="confirm-row done">✓ Confirmed as the track for <b>${esc(host)}</b>
      <button class="link" id="confirm-undo">Undo</button></div>`;
    document.getElementById("confirm-undo").addEventListener("click", async () => {
      await confirms.removeConfirm(url); renderConfirmControl(song); updateConfirmBar();
    });
  } else {
    const note = existing ? `<div class="confirm-note">replaces “${esc(existing.t)}”</div>` : "";
    slot.innerHTML = `<button class="btn ghost confirm-set" id="confirm-set">✓ This is what’s playing on ${esc(host)}</button>${note}`;
    document.getElementById("confirm-set").addEventListener("click", async () => {
      await confirms.saveConfirm(url, song, "panel");
      renderConfirmControl(song); updateConfirmBar();
    });
  }
}

function setupConfirmBar() {
  const bar = document.getElementById("confirm-bar");
  if (!bar) return;
  document.getElementById("confirm-export").addEventListener("click", async () => {
    const all = await confirms.allConfirms();
    if (!all.length) return;
    const blob = new Blob([JSON.stringify(all, null, 2)], { type: "application/json" });
    const u = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = u; a.download = "eleje-confirmations-" + new Date().toISOString().slice(0, 10) + ".json";
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(u), 2000);
  });
  document.getElementById("confirm-clear").addEventListener("click", async () => {
    if (!confirm("Delete all saved page→track confirmations? Export them first if you want to keep them.")) return;
    await confirms.clearConfirms(); updateConfirmBar();
  });
  if (chrome.storage && chrome.storage.onChanged) {
    chrome.storage.onChanged.addListener((ch, area) => {
      if (area === "local" && ch.eleje_confirms) updateConfirmBar();
    });
  }
  updateConfirmBar();
}

async function runDetect(source, opts = {}) {
  const token = ++detectToken;
  detectBusy(true);
  // Fast lookup: a track previously confirmed for this page opens instantly,
  // skipping the 12 s capture (unless the user asked to re-detect).
  if (source === "tab" && !opts.skipConfirmed) {
    const url = await activeUrl();
    if (url) {
      const conf = await confirms.getConfirm(url);
      let gid = conf && conf.songId, via = "confirmed";
      if (!gid) { gid = await ytmap.lookup(url); via = "mapped"; }   // bundled grown map
      const song = gid && db.getSong(gid);
      if (song && token === detectToken) {
        detectBusy(false);
        showDetail(song.id, { via });
        return;
      }
    }
  }
  // 2. METADATA — identify from the page's OWN track info (YouTube's detected song, the
  //    title, the description). Works for videos we've never seen the URL of, as long as
  //    the track is in the library, with NO audio capture. Combined with the fingerprint
  //    fallback below (metadata first because it's instant + reliable, fingerprint when the
  //    page doesn't name the track).
  let pageInfo = {};
  if (source === "tab") {
    pageInfo = (await chrome.runtime.sendMessage({ type: "detectInfo" }).catch(() => ({}))) || {};
    const m = pageInfo.music;
    const q = (m && m.song) ? [m.song, m.artist, m.year].filter(Boolean).join(" ") : (pageInfo.title || "");
    const cands = q ? db.matchTitle(q, 5, pageInfo.description || "") : [];
    if (cands[0] && db.isConfident(cands[0]) && token === detectToken) {
      detectBusy(false);
      showDetail(cands[0].song.id, { via: "title" });
      return;
    }
  }
  // Staged capture: match after ~12 s, then keep listening (up to maxS) only while the
  // verdict is short of a confident hit — easy tracks return fast, faint transfers/old
  // masters get the extra audio they need instead of a false "couldn't identify".
  const minS = 12, maxS = 34;
  let elapsed = 0, extending = false;
  const baseSub = source === "mic" ? "Listening through the microphone."
                                   : "Capturing this tab’s audio — it keeps playing.";
  setDetect(`<div class="dh"><span class="pulse"></span><span>Listening…</span>
      <span id="dt-count" style="color:var(--gold)">0s</span>
      <button class="x" id="dt-cancel" title="Cancel">×</button></div>
    <div class="sub" id="dt-sub">${baseSub}</div>`);
  document.getElementById("dt-cancel").addEventListener("click", () => {
    detectToken++; setDetect(null); detectBusy(false);
  });
  const timer = setInterval(() => {
    if (token !== detectToken) { clearInterval(timer); return; }
    elapsed++;
    const c = document.getElementById("dt-count");
    if (c) c.textContent = elapsed + "s";
    if (elapsed >= maxS) clearInterval(timer);
  }, 1000);
  const onProgress = ({ elapsed: el, verdict }) => {
    if (token !== detectToken) return;
    if (!extending && el >= minS && verdict !== "accept") {
      extending = true;
      const sub = document.getElementById("dt-sub");
      if (sub) sub.textContent = "Faint signal — listening a little longer…";
    }
  };

  // 3. FINGERPRINT — but first seek the player to the intro so the capture overlaps the
  //    indexed first ~40 s. This is THE reason live detection used to never work: the sound
  //    index covers each track's first 40 s, yet the user is usually mid-song, so a capture
  //    from the current position never matched. (Only seeks when actually past the window.)
  let seeked = false;
  if (source === "tab") {
    const sr = (await chrome.runtime.sendMessage({ type: "seekIntro" }).catch(() => ({}))) || {};
    seeked = !!sr.seeked;
    if (seeked) {
      const sub = document.getElementById("dt-sub");
      if (sub) sub.textContent = "Sampling the track’s intro…";
      await new Promise(r => setTimeout(r, 700));   // let the seek + buffering settle
    }
  }

  let res;
  try {
    const o = { minSeconds: minS, maxSeconds: maxS, onProgress, isCancelled: () => token !== detectToken };
    res = source === "mic" ? await detectClient.detectMic(o)
                           : await detectClient.detectTab(o);
  } catch (e) {
    res = { ok: false, error: "exception", detail: String(e && e.message || e) };
  }
  clearInterval(timer);
  if (seeked) chrome.runtime.sendMessage({ type: "restorePlayback" }).catch(() => {});  // back to where they were
  if (token !== detectToken) return;        // cancelled or superseded
  // Broaden: if the sound didn't match (a different transfer/performance, mid-song,
  // or not fingerprinted), fall back to the page TITLE — most YouTube/Spotify pages
  // name the track, so we can still identify it (shown as a title match, to confirm).
  if (res && res.ok !== false && res.verdict === "abstain" && source === "tab") {
    try {
      const { title } = await activePageInfo();
      const cands = title ? db.matchTitle(title, 5) : [];
      if (cands[0] && db.isConfident(cands[0])) {
        res = { ok: true, verdict: "title", songId: cands[0].song.id,
                candidates: cands.map(c => ({ songId: c.song.id })) };
      }
    } catch { /* no page title available */ }
  }
  detectBusy(false);
  renderDetect(res, source);
}

function renderDetect(res, source) {
  if (!res || res.ok === false || res.error) return renderDetectError(res || {}, source);

  if (res.verdict === "accept" && res.songId && db.getSong(res.songId)) {
    setDetect(null);
    showDetail(res.songId, { via: "sound" });
    return;
  }
  if (res.verdict === "title" && res.songId && db.getSong(res.songId)) {
    setDetect(null);
    showDetail(res.songId, { via: "title" });
    return;
  }
  if (res.verdict === "suggest" && res.candidates && res.candidates.length) {
    const items = res.candidates
      .map(c => ({ c, s: db.getSong(c.songId) })).filter(x => x.s).slice(0, 4)
      .map(({ s }) =>
        `<li data-id="${esc(s.id)}"><b>${esc(s.t)}</b>
           <div class="r-sub">${[s.o, s.s, s.y].filter(Boolean).map(esc).join(" · ")}</div></li>`).join("");
    setDetect(`<div class="dh"><span>Not sure — closest matches</span>
        <button class="x" id="dt-x" title="Dismiss">×</button></div>
      <div class="sub">The audio is close to these but not a confident match. Tap one to open it.</div>
      <ul class="cand">${items}</ul>`);
    detectEl.querySelectorAll(".cand li[data-id]").forEach(li =>
      li.addEventListener("click", () => { setDetect(null); showDetail(li.dataset.id, { via: "sound-maybe" }); }));
    document.getElementById("dt-x").addEventListener("click", () => setDetect(null));
    return;
  }
  // abstain
  const micFallback = source === "tab"
    ? `<button class="btn ghost" id="dt-mic">🎤 Use microphone</button>` : "";
  setDetect(`<div class="dh"><span>Couldn’t identify that audio</span>
      <button class="x" id="dt-x" title="Dismiss">×</button></div>
    <div class="sub">We sampled the track’s intro but it didn’t match — this recording may not be in the
      sound index yet. Try searching by name above${source === "tab" ? ", or if the music is playing from a speaker in the room use the microphone" : ""}.</div>
    <div class="row"><button class="btn ghost" id="dt-retry">Try again</button>${micFallback}</div>`, "bad");
  document.getElementById("dt-x").addEventListener("click", () => setDetect(null));
  document.getElementById("dt-retry").addEventListener("click", () => runDetect(source));
  document.getElementById("dt-mic")?.addEventListener("click", () => runDetect("mic"));
}

function renderDetectError(res, source) {
  const map = {
    "permission-denied": "Permission to capture this tab’s audio was declined. Click Detect tango and choose “Allow” on the Chrome prompt.",
    "reopen-needed": "Audio permission granted — close and reopen this panel (click the El Eje toolbar icon), then click Detect tango again.",
    "no-active-tab": "Couldn’t find an active tab to listen to. Click on the web page playing the music, then try again.",
    "capture-failed": "Couldn’t capture this tab’s audio. Chrome can’t capture chrome:// pages, the Web Store, or PDFs — switch to a normal page (YouTube, Spotify, a blog), make sure audio is playing and the tab isn’t muted, then try again.",
    "mic-denied": "Microphone access was blocked. Open the El Eje panel’s site settings (the tune/ⓘ icon left of the address bar → Microphone → Allow), or reset it, then click Mic again. Or use Detect tango to read this tab’s audio directly (no mic needed).",
    "mic-none": "No microphone was found. Use Detect tango to read this tab’s audio directly instead.",
    "mic-failed": "Couldn’t record from the microphone. Use Detect tango to read this tab’s audio directly instead.",
  };
  const known = map[res.error];
  const msg = known || (source === "mic"
    ? "Couldn’t access the microphone. Use Detect tango to read this tab’s audio directly instead — it doesn’t need the mic."
    : "Couldn’t hear this tab’s audio. Make sure something is playing and the tab isn’t muted, then try again.");
  // Surface the raw error/detail (when it isn't already the friendly message) so a
  // capture failure is visible and reportable instead of a silent dead-end.
  const raw = res.detail || (!known && res.error && res.error !== "exception" ? res.error : "");
  const detail = raw ? `<div class="dt-detail">${esc(String(raw))}</div>` : "";
  setDetect(`<div class="dh"><span>Detection unavailable</span>
      <button class="x" id="dt-x" title="Dismiss">×</button></div>
    <div class="sub">${esc(msg)}</div>${detail}
    <div class="row"><button class="btn ghost" id="dt-retry">Try again</button></div>`, "bad");
  document.getElementById("dt-x").addEventListener("click", () => setDetect(null));
  document.getElementById("dt-retry").addEventListener("click", () => runDetect(source));
}
