// ── Character library ─────────────────────────────────────────────────────────
//
// Persistent, book-scoped character sheets stored in SQLite via /api/characters.
// All function signatures are unchanged from the IndexedDB version so callers
// (audiobook.js, character-sheets.js, etc.) need no edits.

// 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'],
];

// ── Server API ────────────────────────────────────────────────────────────────

async function clGetAll() {
  const r = await fetch('/api/characters');
  if (!r.ok) throw new Error('clGetAll failed: ' + r.status);
  const d = await r.json();
  return d.characters || [];
}

async function clGet(id) {
  const r = await fetch('/api/characters/' + encodeURIComponent(id));
  if (r.status === 404) return undefined;
  if (!r.ok) throw new Error('clGet failed: ' + r.status);
  return r.json();
}

async function clPut(rec) {
  const r = await fetch('/api/characters/' + encodeURIComponent(rec.id), {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(rec),
  });
  if (!r.ok) throw new Error('clPut failed: ' + r.status);
  return r.json();
}

async function clDelete(id) {
  const r = await fetch('/api/characters/' + encodeURIComponent(id), { method: 'DELETE' });
  if (!r.ok) throw new Error('clDelete failed: ' + r.status);
}

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

// Coerce a sheet field value to string, matching _csStr in character-sheets.js.
function _clStr(v) {
  if (v == null) return '';
  if (typeof v === 'string') return v;
  if (Array.isArray(v)) return v.filter(Boolean).join(', ');
  return JSON.stringify(v);
}

// Sanitize all scalar string fields on a raw sheet object before storing.
function _clSanitize(sheet) {
  const out = { ...sheet };
  const fields = (typeof CS_SCALAR_FIELDS !== 'undefined') ? CS_SCALAR_FIELDS
    : CL_EDIT_FIELDS.map(f => f[0]).filter(k => k !== 'name');
  fields.forEach(f => { if (out[f] != null) out[f] = _clStr(out[f]); });
  return out;
}

// Merge an incoming sheet into an existing one without blanking solid facts.
function clMergeSheet(existing, incoming) {
  const e = { ...existing };
  const inc = _clSanitize(incoming);
  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 ((inc[f] || '').length > (e[f] || '').length) e[f] = inc[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;
}

// Union a character's tag strings into one clean comma-separated list.
function clMergeTags(...parts) {
  const set = new Set();
  parts.forEach(p => String(p || '').split(',').map(t => t.trim()).filter(Boolean).forEach(t => set.add(t)));
  return [...set].join(', ');
}

// 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) : { ..._clSanitize(sheet), name };
  const tags = clMergeTags(prev?.tags, sheet.tags, bk);
  const rec = {
    id, book: bk, name, tags,
    sheet: merged,
    analysis: prev?.analysis || null,
    voice:    prev?.voice    || sheet.voice || null,
    image:    prev?.image    || sheet.image || null,
    created:  prev?.created  || now,
    updated:  now,
  };
  await clPut(rec);
  return rec;
}

// Save (or clear) a profile image for a character by its record id.
async function clSetImage(id, dataUrl) {
  const rec = await clGet(id).catch(() => null);
  if (!rec) return;
  rec.image = dataUrl || null;
  rec.updated = new Date();
  await clPut(rec);
  return rec;
}
window.clSetImage = clSetImage;

// All characters belonging to a production, by origin book OR by tag membership.
async function clGetAllByTagOrBook(title) {
  const key = String(title || '').trim().toLowerCase();
  if (!key) return [];
  const all = await clGetAll().catch(() => []);
  return all.filter(r => {
    if (String(r.book || '').trim().toLowerCase() === key) return true;
    return String(r.tags || '').split(',').some(t => t.trim().toLowerCase() === key);
  });
}

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

// ── One-time IndexedDB → server migration ────────────────────────────────────
// Runs once after a successful clGetAll() that returns an empty server. Reads
// the old IndexedDB store (if present) and POSTs all records to /api/characters/migrate.

(async function _clMigrateIfNeeded() {
  try {
    const serverRecs = await clGetAll();
    if (serverRecs.length > 0) return; // already migrated
    const idbRecs = await _clIdbGetAll().catch(() => []);
    if (!idbRecs.length) return;
    const r = await fetch('/api/characters/migrate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(idbRecs),
    });
    if (r.ok) {
      const d = await r.json();
      console.log(`[characters-library] migrated ${d.imported} records from IndexedDB → SQLite`);
    }
  } catch (e) {
    console.warn('[characters-library] migration skipped:', e);
  }
})();

function _clIdbGetAll() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open('character-library', 1);
    req.onerror = () => resolve([]);
    req.onsuccess = e => {
      const db = e.target.result;
      if (!db.objectStoreNames.contains('characters')) { db.close(); resolve([]); return; }
      const tx  = db.transaction('characters', 'readonly');
      const all = tx.objectStore('characters').getAll();
      all.onsuccess = ev => { db.close(); resolve(ev.target.result || []); };
      all.onerror   = () => { db.close(); resolve([]); };
    };
  });
}

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

let _clRecords = [];

async function clRender() {
  const grid = document.getElementById('cl-grid');
  if (!grid) return;
  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 = []; }

  const filter = document.getElementById('cl-book-filter');
  const prods = clAllProductions();
  if (filter) {
    const cur = filter.value;
    filter.innerHTML = `<option value="">All productions (${prods.length})</option>` +
      prods.map(b => `<option value="${escHtml(b)}">${escHtml(b)}</option>`).join('');
    if (cur && prods.includes(cur)) filter.value = cur;
  }
  clApplyFilter();
}

function clAllProductions() {
  const set = new Set();
  _clRecords.forEach(r => {
    if (r.book) set.add(r.book);
    String(r.tags || '').split(',').map(t => t.trim()).filter(Boolean).forEach(t => set.add(t));
  });
  return [...set].sort((a, b) => a.localeCompare(b));
}

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) {
    const bk = book.toLowerCase();
    recs = recs.filter(r =>
      (r.book || '').toLowerCase() === bk ||
      String(r.tags || '').split(',').some(t => t.trim().toLowerCase() === bk));
  }
  if (q) recs = recs.filter(r =>
    (r.name || '').toLowerCase().includes(q) ||
    (r.sheet?.aliases || '').toLowerCase().includes(q) ||
    (r.sheet?.archetype || '').toLowerCase().includes(q) ||
    (r.tags || '').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;
  }

  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) {
  const tags = String(rec.tags || '').split(',').map(t => t.trim()).filter(Boolean);
  const chips = tags.length
    ? `<div class="cl-card-tags">${tags.map(t => `<span class="cl-tag-chip"><span class="mdi mdi-tag-outline"></span>${escHtml(t)}</span>`).join('')}</div>`
    : '';
  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)}
    ${chips}
  </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>Tags / productions (comma-separated — reuse across books &amp; scripts)</span>
        <input type="text" data-rec-field="tags" value="${escHtml(String(rec.tags || rec.book || ''))}" placeholder="e.g. Das Jahr des Greifen, Star-Trek"></label>
      <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 };
    const tagsEl = modal.querySelector('[data-rec-field="tags"]');
    const newTags = tagsEl ? clMergeTags(tagsEl.value, rec.book) : (rec.tags || rec.book);
    const newId = clKey(rec.book, newName);
    rec.sheet = newSheet; rec.name = newName; rec.tags = newTags; 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.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;
window.clUpsert = clUpsert;
window.clGetAll = clGetAll;
window.clPut = clPut;
window.clGetAllByTagOrBook = clGetAllByTagOrBook;
