<h1>Titles</h1>
<div class="pagebar"><span id="catcount"></span></div>
<div class="filterbar">
  <input type="search" id="catsearch" placeholder="filter titles…" aria-label="filter titles">
  <button id="shakyfilter" aria-pressed="false" hidden
          title="reads the model wasn't sure of — confirm or fix each one">shaky reads</button>
</div>
<div id="catbody"><p class="empty">Nothing extracted yet — start on the <a href="/photos">photos page</a>.</p></div>
<script>
"use strict";
let CATALOG = [];
let EDITING = null;  // lineKey of the row whose editor is open

function lineKey(c) { return c.title_raw + "|" + c.photos.join(";"); }

function editorRow(c) {
  const cue = c.cues || {};
  return `
  <tr class="editrow"><td colspan="5">
    <form class="editform" data-title="${esc(c.title_raw)}"
          data-photos="${esc(c.photos.join(";"))}">
      <label>Title <input name="title" value="${esc(c.title_raw)}" required></label>
      <label>Publisher <input name="publisher" value="${esc(cue.publisher || "")}"></label>
      <label>Edition <input name="edition" value="${esc(cue.edition || "")}"></label>
      <label>Year <input name="year" value="${esc(cue.year ?? "")}" inputmode="numeric" size="6"></label>
      <label>Language <input name="language" value="${esc(cue.language || "")}"></label>
      <span class="editactions">
        <button type="submit" class="primary">save</button>
        <button type="button" class="canceledit">cancel</button>
        <button type="button" class="removetitle danger">remove — not a game</button>
      </span>
      <span class="edithint">saving re-queues this title for resolve with the corrected data</span>
    </form>
  </td></tr>`;
}

let SHAKY_ONLY = false;

function render() {
  const q = document.getElementById("catsearch").value.trim().toLowerCase();
  const shakyCount = CATALOG.filter(c => c.shaky).length;
  const fbtn = document.getElementById("shakyfilter");
  fbtn.hidden = shakyCount === 0 && !SHAKY_ONLY;
  fbtn.textContent = `shaky reads — ${shakyCount}`;
  fbtn.setAttribute("aria-pressed", String(SHAKY_ONLY));
  const sorted = [...CATALOG].sort((a, b) =>
    a.title_raw.localeCompare(b.title_raw, undefined, { sensitivity: "base" }));
  const searched = q
    ? sorted.filter(c => (c.title_raw + " " + c.bgg_name).toLowerCase().includes(q))
    : sorted;
  const rows = SHAKY_ONLY ? searched.filter(c => c.shaky) : searched;
  document.getElementById("catcount").innerHTML =
    `<b>${rows.length}</b> of <b>${CATALOG.length}</b> title(s)`;
  document.getElementById("catbody").innerHTML = rows.length
    ? `<div class="catalog"><table>` + rows.map(c => `
      <tr>
        <td class="t">${esc(c.title_raw)}
          ${c.split_copy ? `<span class="chip merged">copy</span>` : ""}
          ${c.shaky ? `<span class="chip shaky"
              title="the model wasn't sure of this read — press ✓ if it's right, or edit it">shaky read</span>` : ""}</td>
        <td>${statusChip(c)}</td>
        <td class="meta">${metaLine(c)}</td>
        <td class="meta">${c.photos.map(p =>
          `<a href="/photos/view/${encodeURIComponent(p)}">${esc(p)}</a>`
        ).join(", ")}</td>
        <td class="actions">${c.can_split
          ? `<button class="split" data-title="${esc(c.title_raw)}"
                     data-photos="${esc(c.photos.join(";"))}"
                     data-rowix="${c.row_ix ?? ""}"
                     title="one line, several boxes? make each photo its own copy">
               split into copies</button>`
          : ""}
          ${c.shaky ? `<button class="confirmread" data-title="${esc(c.title_raw)}"
                  data-photos="${esc(c.photos.join(";"))}"
                  title="the read is correct as-is — mark it verified">✓ looks right</button>` : ""}
          <button class="edit" data-key="${esc(lineKey(c))}"
                  title="fix a misread title or add cues you already know">edit</button>
        </td>
      </tr>` + (EDITING === lineKey(c) ? editorRow(c) : "")).join("") + `</table></div>`
    : `<p class="empty">${CATALOG.length
        ? "No titles match that filter."
        : `Nothing extracted yet — start on the <a href="/photos">photos page</a>.`}</p>`;
}

let LAST = null;
async function refresh() {
  const state = await fetchJSON("/api/state");
  if (EDITING) return;  // never repaint under an open editor
  const payload = JSON.stringify(state.catalog);
  if (payload === LAST) return;
  CATALOG = state.catalog;
  render();
  LAST = payload;  // after render: a throw must not freeze the page as "current"
}

document.getElementById("catbody").addEventListener("click", async e => {
  const cancel = e.target.closest("button.canceledit");
  if (cancel) { EDITING = null; LAST = null; render(); refresh().catch(() => {}); return; }
  const ok = e.target.closest("button.confirmread");
  if (ok) {
    const res = await apiPost("/api/edit-title", {
      title_raw: ok.dataset.title,
      source_photos: ok.dataset.photos,
      confirm: true,
    });
    if (res) { LAST = null; refresh().catch(() => {}); }
    return;
  }
  const rm = e.target.closest("button.removetitle");
  if (rm) {
    const f = rm.closest("form.editform");
    if (!confirm(`Remove "${f.dataset.title}" from the titles list? ` +
                 `Re-running extract won't bring it back — the removal is saved ` +
                 `in data/title_removals.json (delete its record there to undo).`)) return;
    const res = await apiPost("/api/remove-title", {
      title_raw: f.dataset.title,
      source_photos: f.dataset.photos,
    });
    if (res) { EDITING = null; LAST = null; refresh().catch(() => {}); }
    return;
  }
  const edit = e.target.closest("button.edit");
  if (edit) {
    EDITING = EDITING === edit.dataset.key ? null : edit.dataset.key;
    render();
    return;
  }
  const b = e.target.closest("button.split");
  if (!b) return;
  const n = b.dataset.photos.split(";").length;
  if (!confirm(`Split "${b.dataset.title}" into ${n} separate copies (one per photo)? ` +
               `Each picks its own edition afterward.`)) return;
  const res = await apiPost("/api/split", {
    title_raw: b.dataset.title,
    source_photos: b.dataset.photos,
    row_ix: b.dataset.rowix === "" ? null : Number(b.dataset.rowix),
  });
  if (res) refresh().catch(() => {});
});

document.getElementById("catbody").addEventListener("submit", async e => {
  const f = e.target.closest("form.editform");
  if (!f) return;
  e.preventDefault();
  const orig = CATALOG.find(c => lineKey(c) === EDITING) || {};
  const cue = orig.cues || {};
  const v = name => f.elements[name].value;
  const body = { title_raw: f.dataset.title, source_photos: f.dataset.photos };
  if (v("title").trim() !== f.dataset.title) body.title_new = v("title").trim();
  if (v("publisher") !== (cue.publisher || "")) body.publisher = v("publisher");
  if (v("edition") !== (cue.edition || "")) body.edition = v("edition");
  if (v("year") !== String(cue.year ?? "")) body.year = v("year");
  if (v("language") !== (cue.language || "")) body.language = v("language");
  if (Object.keys(body).length <= 2) {
    // saving unchanged: on a shaky line that means "it's right as-is"
    if (orig.shaky) {
      const res = await apiPost("/api/edit-title", { ...body, confirm: true });
      if (res) { EDITING = null; LAST = null; refresh().catch(() => {}); return; }
    }
    EDITING = null; render(); return;
  }
  const res = await apiPost("/api/edit-title", body);
  if (res) { EDITING = null; LAST = null; refresh().catch(() => {}); }
});

document.getElementById("catsearch").addEventListener("input", render);
document.getElementById("shakyfilter").addEventListener("click", () => {
  SHAKY_ONLY = !SHAKY_ONLY;
  render();
});
refresh().catch(err => errorBanner(err.message || err));
pollLoop(refresh, 5000, () => showBanner(""));
</script>
