// ── SillyTavern character-card compatibility ──────────────────────────────────
//
// Import and export characters in SillyTavern / TavernAI "Character Card" format
// so a cast built here can be moved to/from SillyTavern (and the wider ecosystem
// that follows the same spec: AI Character Editor, Chub, Agnai, …).
//
// Supported on import:
//   • V1 (legacy TavernAI): flat JSON  { name, description, personality, … }
//   • V2: { spec:'chara_card_v2', spec_version:'2.0', data:{ … } }
//   • V3: { spec:'chara_card_v3', data:{ … } }  (read; extra V3 fields preserved)
//   • PNG cards: JSON is base64 in a tEXt chunk (keyword 'chara' = V2, 'ccv3' = V3)
//
// Export is V2 JSON — the most widely accepted format. The assigned TTS voice is
// stored under data.extensions.tts_voice so a round-trip keeps the casting.

// ── PNG tEXt chunk reader ─────────────────────────────────────────────────────
// PNG = 8-byte signature then chunks of [len:4][type:4][data:len][crc:4].
// SillyTavern stores the card as base64 JSON in a tEXt chunk whose keyword is
// 'chara' (V2) or 'ccv3' (V3): data = keyword + 0x00 + base64text.
function stReadPngText(buf) {
  const dv = new DataView(buf);
  const sig = [137, 80, 78, 71, 13, 10, 26, 10];
  for (let i = 0; i < 8; i++) if (dv.getUint8(i) !== sig[i]) throw new Error('Not a PNG file');
  let off = 8;
  const out = {};
  while (off + 8 <= dv.byteLength) {
    const len = dv.getUint32(off); off += 4;
    let type = '';
    for (let i = 0; i < 4; i++) type += String.fromCharCode(dv.getUint8(off + i));
    off += 4;
    if (type === 'tEXt') {
      const bytes = new Uint8Array(buf, off, len);
      let sep = bytes.indexOf(0);
      if (sep === -1) sep = len;
      let keyword = '';
      for (let i = 0; i < sep; i++) keyword += String.fromCharCode(bytes[i]);
      const text = new TextDecoder('latin1').decode(bytes.subarray(sep + 1));
      out[keyword] = text;
    }
    off += len + 4; // skip data + CRC
    if (type === 'IEND') break;
  }
  return out;
}

function stB64ToJson(b64) {
  const bin = atob(b64.trim());
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return JSON.parse(new TextDecoder('utf-8').decode(bytes));
}

// Normalise any card variant into the V2-style "data" object.
function stNormalize(card) {
  if (!card || typeof card !== 'object') throw new Error('Invalid card JSON');
  if (card.data && typeof card.data === 'object' && card.data.name != null) return card.data; // V2/V3
  if (card.name != null) return card;                                                          // V1 flat
  throw new Error('Unrecognised character-card structure');
}

// Parse a File (.json or .png) into the normalised data object.
async function stParseFile(file) {
  const name = (file.name || '').toLowerCase();
  if (name.endsWith('.png')) {
    const buf = await file.arrayBuffer();
    const chunks = stReadPngText(buf);
    const raw = chunks['ccv3'] || chunks['chara'];
    if (!raw) throw new Error('PNG has no embedded character card');
    return stNormalize(stB64ToJson(raw));
  }
  // JSON (or anything else we try as text)
  const txt = await file.text();
  return stNormalize(JSON.parse(txt));
}

// ── Mapping: SillyTavern data → our character sheet ───────────────────────────
function stGuessGender(text) {
  const t = String(text || '').toLowerCase();
  const f = (t.match(/\b(she|her|female|woman|girl|mother|daughter|sister|queen|lady|mrs|miss)\b/g) || []).length;
  const m = (t.match(/\b(he|him|his|male|man|boy|father|son|brother|king|lord|mr|sir)\b/g) || []).length;
  if (f > m * 1.3) return 'female';
  if (m > f * 1.3) return 'male';
  return '';
}

function stToSheet(data) {
  const tagList = Array.isArray(data.tags) ? data.tags : String(data.tags || '').split(',');
  const tags = tagList.map(t => String(t || '').trim()).filter(Boolean).join(', ');
  const ext = data.extensions || {};
  return {
    name: (data.name || data.char_name || 'Unnamed').trim(),
    archetype: (data.creator_notes || '').split('\n')[0].slice(0, 60) || '',
    backstory: data.description || data.char_persona || '',
    mannerisms: data.personality || '',
    arc_note: data.scenario || '',
    voice_pattern: data.mes_example || data.example_dialogue || data.first_mes || data.char_greeting || '',
    gender: ext.gender || stGuessGender((data.description || '') + ' ' + (data.personality || '')),
    note: data.creator_notes || '',
    tags,
    voice: ext.tts_voice || null,
    tier: 'supporting',
    st_card: data, // full original, kept for lossless round-trip export
  };
}

// ── Mapping: our record → SillyTavern V2 card ─────────────────────────────────
function stFromRecord(rec) {
  const sh = rec.sheet || {};
  const prev = sh.st_card || {};
  const descParts = [sh.physical, sh.clothing, sh.backstory, sh.alignment]
    .map(x => String(x || '').trim()).filter(Boolean);
  const persona = [sh.archetype, sh.mannerisms, sh.motivation, sh.fears]
    .map(x => String(x || '').trim()).filter(Boolean).join('\n');
  return {
    spec: 'chara_card_v2',
    spec_version: '2.0',
    data: {
      name: rec.name,
      description: prev.description || descParts.join('\n\n'),
      personality: persona || prev.personality || '',
      scenario: rec.book || prev.scenario || '',
      first_mes: prev.first_mes || '',
      mes_example: sh.voice_pattern || prev.mes_example || '',
      creator_notes: 'Exported from TTS Voice Creator' + (rec.book ? ' · ' + rec.book : ''),
      system_prompt: prev.system_prompt || '',
      post_history_instructions: prev.post_history_instructions || '',
      tags: String(rec.tags || rec.book || '').split(',').map(t => t.trim()).filter(Boolean),
      creator: prev.creator || '',
      character_version: prev.character_version || '1.0',
      extensions: Object.assign({}, prev.extensions, { tts_voice: rec.voice || '' }),
    },
  };
}

function stDownloadJson(card, filename) {
  const blob = new Blob([JSON.stringify(card, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = filename;
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(a.href), 1000);
}

// ── Public actions ────────────────────────────────────────────────────────────
// Import one or more ST cards into a production (book). Returns count imported.
async function stImportCards(book, fileList) {
  let n = 0;
  for (const file of fileList) {
    try {
      const data = await stParseFile(file);
      const sheet = stToSheet(data);
      if (typeof clUpsert === 'function') {
        await clUpsert(book || sheet.name, Object.assign({}, sheet, { tags: book || '' }));
        n++;
      }
    } catch (e) {
      if (typeof toast === 'function') toast('“' + (file.name || 'card') + '”: ' + (e.message || e), 'error');
    }
  }
  return n;
}

// Trigger a file picker and import into `book`, then refresh the cast view.
function stImportDialog(book, onDone) {
  const inp = document.createElement('input');
  inp.type = 'file';
  inp.accept = '.json,.png';
  inp.multiple = true;
  inp.onchange = async () => {
    if (!inp.files.length) return;
    const n = await stImportCards(book, inp.files);
    if (typeof toast === 'function') toast(n ? 'Imported ' + n + ' character' + (n > 1 ? 's' : '') : 'Nothing imported', n ? 'success' : 'error');
    if (typeof onDone === 'function') onDone();
  };
  inp.click();
}

// Export a single character record as an ST V2 JSON card.
function stExportRecord(rec) {
  const card = stFromRecord(rec);
  const safe = String(rec.name || 'character').replace(/[^\w\- ]+/g, '').trim().replace(/\s+/g, '_') || 'character';
  stDownloadJson(card, safe + '.card.json');
}

window.stParseFile     = stParseFile;
window.stToSheet       = stToSheet;
window.stFromRecord    = stFromRecord;
window.stImportCards   = stImportCards;
window.stImportDialog  = stImportDialog;
window.stExportRecord  = stExportRecord;
