// ── Character library ─────────────────────────────────────────────────────────
//
// Persistent, book-scoped character sheets. Running "Character sheets" in Read
// Aloud or the Rehearser upserts each extracted character into this library,
// keyed by (book + name). Cards reuse the renderers from character-sheets.js
// (csCardHtml, csAlignmentBar, csDeepAnalysis), which loads before this module.

const CL_DB_NAME = 'character-library';
const CL_STORE   = 'characters';

// Fields a user can edit, mirroring CS_SCALAR_FIELDS plus the labelled basics.
const CL_EDIT_FIELDS = [
  ['name', 'Name'], ['aliases', 'Aliases'], ['archetype', 'Archetype'],
  ['physical', 'Physical'], ['clothing', 'Clothing & Appearance'],
  ['alignment', 'Alignment & Ethos'], ['arc_note', 'Arc note'],
  ['skills', 'Trained Skills'], ['capabilities', 'Capabilities'],
  ['backstory', 'Backstory & Origin'], ['relationships', 'Relationships'],
  ['motivation', 'Motivation'], ['fears', 'Fears'], ['mannerisms', 'Mannerisms & Habits'],
  ['voice_pattern', 'Voice & Speech'], ['secret', 'Dark Secret / Fatal Flaw'],
  ['conflict_style', 'Conflict Style'], ['win_condition', 'Win Condition'],
];

// ── IndexedDB ─────────────────────────────────────────────────────────────────

function clDbOpen() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(CL_DB_NAME, 1);
    req.onupgradeneeded = e => {
      const db = e.target.result;
      if (!db.objectStoreNames.contains(CL_STORE)) {
        const store = db.createObjectStore(CL_STORE, { keyPath: 'id' });
        store.createIndex('book', 'book', { unique: false });
      }
    };
    req.onsuccess = e => resolve(e.target.result);
    req.onerror   = e => reject(e.target.error);
  });
}

async function clDbOp(mode, fn) {
  const db = await clDbOpen();
  return new Promise((resolve, reject) => {
    const tx  = db.transaction(CL_STORE, mode);
    const req = fn(tx.objectStore(CL_STORE));
    req.onsuccess = e => resolve(e.target.result);
    req.onerror   = e => reject(e.target.error);
  });
}

async function clGetAll() {
  const db = await clDbOpen();
  return new Promise((resolve, reject) => {
    const req = db.transaction(CL_STORE, 'readonly').objectStore(CL_STORE).getAll();
    req.onsuccess = e => resolve(e.target.result || []);
    req.onerror   = e => reject(e.target.error);
  });
}

async function clGet(id)      { return clDbOp('readonly',  s => s.get(id)); }
async function clPut(rec)     { return clDbOp('readwrite', s => s.put(rec)); }
async function clDelete(id)   { return clDbOp('readwrite', s => s.delete(id)); }

function clKey(book, name) {
  return `${String(book || '').trim()}::${String(name || '').trim()}`.toLowerCase();
}

// Merge an incoming sheet into an existing one without blanking solid facts —
// same rule as csMerge in character-sheets.js (longer value wins; sources append;
// alignment score averages).
function clMergeSheet(existing, incoming) {
  const e = { ...existing };
  const scalars = (typeof CS_SCALAR_FIELDS !== 'undefined') ? CS_SCALAR_FIELDS
    : CL_EDIT_FIELDS.map(f => f[0]).filter(k => k !== 'name');
  scalars.forEach(f => { if ((incoming[f] || '').length > (e[f] || '').length) e[f] = incoming[f]; });
  if (incoming.tier === 'main') e.tier = 'main';
  if (incoming.moral_alignment_score != null) {
    e.moral_alignment_score = e.moral_alignment_score != null
      ? Math.round((e.moral_alignment_score + incoming.moral_alignment_score) / 2)
      : incoming.moral_alignment_score;
  }
  if (incoming.arc_direction && incoming.arc_direction !== 'neutral') e.arc_direction = incoming.arc_direction;
  e.inventory = [...(existing.inventory || [])];
  (incoming.inventory || []).forEach(it => { if (it && !e.inventory.includes(it) && e.inventory.length < 3) e.inventory.push(it); });
  e.sources = [...(existing.sources || [])];
  (incoming.sources || []).forEach(src => {
    if (src && src.quote && e.sources.length < 5 && !e.sources.some(x => x.quote === src.quote)) e.sources.push(src);
  });
  return e;
}

// Upsert one sheet into the library under a book. Returns the stored record.
async function clUpsert(book, sheet) {
  const name = (sheet.name || '').trim();
  if (!name) return null;
  const bk = (book || '').trim() || 'Unsorted';
  const id = clKey(bk, name);
  const now = new Date();
  const prev = await clGet(id).catch(() => null);
  const merged = prev ? clMergeSheet(prev.sheet || {}, sheet) : { ...sheet, name };
  const rec = {
    id, book: bk, name,
    sheet: merged,
    analysis: prev?.analysis || null,
    voice: prev?.voice || null,
    created: prev?.created || now,
    updated: now,
  };
  await clPut(rec);
  return rec;
}

// Bulk upsert a list of sheets for one book (used by the analysis side-effect).
async function clUpsertMany(book, sheets) {
  let n = 0;
  for (const s of (sheets || [])) { if (await clUpsert(book, s)) n++; }
  return n;
}

// ── Rendering ─────────────────────────────────────────────────────────────────

let _clRecords = [];

async function clRender() {
  const grid = document.getElementById('cl-grid');
  if (!grid) return;
  // Bind filter/search once the section fragment exists (it loads after this JS).
  const filterEl = document.getElementById('cl-book-filter');
  const searchEl = document.getElementById('cl-search');
  if (filterEl && !filterEl.dataset.bound) { filterEl.dataset.bound = '1'; filterEl.addEventListener('change', clApplyFilter); }
  if (searchEl && !searchEl.dataset.bound) { searchEl.dataset.bound = '1'; searchEl.addEventListener('input', clApplyFilter); }
  try { _clRecords = await clGetAll(); } catch (e) { _clRecords = []; }

  // Populate the book filter (preserve current selection if still present).
  const filter = document.getElementById('cl-book-filter');
  const books = [...new Set(_clRecords.map(r => r.book))].sort((a, b) => a.localeCompare(b));
  if (filter) {
    const cur = filter.value;
    filter.innerHTML = `<option value="">All books (${books.length})</option>` +
      books.map(b => `<option value="${escHtml(b)}">${escHtml(b)}</option>`).join('');
    if (cur && books.includes(cur)) filter.value = cur;
  }
  clApplyFilter();
}

function clApplyFilter() {
  const grid = document.getElementById('cl-grid');
  if (!grid) return;
  const book = document.getElementById('cl-book-filter')?.value || '';
  const q = (document.getElementById('cl-search')?.value || '').trim().toLowerCase();

  let recs = _clRecords.slice();
  if (book) recs = recs.filter(r => r.book === book);
  if (q) recs = recs.filter(r =>
    (r.name || '').toLowerCase().includes(q) ||
    (r.sheet?.aliases || '').toLowerCase().includes(q) ||
    (r.sheet?.archetype || '').toLowerCase().includes(q) ||
    (r.book || '').toLowerCase().includes(q));

  if (!recs.length) {
    grid.innerHTML = `<div class="cl-empty">
      <span class="mdi mdi-account-box-multiple-outline" style="font-size:48px;opacity:0.4"></span>
      <p>${_clRecords.length ? 'No characters match your filter.' : 'No characters yet.'}</p>
      <p class="cl-empty-hint">Run <b>Character sheets</b> from Read Aloud or the Script Rehearser to populate your library.</p>
    </div>`;
    return;
  }

  // Group by book.
  const byBook = new Map();
  for (const r of recs) { if (!byBook.has(r.book)) byBook.set(r.book, []); byBook.get(r.book).push(r); }
  const groups = [...byBook.keys()].sort((a, b) => a.localeCompare(b));

  grid.innerHTML = groups.map(bk => {
    const list = byBook.get(bk).sort((a, b) => {
      const t = (a.sheet?.tier === 'main' ? 0 : 1) - (b.sheet?.tier === 'main' ? 0 : 1);
      return t || (a.name || '').localeCompare(b.name || '');
    });
    return `<div class="cl-book-group">
      <div class="cl-book-head"><span class="mdi mdi-book-open-page-variant-outline"></span> ${escHtml(bk)}
        <span class="cl-book-count">${list.length}</span></div>
      <div class="cl-book-cards">${list.map(clCardHtml).join('')}</div>
    </div>`;
  }).join('');
}

function clCardHtml(rec) {
  return `<div class="cl-card-wrap" data-id="${escHtml(rec.id)}">
    <div class="cl-card-tools">
      <button class="btn-secondary btn-sm cl-edit" data-id="${escHtml(rec.id)}" title="Edit this character"><span class="mdi mdi-pencil-outline"></span> Edit</button>
      <button class="btn-secondary btn-sm cl-delete" data-id="${escHtml(rec.id)}" title="Delete from library"><span class="mdi mdi-trash-can-outline"></span></button>
    </div>
    ${csCardHtml(rec.sheet)}
  </div>`;
}

// ── Edit modal ──────────────────────────────────────────────────────────────

function clEdit(id) {
  const rec = _clRecords.find(r => r.id === id);
  if (!rec) return;
  const s = rec.sheet || {};
  const modal = document.createElement('div');
  modal.className = 'audiobook-overlay';
  const rows = CL_EDIT_FIELDS.map(([k, label]) => {
    const multiline = !['name', 'aliases', 'archetype'].includes(k);
    const val = escHtml(String(s[k] || ''));
    return `<label class="cl-edit-row"><span>${label}</span>${
      multiline
        ? `<textarea data-field="${k}" rows="2">${val}</textarea>`
        : `<input type="text" data-field="${k}" value="${val}">`
    }</label>`;
  }).join('');
  modal.innerHTML = `<div class="audiobook-box cl-edit-box">
    <div class="cs-titlebar">
      <span class="audiobook-title"><span class="mdi mdi-pencil-outline"></span> Edit — ${escHtml(rec.name)}</span>
      <span style="flex:1"></span>
      <button class="btn-secondary btn-sm" id="cl-edit-cancel">Cancel</button>
      <button class="btn-primary btn-sm" id="cl-edit-save"><span class="mdi mdi-content-save-outline"></span> Save</button>
    </div>
    <div class="cl-edit-body">
      <label class="cl-edit-row"><span>Moral alignment (0 evil – 100 good)</span>
        <input type="number" min="0" max="100" data-field="moral_alignment_score" value="${s.moral_alignment_score ?? 50}"></label>
      <label class="cl-edit-row"><span>Arc direction</span>
        <select data-field="arc_direction">${
          ['stable-good', 'stable-bad', 'neutral', 'good-to-bad', 'bad-to-good', 'complex']
            .map(o => `<option value="${o}"${(s.arc_direction || 'neutral') === o ? ' selected' : ''}>${o}</option>`).join('')
        }</select></label>
      ${rows}
    </div>
  </div>`;
  document.body.appendChild(modal);
  const close = () => modal.remove();
  modal.querySelector('#cl-edit-cancel').addEventListener('click', close);
  modal.addEventListener('click', e => { if (e.target === modal) close(); });
  modal.querySelector('#cl-edit-save').addEventListener('click', async () => {
    const patch = {};
    modal.querySelectorAll('[data-field]').forEach(el => { patch[el.dataset.field] = el.value; });
    const newName = (patch.name || '').trim() || rec.name;
    let mas = parseInt(patch.moral_alignment_score, 10);
    mas = isNaN(mas) ? 50 : Math.max(0, Math.min(100, mas));
    const newSheet = { ...s, ...patch, name: newName, moral_alignment_score: mas };
    // If the name changed, the key changes too — delete old, write new.
    const newId = clKey(rec.book, newName);
    rec.sheet = newSheet; rec.name = newName; rec.updated = new Date();
    try {
      if (newId !== rec.id) { await clDelete(rec.id); rec.id = newId; }
      await clPut(rec);
      toast('Character saved', 'success');
      close();
      clRender();
    } catch (e) { toast('Save failed: ' + (e.message || e), 'error'); }
  });
}

async function clDeleteRec(id) {
  const rec = _clRecords.find(r => r.id === id);
  if (!rec) return;
  if (!confirm(`Delete "${rec.name}" from ${rec.book}?`)) return;
  try { await clDelete(id); toast('Deleted', 'success'); clRender(); }
  catch (e) { toast('Delete failed: ' + (e.message || e), 'error'); }
}

// ── Wiring ──────────────────────────────────────────────────────────────────

// Document-level delegation — the #cl-grid is (re)built on every render and the
// section fragment loads after this script, so we can't bind to it directly.
document.addEventListener('click', e => {
  const edit = e.target.closest?.('.cl-edit');
  if (edit) { e.stopPropagation(); clEdit(edit.dataset.id); return; }
  const del = e.target.closest?.('.cl-delete');
  if (del) { e.stopPropagation(); clDeleteRec(del.dataset.id); return; }
  const deep = e.target.closest?.('.cl-card-wrap .cs-deep-btn');
  if (deep) {
    e.stopPropagation();
    const rec = _clRecords.find(r => r.id === deep.closest('.cl-card-wrap')?.dataset.id);
    if (rec && typeof csDeepAnalysis === 'function') csDeepAnalysis(rec.sheet, '');
  }
});

window.clRender = clRender;
window.clUpsertMany = clUpsertMany;
