// ── Character sheets ────────────────────────────────────────────────────────
//
// Actor-facing, RPG-style character sheets extracted by the user's LLM from a
// document (Read Aloud) or a script (Script Rehearser). Each claim cites a
// source (page + short quote + category hint). Shared by both sections via one overlay.

const CS_CHUNK_CHARS = 4000;
const _cs = { running: false, cancel: false, cache: {} };

// Resolve the LLM endpoint. Prefer an explicitly-typed rehearser URL, then the
// app's loaded settings. Crucially, NEVER fall back to the hardcoded
// localhost:11434 default — if settings aren't loaded yet, return '' so the
// SERVER uses its own configured llm_url instead of a dead localhost address
// (which caused "Connection refused" character-sheet failures).
function csLlmUrl() {
  const explicit = $('reh-llm-url')?.value.trim();
  if (explicit) return explicit;
  const fromSettings = (typeof _appSettings !== 'undefined' && _appSettings && _appSettings.llm_url) ? _appSettings.llm_url : '';
  if (fromSettings && !/localhost:11434|127\.0\.0\.1:11434/.test(fromSettings)) return fromSettings;
  return '';
}
function csLlmModel() { return $('reh-llm-model')?.value || ''; }
function csLang()     { return $('reh-design-lang')?.value || ''; }

// Build page-annotated text from the current reader scope ([p.N] at page changes).
function csReaderText() {
  if (typeof readerScopeIndices !== 'function' || !readerState?.sentences?.length) return '';
  let out = '', lastPage = -1;
  for (const i of readerScopeIndices()) {
    const u = readerState.sentences[i];
    const pg = u.words?.[0]?.page;
    if (readerState.mode === 'pdf' && pg != null && pg !== lastPage) { out += `\n[p.${pg + 1}] `; lastPage = pg; }
    out += u.text + ' ';
  }
  return out.trim();
}

// Build text from the rehearser script (narration + "SPEAKER: line"), page markers at page breaks.
function csRehearserText() {
  if (!window.rehState || !(rehState.lines || []).length) return '';
  let out = '', page = 1, started = false;
  for (const l of rehState.lines) {
    if (l.type === 'pagebreak') { page++; out += `\n[p.${page}] `; continue; }
    const t = (typeof stripMarkdown === 'function' ? stripMarkdown(l.text || '') : (l.text || '')).trim();
    if (!t) continue;
    if (!started) { out += '[p.1] '; started = true; }
    out += (l.type === 'dialog' && l.speaker ? l.speaker + ': ' : '') + t + '\n';
  }
  return out.trim();
}

// ── Generation (chunked + merged by character) ───────────────────────────────

const CS_SCALAR_FIELDS = ['aliases', 'archetype', 'physical', 'clothing', 'alignment', 'arc_note',
  'attribute_high', 'attribute_low', 'skills', 'capabilities',
  'backstory', 'relationships', 'motivation', 'fears', 'mannerisms', 'voice_pattern',
  'secret', 'conflict_style', 'win_condition'];

function csExistingSummary(map) {
  if (!map.size) return '';
  // Keep the summary compact — just name + archetype + which key fields are still blank.
  // Long "still needs:" lists were blowing up context windows on early passages.
  const KEY_FIELDS = ['physical', 'backstory', 'motivation', 'voice_pattern', 'relationships'];
  return [...map.values()].slice(0, 40).map(s => {
    const missing = KEY_FIELDS.filter(f => !(s[f] || '').trim());
    const suffix = missing.length < KEY_FIELDS.length ? ` | needs: ${missing.join(', ')}` : ' | complete';
    return `- ${s.name}${s.archetype ? ` (${s.archetype})` : ''}${suffix}`;
  }).join('\n');
}

function _csStr(v) {
  if (v == null) return '';
  if (typeof v === 'string') return v;
  if (Array.isArray(v)) return v.filter(Boolean).join(', ');
  return JSON.stringify(v);
}

function csMerge(map, sheets) {
  for (const s of sheets) {
    const name = (s.name || '').trim(); if (!name) continue;
    const key = name.toLowerCase();
    if (!map.has(key)) {
      const copy = { ...s, name, inventory: [...(s.inventory || [])], sources: [...(s.sources || [])] };
      CS_SCALAR_FIELDS.forEach(f => { copy[f] = _csStr(copy[f]); });
      map.set(key, copy);
      continue;
    }
    const e = map.get(key);
    CS_SCALAR_FIELDS.forEach(f => { const sv = _csStr(s[f]); if (sv.length > (e[f] || '').length) e[f] = sv; });
    if (s.tier === 'main') e.tier = 'main';
    if (s.moral_alignment_score != null) {
      e.moral_alignment_score = e.moral_alignment_score != null
        ? Math.round((e.moral_alignment_score + s.moral_alignment_score) / 2)
        : s.moral_alignment_score;
    }
    if (s.arc_direction && s.arc_direction !== 'neutral') e.arc_direction = s.arc_direction;
    (s.inventory || []).forEach(it => { if (it && !e.inventory.includes(it) && e.inventory.length < 3) e.inventory.push(it); });
    (s.sources || []).forEach(src => {
      if (src && src.quote && e.sources.length < 5 && !e.sources.some(x => x.quote === src.quote)) e.sources.push(src);
    });
  }
}

async function csGenerate(text, cacheKey, initialRoster) {
  if (_cs.running) return null;
  if (!text) { toast('Nothing to analyse', 'error'); return null; }
  const chunks = (typeof splitTextIntoChunks === 'function') ? splitTextIntoChunks(text, CS_CHUNK_CHARS) : [text];
  _cs.running = true; _cs.cancel = false;
  const prog = csProgress(chunks.length);
  const llm_url = csLlmUrl(), model = csLlmModel();
  let language = csLang();
  if ((!language || /^auto$/i.test(language)) && typeof detectLang === 'function') {
    const detected = detectLang(text);
    if (detected) {
      language = detected;
      const sel = $('reh-design-lang');
      if (sel) { const o = [...sel.options].find(x => x.value.toLowerCase() === detected.toLowerCase() || x.textContent.toLowerCase() === detected.toLowerCase()); if (o) sel.value = o.value; }
    }
  }
  const map = new Map();
  // Pre-seed the roster with known characters from the casting run so the LLM
  // knows all character names from passage 1 and won't rediscover them.
  const roster = initialRoster ? [...initialRoster] : [];
  const knownTotal = roster.length || null;
  try {
    for (let i = 0; i < chunks.length; i++) {
      if (_cs.cancel) break;
      const detailed = [...map.values()].filter(s => CS_SCALAR_FIELDS.some(f => (s[f] || '').trim())).length;
      const total = knownTotal || map.size;
      const charLabel = knownTotal
        ? `${detailed} / ${knownTotal} characters detailed`
        : `${map.size} characters found`;
      prog.update(i, `Passage ${i + 1} / ${chunks.length}…`, charLabel);
      try {
        const r = await fetch('/api/character-sheets', {
          method: 'POST', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ text: chunks[i], known_characters: roster.slice(0, 60), existing: csExistingSummary(map), language, llm_url, model }),
        });
        if (!r.ok) {
          let detail = `HTTP ${r.status}`;
          try { const e = await r.json(); detail = e.detail || e.error || detail; } catch (_) {}
          throw new Error(detail);
        }
        const raw = await r.text();
        let data;
        try { data = JSON.parse(raw); }
        catch (_) { throw new Error('Invalid JSON from server (passage may be too large)'); }
        csMerge(map, data.sheets || []);
        (data.characters || []).forEach(n => { if (!roster.includes(n)) roster.push(n); });
      } catch (e) {
        console.error('Character sheets passage', i + 1, 'failed:', e);
        toast('Passage ' + (i + 1) + ' failed: ' + (e.message || String(e)), 'error');
      }
    }
  } finally { prog.done(); _cs.running = false; }
  if (_cs.cancel) { toast('Cancelled', 'error'); return null; }
  const sheets = [...map.values()];
  if (cacheKey) _cs.cache[cacheKey] = sheets;
  return sheets;
}

function csProgress(total) {
  let ov = document.getElementById('cs-progress');
  if (!ov) {
    ov = document.createElement('div');
    ov.id = 'cs-progress'; ov.className = 'audiobook-overlay';
    ov.innerHTML = `<div class="audiobook-box">
      <div class="audiobook-title"><span class="mdi mdi-account-details-outline"></span> Character sheets</div>
      <div class="audiobook-msg" id="cs-progress-msg">Analysing…</div>
      <div class="reader-synth-track" style="margin-bottom:4px"><div class="reader-synth-fill" id="cs-progress-fill"></div></div>
      <div class="cs-progress-chars" id="cs-progress-chars"></div>
      <div class="audiobook-actions"><button class="btn-secondary btn-sm" id="cs-progress-cancel">Cancel</button></div>
    </div>`;
    document.body.appendChild(ov);
    ov.querySelector('#cs-progress-cancel').addEventListener('click', () => { _cs.cancel = true; });
  }
  ov.style.display = 'flex';
  const fill  = ov.querySelector('#cs-progress-fill');
  const msg   = ov.querySelector('#cs-progress-msg');
  const chars = ov.querySelector('#cs-progress-chars');
  return {
    update(d, label, charLabel) {
      if (fill)  fill.style.width = (d / total * 100) + '%';
      if (msg && label)    msg.textContent = label;
      if (chars && charLabel) chars.textContent = charLabel;
    },
    done() { ov.style.display = 'none'; },
  };
}

// ── Alignment bar ─────────────────────────────────────────────────────────────

function csAlignmentBar(score, arcDirection, arcNote) {
  if (score == null) return '';
  const pct = Math.max(0, Math.min(100, score));
  const arcMap = {
    'stable-good': { arrow: '→', label: 'Stable Good',  color: '#66bb6a' },
    'stable-bad':  { arrow: '→', label: 'Stable Evil',  color: '#aaa'    },
    'neutral':     { arrow: '→', label: 'Neutral',      color: '#aaa'    },
    'good-to-bad': { arrow: '↘', label: 'Descends toward evil', color: '#ff7043' },
    'bad-to-good': { arrow: '↗', label: 'Redeems toward good',  color: '#66bb6a' },
    'complex':     { arrow: '↕', label: 'Complex arc',  color: '#ab47bc' },
  };
  const arc = arcMap[arcDirection] || arcMap['neutral'];
  const label = pct >= 70 ? 'Good' : pct <= 30 ? 'Evil' : 'Neutral/Ambiguous';
  const showArrow = arcDirection && arcDirection !== 'neutral';
  return `<div class="cs-alignment-wrap">
    <div class="cs-alignment-labels"><span>Evil</span><span>Good</span></div>
    <div class="cs-alignment-gradient" title="${label} (${pct}/100)${arcNote ? ' — ' + arcNote : ''}">
      <div class="cs-alignment-dot" style="left:${pct}%">
        ${showArrow ? `<span class="cs-alignment-arc-arrow" style="color:${arc.color}" title="${escHtml(arc.label)}">${arc.arrow}</span>` : ''}
      </div>
    </div>
    ${arcNote ? `<div class="cs-arc-note">${escHtml(arc.label)} · <em>${escHtml(arcNote)}</em></div>` : `<div class="cs-arc-note" style="color:${arc.color}">${escHtml(arc.label)}</div>`}
  </div>`;
}

// ── Image & Voice prompt generators ──────────────────────────────────────────

function csBuildImagePrompt(s) {
  const parts = [];
  if (s.archetype) parts.push(s.archetype);
  if (s.gender)    parts.push(`${s.gender} character`);
  if (s.physical)  parts.push(s.physical);
  if (s.clothing)  parts.push(s.clothing);
  if (s.alignment) parts.push(s.alignment);
  const pct = s.moral_alignment_score ?? 50;
  parts.push(pct >= 70 ? 'benevolent expression' : pct <= 30 ? 'dark and menacing presence' : 'ambiguous expression');
  if (s.arc_direction === 'bad-to-good') parts.push('redemptive aura');
  if (s.arc_direction === 'good-to-bad') parts.push('ominous aura, turning to darkness');
  return `portrait of ${escHtml(s.name)}, ${parts.filter(Boolean).join(', ')}, fantasy character art, detailed face, dramatic lighting, high detail`;
}

function csBuildVoicePrompt(s) {
  const parts = [];
  if (s.voice_pattern) parts.push(s.voice_pattern);
  if (s.mannerisms)    parts.push(s.mannerisms);
  if (s.archetype)     parts.push(`archetype: ${s.archetype}`);
  if (s.gender)        parts.push(`gender: ${s.gender}`);
  const pct = s.moral_alignment_score ?? 50;
  if (pct >= 70)      parts.push('warm, trustworthy tone');
  else if (pct <= 30) parts.push('cold, threatening or sinister tone');
  else                parts.push('neutral, measured tone');
  return parts.filter(Boolean).join('. ');
}

// ── Deep analysis modal ───────────────────────────────────────────────────────

async function csDeepAnalysis(sheet, sourceText) {
  const llm_url = csLlmUrl(), model = csLlmModel(), language = csLang();
  const modal = document.createElement('div');
  modal.className = 'audiobook-overlay';
  modal.innerHTML = `<div class="audiobook-box cs-deep-box">
    <div class="cs-titlebar">
      <span class="audiobook-title"><span class="mdi mdi-brain"></span> Deep Analysis — ${escHtml(sheet.name)}</span>
      <span style="flex:1"></span>
      <button class="btn-secondary btn-sm" id="cs-deep-close">Close</button>
    </div>
    <div class="cs-deep-body" id="cs-deep-body">
      <div style="text-align:center;padding:40px;color:var(--subtext)">
        <span class="mdi mdi-loading mdi-spin" style="font-size:28px"></span><br>Running deep psychological analysis…
      </div>
    </div>
  </div>`;
  document.body.appendChild(modal);
  modal.querySelector('#cs-deep-close').addEventListener('click', () => modal.remove());
  modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); });

  try {
    const summary = [sheet.archetype, sheet.alignment, sheet.secret, sheet.win_condition, sheet.conflict_style]
      .filter(Boolean).join('; ');
    const r = await fetch('/api/character-deep-analysis', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        name: sheet.name, role: sheet.tier, goal: sheet.win_condition,
        summary, text_excerpt: (sourceText || '').slice(0, 8000), language, llm_url, model,
      }),
    });
    if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail || r.statusText);
    const data = await r.json();
    const a = data.analysis || {};
    const section = (icon, title, content) => content
      ? `<div class="cs-deep-section">
          <div class="cs-deep-section-head"><span class="mdi ${icon}"></span> ${title}</div>
          <div class="cs-deep-section-body">${escHtml(String(content))}</div>
        </div>` : '';
    modal.querySelector('#cs-deep-body').innerHTML = `
      <div class="cs-deep-header">
        <div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
          <span class="cs-name" style="font-size:20px">${escHtml(sheet.name)}</span>
          ${sheet.archetype ? `<span class="cs-archetype">${escHtml(sheet.archetype)}</span>` : ''}
        </div>
        ${csAlignmentBar(sheet.moral_alignment_score, sheet.arc_direction, sheet.arc_note)}
      </div>
      ${section('mdi-heart-broken-outline',  '1 — Core Flaw & Desire',    a.core_flaw)}
      ${section('mdi-run-fast',              '2 — Agency & Passivity',    a.agency)}
      ${section('mdi-comment-quote-outline', '3 — Dialogue & Voice',      a.dialogue_voice)}
      ${section('mdi-timeline-outline',      '4 — Narrative Arc',         a.narrative_arc)}
      ${section('mdi-infinity',              '5 — Paradox & Depth',       a.paradox)}
    `;
  } catch (err) {
    modal.querySelector('#cs-deep-body').innerHTML =
      `<div style="color:var(--error);padding:20px">Error: ${escHtml(String(err.message))}</div>`;
  }
}

// ── Card renderer ─────────────────────────────────────────────────────────────

function csField(label, value) {
  if (!value) return '';
  return `<div class="cs-row"><dt>${label}</dt><dd>${escHtml(String(value))}</dd></div>`;
}

function csAvatarHtml(s) {
  const hue = Math.abs((s.name || '?').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) % 360, 0));
  if (s._image) {
    return `<div class="cs-avatar cs-avatar-img" data-name="${escHtml(s.name)}" title="Click to change profile picture">
      <img src="${s._image}" alt="${escHtml(s.name)}">
      <div class="cs-avatar-overlay"><span class="mdi mdi-camera"></span></div>
    </div>`;
  }
  return `<div class="cs-avatar cs-avatar-letter" style="background:hsl(${hue},50%,35%)" data-name="${escHtml(s.name)}" title="Click to upload profile picture">
    <span>${escHtml((s.name || '?')[0].toUpperCase())}</span>
    <div class="cs-avatar-overlay"><span class="mdi mdi-camera"></span></div>
  </div>`;
}

function csCardHtml(s) {
  const inv = (s.inventory || []).filter(Boolean);
  const invHtml = inv.length
    ? `<div class="cs-row"><dt>Signature Items</dt><dd><ul>${inv.map(i => `<li>${escHtml(i)}</li>`).join('')}</ul></dd></div>`
    : '';
  const attrs = (s.attribute_high || s.attribute_low)
    ? `<div class="cs-row"><dt>Core Attributes</dt><dd>▲ ${escHtml(s.attribute_high || '—')} &nbsp;·&nbsp; ▼ ${escHtml(s.attribute_low || '—')}</dd></div>`
    : '';
  const sources = (s.sources || []).filter(x => x && (x.quote || x.page != null));
  const srcHtml = sources.length
    ? `<div class="cs-sources"><span class="mdi mdi-book-open-page-variant-outline"></span> ${
        sources.map(x =>
          `${x.line_hint ? `<span class="cs-src-hint">[${escHtml(x.line_hint)}]</span> ` : ''}` +
          `${x.page != null ? `<b>p.${escHtml(String(x.page))}</b> ` : ''}` +
          `${x.quote ? `"${escHtml(x.quote)}"` : ''}`
        ).join(' · ')
      }</div>`
    : '';

  return `<div class="cs-card cs-${s.tier === 'main' ? 'main' : 'supp'}" data-name="${escHtml(s.name)}">
    <div class="cs-card-top">
      ${csAvatarHtml(s)}
      <div class="cs-card-top-body">
        <div class="cs-head">
          <span class="cs-name">${escHtml(s.name)}</span>
          ${s.archetype ? `<span class="cs-archetype">${escHtml(s.archetype)}</span>` : ''}
          <span class="cs-tier">${s.tier === 'main' ? 'Main' : 'Supporting'}</span>
          <div class="cs-head-btns">
            <button class="btn-secondary btn-sm cs-img-btn" data-name="${escHtml(s.name)}" title="Copy image-gen prompt for ComfyUI / SD"><span class="mdi mdi-image-outline"></span> Img prompt</button>
            <button class="btn-secondary btn-sm cs-voice-btn" data-name="${escHtml(s.name)}" title="Copy voice design prompt"><span class="mdi mdi-account-voice"></span> Voice prompt</button>
            <button class="btn-secondary btn-sm cs-deep-btn" data-name="${escHtml(s.name)}" title="5-area psychological deep analysis"><span class="mdi mdi-brain"></span> Deep</button>
          </div>
        </div>
        ${s.aliases ? `<div class="cs-aliases">aka ${escHtml(s.aliases)}</div>` : ''}
        ${csAlignmentBar(s.moral_alignment_score, s.arc_direction, s.arc_note)}
      </div>
    </div>
    <dl class="cs-fields">
      ${csField('Physical', s.physical)}
      ${csField('Clothing & Appearance', s.clothing)}
      ${csField('Alignment & Ethos', s.alignment)}
      ${attrs}
      ${csField('Trained Skills', s.skills)}
      ${csField('Capabilities', s.capabilities)}
      ${invHtml}
      ${csField('Backstory & Origin', s.backstory)}
      ${csField('Relationships', s.relationships)}
      ${csField('Motivation', s.motivation)}
      ${csField('Fears', s.fears)}
      ${csField('Mannerisms & Habits', s.mannerisms)}
      ${s.voice_pattern ? `<div class="cs-row cs-row-voice"><dt><span class="mdi mdi-account-voice"></span> Voice &amp; Speech</dt><dd>${escHtml(String(s.voice_pattern))}</dd></div>` : ''}
      ${csField('Dark Secret / Fatal Flaw', s.secret)}
      ${csField('Conflict Style', s.conflict_style)}
      ${csField('Win Condition', s.win_condition)}
    </dl>
    ${srcHtml}
  </div>`;
}

function csToMarkdown(sheets) {
  const sec = t => `\n## ${t}\n`;
  let md = '# Character Sheets\n';
  for (const tier of ['main', 'supp']) {
    const list = sheets.filter(s => (s.tier === 'main') === (tier === 'main'));
    if (!list.length) continue;
    md += sec(tier === 'main' ? 'Main characters' : 'Supporting characters');
    for (const s of list) {
      md += `\n### ${s.name}${s.archetype ? ' — ' + s.archetype : ''}\n`;
      if (s.aliases) md += `*aka ${s.aliases}*\n`;
      const mas = s.moral_alignment_score ?? 50;
      const masLabel = mas >= 70 ? 'Good' : mas <= 30 ? 'Evil' : 'Neutral';
      md += `- **Moral alignment:** ${mas}/100 (${masLabel}) — Arc: ${s.arc_direction || 'neutral'}\n`;
      if (s.arc_note) md += `  *${s.arc_note}*\n`;
      const f = (l, v) => v ? `- **${l}:** ${v}\n` : '';
      md += f('Physical', s.physical) + f('Clothing', s.clothing)
        + f('Alignment & Ethos', s.alignment)
        + f('Core Attributes', [s.attribute_high && '▲ ' + s.attribute_high, s.attribute_low && '▼ ' + s.attribute_low].filter(Boolean).join(' · '))
        + f('Trained Skills', s.skills) + f('Capabilities', s.capabilities)
        + f('Backstory & Origin', s.backstory) + f('Relationships', s.relationships)
        + f('Motivation', s.motivation) + f('Fears', s.fears)
        + f('Mannerisms & Habits', s.mannerisms) + f('Voice & Speech', s.voice_pattern)
        + f('Signature Items', (s.inventory || []).join(', '))
        + f('Dark Secret / Fatal Flaw', s.secret)
        + f('Conflict Style', s.conflict_style) + f('Win Condition', s.win_condition);
      const src = (s.sources || []).filter(x => x && x.quote)
        .map(x => `${x.line_hint ? '[' + x.line_hint + '] ' : ''}${x.page != null ? 'p.' + x.page + ' ' : ''}"${x.quote}"`).join('; ');
      if (src) md += `- *Sources:* ${src}\n`;
    }
  }
  return md.trim();
}

async function csShow(sheets, title, sourceText, book) {
  document.getElementById('cs-overlay')?.remove();

  // Enrich sheets with stored profile images from the character library.
  if (typeof clGetAll === 'function') {
    try {
      const bk = book || title || '';
      const all = await clGetAll();
      const imgMap = new Map(all.filter(r => r.image).map(r => [r.id, r.image]));
      sheets.forEach(s => {
        const id = `${bk}::${s.name}`.toLowerCase();
        if (imgMap.has(id)) s._image = imgMap.get(id);
      });
    } catch (_) {}
  }

  const ov = document.createElement('div');
  ov.id = 'cs-overlay'; ov.className = 'audiobook-overlay';
  const main = sheets.filter(s => s.tier === 'main');
  const supp = sheets.filter(s => s.tier !== 'main');
  const group = (label, list) => list.length
    ? `<div class="cs-group-label">${label}</div>` + list.map(csCardHtml).join('')
    : '';
  ov.innerHTML = `<div class="audiobook-box cs-box">
    <div class="cs-titlebar">
      <span class="audiobook-title"><span class="mdi mdi-account-details-outline"></span>
        ${escHtml(title || 'Character sheets')}
        <span class="audiobook-count">${sheets.length} character${sheets.length !== 1 ? 's' : ''}</span>
      </span>
      <span style="flex:1"></span>
      <button class="btn-secondary btn-sm" id="cs-copy"><span class="mdi mdi-content-copy"></span> Copy</button>
      <button class="btn-secondary btn-sm" id="cs-close">Close</button>
    </div>
    <div class="cs-list">${group('Main characters', main)}${group('Supporting characters', supp)}</div>
  </div>`;
  document.body.appendChild(ov);
  ov.querySelector('#cs-close').addEventListener('click', () => ov.remove());
  ov.querySelector('#cs-copy').addEventListener('click', () => {
    navigator.clipboard?.writeText(csToMarkdown(sheets))
      .then(() => toast('Copied as Markdown', 'success'), () => toast('Copy failed', 'error'));
  });
  ov.addEventListener('click', e => { if (e.target === ov) ov.remove(); });

  // Wire avatar upload (click on avatar → file picker → save to library)
  const bkCtx = book || title || '';
  ov.querySelectorAll('.cs-avatar').forEach(av => {
    av.addEventListener('click', e => {
      e.stopPropagation();
      const name = av.dataset.name;
      const inp = document.createElement('input');
      inp.type = 'file'; inp.accept = 'image/*';
      inp.onchange = async () => {
        const file = inp.files[0]; if (!file) return;
        const reader = new FileReader();
        reader.onload = async ev => {
          const dataUrl = ev.target.result;
          const id = `${bkCtx}::${name}`.toLowerCase();
          if (typeof clSetImage === 'function') await clSetImage(id, dataUrl);
          // Update in-memory sheet and re-render avatar
          const s = sheets.find(x => x.name === name);
          if (s) s._image = dataUrl;
          const card = ov.querySelector(`.cs-card[data-name="${CSS.escape(name)}"]`);
          if (card) {
            const oldAv = card.querySelector('.cs-avatar');
            if (oldAv) { oldAv.outerHTML = csAvatarHtml(s); card.querySelector('.cs-avatar').addEventListener('click', av.onclick); }
          }
          toast('Profile picture saved', 'success');
        };
        reader.readAsDataURL(file);
      };
      inp.click();
    });
  });

  // Wire Deep Analysis buttons
  ov.querySelectorAll('.cs-deep-btn').forEach(btn => {
    btn.addEventListener('click', e => {
      e.stopPropagation();
      const sheet = sheets.find(s => s.name === btn.dataset.name);
      if (sheet) csDeepAnalysis(sheet, sourceText);
    });
  });

  // Wire Image prompt buttons
  ov.querySelectorAll('.cs-img-btn').forEach(btn => {
    btn.addEventListener('click', e => {
      e.stopPropagation();
      const sheet = sheets.find(s => s.name === btn.dataset.name);
      if (!sheet) return;
      navigator.clipboard?.writeText(csBuildImagePrompt(sheet))
        .then(() => toast('Image prompt copied', 'success'), () => toast('Copy failed', 'error'));
    });
  });

  // Wire Voice prompt buttons
  ov.querySelectorAll('.cs-voice-btn').forEach(btn => {
    btn.addEventListener('click', e => {
      e.stopPropagation();
      const sheet = sheets.find(s => s.name === btn.dataset.name);
      if (!sheet) return;
      navigator.clipboard?.writeText(csBuildVoicePrompt(sheet))
        .then(() => toast('Voice prompt copied — paste into Design a Voice', 'success'), () => toast('Copy failed', 'error'));
    });
  });
}

// ── Entry points (reader + rehearser) ────────────────────────────────────────

// Persist a fresh batch of sheets into the Character Library (auto-save).
async function csSaveToLibrary(book, sheets) {
  if (typeof clUpsertMany !== 'function') return;
  try {
    const n = await clUpsertMany(book, sheets);
    if (n) toast(`${n} character${n !== 1 ? 's' : ''} saved to library`, 'success');
  } catch (e) { /* non-fatal — the overlay still works */ }
}

function csGoToLibrary() {
  if (typeof navTo === 'function') navTo('s-library');
  if (typeof navLibraryView === 'function') navLibraryView('characters');
  else if (typeof libraryRender === 'function') libraryRender('characters');
}

async function csForReader() {
  const text = csReaderText();
  const book = readerState.title || 'Untitled book';
  const key = 'reader:' + (readerState.title || '') + ':' + (typeof readerScopeIndices === 'function' ? readerScopeIndices().length : 0);
  if (_cs.cache[key]) { csGoToLibrary(); return; }
  const knownRoster = (window._audiobook && _audiobook.roster && _audiobook.roster.length)
    ? _audiobook.roster.slice() : null;
  const sheets = await csGenerate(text, key, knownRoster);
  if (!sheets) return;
  if (!sheets.length) { toast('No characters found', 'error'); return; }
  await csSaveToLibrary(book, sheets);
  csGoToLibrary();
  toast(sheets.length + ' character sheets saved — Library → Characters / Cast', 'success');
}

async function csForRehearser() {
  const title = $('reh-script-title')?.value.trim() || 'Character sheets';
  const book  = $('reh-script-title')?.value.trim() || 'Untitled script';
  const text  = csRehearserText();
  const key   = 'reh:' + title + ':' + ((rehState.lines || []).length);
  if (_cs.cache[key]) { csGoToLibrary(); return; }
  const sheets = await csGenerate(text, key);
  if (!sheets) return;
  if (!sheets.length) { toast('No characters found', 'error'); return; }
  await csSaveToLibrary(book, sheets);
  csGoToLibrary();
  toast(sheets.length + ' character sheets saved — Library → Characters / Cast', 'success');
}

window.csForReader     = csForReader;
window.csForRehearser  = csForRehearser;
$('reader-charsheets-btn')?.addEventListener('click', csForReader);
$('reh-charsheets-btn')?.addEventListener('click', csForRehearser);
