/* ============================================================
   SCRIPT & BREAKDOWN — import script, tag elements per scene
   Paste or upload a script (Fountain/plain). Scene headings
   (INT./EXT./EST.) are parsed into a scene list; each scene gets
   element tags by department (cast, props, wardrobe, SFX…). Rolls
   up element counts per department. Persisted per project.
   ============================================================ */

const SCRIPT_KEY = 'silverlines.script.' + (window.PROJ_ID || 'crimson') + '.v1';

const BD_CATS = [
  { id:'cast',     label:'Cast',        color:'#F5A742', icon:'User' },
  { id:'extras',   label:'Background',  color:'#9B8BE8', icon:'Users' },
  { id:'props',    label:'Props',       color:'#22D48A', icon:'Package' },
  { id:'wardrobe', label:'Wardrobe',    color:'#5FB0E8', icon:'Shirt' },
  { id:'vehicles', label:'Vehicles',    color:'#FF6FA5', icon:'Car' },
  { id:'sfx',      label:'SFX / Stunt', color:'#FF4D6D', icon:'Zap' },
  { id:'sound',    label:'Sound',       color:'#E0B341', icon:'Music' },
  { id:'art',      label:'Set Dressing',color:'#7CC4A0', icon:'Armchair' },
];
const BD_BY_ID = Object.fromEntries(BD_CATS.map(c => [c.id, c]));

const SAMPLE_SCRIPT = `INT. COCKPIT — DAWN
The instrument panel glows. LENA FROST (38), flight suit zipped to the throat, flips the ignition switch. A low whine builds.

EXT. TARMAC — DAWN
Lena strides toward the aircraft, helmet under one arm. Ground crew scatter. Wind whips the windsock.

INT. HANGAR 7 — DAY
Vast and silent. A single work light. Lena runs a gloved hand along the fuselage.

EXT. RUNWAY — MAGIC HOUR
The aircraft lifts. Crimson light floods the frame. Lena's face, calm, behind the visor.`;

/* split raw script into scenes on slug lines */
const isSlug = line => /^\s*(INT|EXT|EST|INT\.?\/EXT|I\/E)[\.\/ ]/i.test(line);
function parseScript(text, prev = []) {
  const lines = (text || '').split(/\r?\n/);
  const scenes = [];
  let cur = null;
  lines.forEach(line => {
    if (isSlug(line)) {
      if (cur) scenes.push(cur);
      cur = { heading: line.trim(), body: '' };
    } else if (cur) {
      cur.body += (cur.body ? '\n' : '') + line;
    }
  });
  if (cur) scenes.push(cur);
  return scenes.map((s, i) => {
    const match = prev.find(p => p.heading === s.heading);
    return { id: 's' + i + '-' + Date.now(), heading: s.heading, body: s.body.trim(), tags: match ? match.tags : {} };
  });
}

const SCRIPT_SEED = (() => {
  const sc = parseScript(SAMPLE_SCRIPT);
  if (sc[0]) sc[0].tags = { cast:['Lena Frost'], wardrobe:['Flight suit'], props:['Ignition switch'], sound:['Engine whine'] };
  if (sc[1]) sc[1].tags = { cast:['Lena Frost'], extras:['Ground crew ×4'], vehicles:['Aircraft'], props:['Helmet'], sfx:['Wind FX'] };
  if (sc[2]) sc[2].tags = { cast:['Lena Frost'], wardrobe:['Gloves'], art:['Work light'], vehicles:['Aircraft'] };
  if (sc[3]) sc[3].tags = { cast:['Lena Frost'], wardrobe:['Helmet/visor'], vehicles:['Aircraft'], sfx:['Practical flare'] };
  return { text: SAMPLE_SCRIPT, scenes: sc };
})();

function scriptLoad() {
  const s = window.SLStore.get(SCRIPT_KEY); if (s && Array.isArray(s.scenes)) return s;
  return window.PROJ_BLANK ? { text: '', scenes: [] } : SCRIPT_SEED;
}

/* ============================================================
   SCENE LINKAGE — connect script scenes to storyboard scenes so
   breakdown tags flow into the schedule, crew call and pull lists.
   Script scene ids regenerate on every re-parse, so links are keyed
   by heading + occurrence (with an index fallback) — they survive
   re-imports and inserted scenes. Keyed by storyboard scene no.
   ============================================================ */
const SCENELINK_KEY = 'silverlines.scenelink.' + (window.PROJ_ID || 'crimson') + '.v1';
function scenelinkLoad() { const s = window.SLStore.get(SCENELINK_KEY); return (s && s.links) ? s : { v: 1, auto: false, links: {} }; }
function scenelinkSave(o) { window.SLStore.set(SCENELINK_KEY, o); try { window.dispatchEvent(new CustomEvent('sl-scenelink')); } catch (e) {} }

const SL_STOP = new Set(['the', 'and', 'for', 'with', 'into', 'from', 'out', 'day', 'night', 'dawn', 'dusk', 'morning', 'evening', 'continuous', 'later', 'cont', 'int', 'ext', 'est']);
function slugTokens(h) {
  return String(h || '').replace(/^\s*(int|ext|est|i\/e|int\/ext)[.\/ ]+/i, '').split(/[—–\-]/)[0]
    .toLowerCase().replace(/[^a-z0-9 ]/g, ' ').split(/\s+/).filter(w => w.length > 2 && !SL_STOP.has(w));
}
function plainTokens(str) {
  return String(str || '').toLowerCase().replace(/[^a-z0-9 ]/g, ' ').split(/\s+/).filter(w => w.length > 2 && !SL_STOP.has(w));
}
function slJaccard(a, b) {
  if (!a.length || !b.length) return 0;
  const A = new Set(a), B = new Set(b); let inter = 0;
  A.forEach(x => { if (B.has(x)) inter++; });
  return inter / (A.size + B.size - inter);
}
function slOccurrence(scenes, ix) { const h = scenes[ix].heading; let o = 0; for (let i = 0; i < ix; i++) if (scenes[i].heading === h) o++; return o; }
function resolveScriptScene(link, scenes) {
  if (!link) return null;
  const same = scenes.filter(s => s.heading === link.h);
  if (same.length) return same[link.occ] || same[0];
  const at = scenes[link.ix];
  if (at && slJaccard(slugTokens(at.heading), slugTokens(link.h)) >= 0.5) return at;
  return null;
}

window.scriptDoc = scriptLoad;
window.sceneLinkMap = function () { return scenelinkLoad().links; };
window.resolveScriptScene = resolveScriptScene;
/* the linked script scene's breakdown tags for a storyboard scene no, or null */
window.scriptTagsFor = function (sbNo) {
  try {
    const link = scenelinkLoad().links[sbNo]; if (!link) return null;
    const sc = resolveScriptScene(link, scriptLoad().scenes); return sc ? (sc.tags || {}) : null;
  } catch (e) { return null; }
};
/* manually (re)pair or clear a storyboard scene's script link */
window.linkScene = function (sbNo, scriptIx) {
  const doc = scriptLoad(); const store = scenelinkLoad();
  if (scriptIx == null || scriptIx < 0 || !doc.scenes[scriptIx]) delete store.links[sbNo];
  else { const s = doc.scenes[scriptIx]; store.links[sbNo] = { h: s.heading, occ: slOccurrence(doc.scenes, scriptIx), ix: scriptIx, conf: 'manual' }; }
  scenelinkSave(store);
};
/* auto-match: Fountain #num# → token similarity → equal-run order fallback */
window.autoLinkScenes = function (sbScenes) {
  const sc = scriptLoad().scenes; const links = {}; const usedScript = new Set();
  const byNo = {}; sbScenes.forEach(s => { byNo[String(s.no).toLowerCase()] = s; });
  sc.forEach((s, ix) => {
    const m = /#\s*([a-z0-9]+)\s*#\s*$/i.exec(s.heading);
    if (m && byNo[m[1].toLowerCase()]) { const sb = byNo[m[1].toLowerCase()]; if (!links[sb.no]) { links[sb.no] = { h: s.heading, occ: slOccurrence(sc, ix), ix, conf: 'exact' }; usedScript.add(ix); } }
  });
  const pairs = [];
  sbScenes.forEach((sb, bi) => {
    if (links[sb.no]) return; const bt = plainTokens((sb.loc || '') + ' ' + (sb.title || ''));
    sc.forEach((s, ix) => {
      if (usedScript.has(ix)) return; let score = slJaccard(slugTokens(s.heading), bt);
      if (sbScenes.length > 1 && sc.length > 1 && Math.abs(ix / (sc.length - 1) - bi / (sbScenes.length - 1)) < 0.15) score += 0.25;
      if (score > 0) pairs.push({ score, sbNo: sb.no, ix });
    });
  });
  pairs.sort((a, b) => b.score - a.score);
  const takenSb = new Set(Object.keys(links));
  pairs.forEach(p => {
    if (p.score < 0.34 || takenSb.has(String(p.sbNo)) || usedScript.has(p.ix)) return;
    links[p.sbNo] = { h: sc[p.ix].heading, occ: slOccurrence(sc, p.ix), ix: p.ix, conf: 'high' }; takenSb.add(String(p.sbNo)); usedScript.add(p.ix);
  });
  const remSb = sbScenes.filter(s => !takenSb.has(String(s.no)));
  const remSc = sc.map((s, ix) => ({ s, ix })).filter(x => !usedScript.has(x.ix));
  if (remSb.length && remSb.length === remSc.length) remSb.forEach((sb, k) => { const r = remSc[k]; links[sb.no] = { h: r.s.heading, occ: slOccurrence(sc, r.ix), ix: r.ix, conf: 'order' }; });
  scenelinkSave({ v: 1, auto: true, links });
  return { linked: Object.keys(links).length, total: sbScenes.length };
};

const slugTone = h => /^\s*EXT/i.test(h) ? '#3FA46A' : '#5E7CE0';

function TagField({ cat, tags, onAdd, onRemove, readOnly }) {
  const [draft, setDraft] = useState('');
  const add = () => { const v = draft.trim(); if (v) { onAdd(v); setDraft(''); } };
  return (
    <div className="flex items-start gap-2 py-1.5">
      <span className="mt-0.5 flex w-[108px] shrink-0 items-center gap-1.5 text-[11px] font-600" style={{ color: cat.color }}>
        <Icon name={cat.icon} size={12} />{cat.label}
      </span>
      <div className="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
        {(tags || []).map((t, i) => (
          <span key={i} className="group/tag inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[11px] font-600"
            style={{ borderColor: cat.color + '55', background: cat.color + '14', color: 'rgb(var(--z200))' }}>
            {t}
            {!readOnly && <button onClick={() => onRemove(i)} className="text-zinc-500 hover:text-[#FF4D6D]"><Icon name="X" size={10} /></button>}
          </span>
        ))}
        {!readOnly && (
          <span className="inline-flex items-center">
            <input value={draft} onChange={e => setDraft(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') add(); }}
              placeholder="+ tag" className="w-20 rounded-md border border-line bg-ink-900 px-1.5 py-0.5 text-[11px] text-zinc-200 outline-none focus:w-28 focus:border-electric/60" />
          </span>
        )}
      </div>
    </div>
  );
}

/* per-scene chip showing (and re-pairing) which storyboard scene this links to */
function SceneLinkChip({ sbNo, sbScenes, scriptIx, readOnly }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => { if (!open) return; const h = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, [open]);
  if (readOnly && !sbNo) return null;
  return (
    <div ref={ref} className="relative shrink-0">
      <button onClick={() => !readOnly && setOpen(o => !o)} title="Link to a storyboard scene"
        className={cx('inline-flex items-center gap-1 rounded-lg border px-2 py-1.5 text-[11px] font-700',
          sbNo ? 'border-electric/40 bg-electric/10 text-electric-soft' : 'border-dashed border-ink-600 text-zinc-500 hover:text-zinc-300')}>
        <Icon name="Link2" size={12} />{sbNo ? ('SB ' + sbNo) : 'Link'}{!readOnly && <Icon name="ChevronDown" size={9} />}
      </button>
      {open && (
        <div className="absolute right-0 z-30 mt-1 max-h-56 w-52 overflow-y-auto rounded-xl border border-line bg-ink-850 p-1.5 shadow-xl shadow-black/40">
          {sbNo && <button onClick={() => { window.linkScene(sbNo, null); setOpen(false); }} className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-[12px] text-zinc-400 hover:bg-ink-800"><Icon name="Unlink" size={12} /> Unlink</button>}
          {sbScenes.length === 0 && <div className="px-2 py-2 text-[11.5px] text-zinc-500">No storyboard scenes yet.</div>}
          {sbScenes.map(sb => (
            <button key={sb.no} onClick={() => { window.linkScene(sb.no, scriptIx); setOpen(false); }}
              className={cx('flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-[12px] hover:bg-ink-800', String(sb.no) === String(sbNo) ? 'font-700 text-electric-soft' : 'text-zinc-200')}>
              <span className="font-mono text-[10px] text-zinc-500">{sb.no}</span><span className="truncate">{sb.title}</span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

function ScriptScene({ s, idx, onTag, onUntag, readOnly, sbNo, sbScenes }) {
  const [open, setOpen] = useState(false);
  const count = Object.values(s.tags || {}).reduce((a, arr) => a + (arr ? arr.length : 0), 0);
  return (
    <Panel pad={false} className="overflow-hidden">
      <div className="flex items-center gap-3 border-b border-line p-3">
        <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md font-mono text-[12px] font-700 text-white" style={{ background: slugTone(s.heading) }}>{idx + 1}</span>
        <div className="min-w-0 flex-1">
          <div className="truncate font-mono text-[12.5px] font-700 text-head">{s.heading}</div>
          <div className="text-[11px] text-zinc-500">{count} element{count === 1 ? '' : 's'} tagged</div>
        </div>
        <SceneLinkChip sbNo={sbNo} sbScenes={sbScenes || []} scriptIx={idx} readOnly={readOnly} />
        <button onClick={() => setOpen(o => !o)} className="flex shrink-0 items-center gap-1 rounded-lg border border-line bg-ink-900 px-2.5 py-1.5 text-[11.5px] font-600 text-zinc-400 hover:text-zinc-200">
          <Icon name={open ? 'ChevronUp' : 'ListChecks'} size={13} />{open ? 'Hide' : 'Breakdown'}
        </button>
      </div>
      {s.body && <div className="whitespace-pre-wrap border-b border-line bg-ink-900/40 px-3 py-2.5 text-[12px] leading-relaxed text-zinc-400">{s.body}</div>}
      {open && (
        <div className="fade-up divide-y divide-line/60 p-3">
          {BD_CATS.map(cat => (
            <TagField key={cat.id} cat={cat} tags={(s.tags || {})[cat.id]} readOnly={readOnly}
              onAdd={v => onTag(s.id, cat.id, v)} onRemove={i => onUntag(s.id, cat.id, i)} />
          ))}
        </div>
      )}
    </Panel>
  );
}

function ScriptModule({ mobile, readOnly = false }) {
  const [doc, setDocState] = useState(scriptLoad);
  const setDoc = next => setDocState(prev => {
    const v = typeof next === 'function' ? next(prev) : next;
    window.SLStore.set(SCRIPT_KEY, v);
    return v;
  });
  const [draft, setDraft] = useState('');
  const [importing, setImporting] = useState(false);
  const fileRef = useRef(null);
  const [, force] = useState(0);
  useEffect(() => { const h = () => force(n => n + 1); window.addEventListener('sl-scenelink', h); return () => window.removeEventListener('sl-scenelink', h); }, []);

  /* storyboard scenes + which script scene each is linked to (reverse of the map) */
  const sbScenes = window.sbLoad ? window.sbLoad() : [];
  const linkMap = scenelinkLoad().links;
  const scIxToSb = {};
  Object.keys(linkMap).forEach(sbNo => { const sc = resolveScriptScene(linkMap[sbNo], doc.scenes); if (sc) { const ix = doc.scenes.indexOf(sc); if (ix >= 0 && scIxToSb[ix] == null) scIxToSb[ix] = sbNo; } });
  const linkedCount = Object.keys(scIxToSb).length;

  const doParse = txt => { const t = txt != null ? txt : draft; if (!t.trim()) return; setDoc(d => ({ text: t, scenes: parseScript(t, d.scenes) })); setImporting(false); setDraft(''); window.toast && window.toast('Script parsed into ' + parseScript(t).length + ' scenes', 'FileText'); };
  const onFile = e => { const f = e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = () => doParse(String(r.result)); r.readAsText(f); e.target.value = ''; };

  const tag = (sid, cid, v) => setDoc(d => ({ ...d, scenes: d.scenes.map(s => s.id !== sid ? s : { ...s, tags: { ...s.tags, [cid]: [...((s.tags || {})[cid] || []), v] } }) }));
  const untag = (sid, cid, idx) => setDoc(d => ({ ...d, scenes: d.scenes.map(s => s.id !== sid ? s : { ...s, tags: { ...s.tags, [cid]: ((s.tags || {})[cid] || []).filter((_, i) => i !== idx) } }) }));

  const counts = BD_CATS.map(c => ({ ...c, n: doc.scenes.reduce((a, s) => a + (((s.tags || {})[c.id] || []).length), 0) }));
  const totalEls = counts.reduce((a, c) => a + c.n, 0);

  return (
    <div className="fade-up">
      {!readOnly && <SectionTitle kicker="Pre-Production · Script" icon="ScrollText" title="Script & Breakdown"
        action={<div className="flex items-center gap-2.5">
          <Chip tone="neutral"><Icon name="Film" size={12} /> {doc.scenes.length} scenes</Chip>
          <Chip tone="electric"><Icon name="Tags" size={12} /> {totalEls} elements</Chip>
          {doc.scenes.length > 0 && <Chip tone={linkedCount ? 'mint' : 'neutral'}><Icon name="Link2" size={12} /> {linkedCount} linked</Chip>}
          {!readOnly && doc.scenes.length > 0 && <button onClick={() => { const r = window.autoLinkScenes(sbScenes); force(n => n + 1); window.toast && window.toast('Linked ' + r.linked + ' of ' + r.total + ' storyboard scenes', 'Link2'); }}
            className="flex items-center gap-2 rounded-xl border border-line bg-ink-900 px-3.5 py-2.5 text-[12.5px] font-700 text-zinc-200 transition-colors hover:border-electric/50 hover:text-electric-soft"><Icon name="Link2" size={15} /> Auto-match storyboard</button>}
          {!readOnly && <button onClick={() => setImporting(v => !v)} className="flex items-center gap-2 rounded-xl bg-cta px-3.5 py-2.5 text-[12.5px] font-700 text-cta-ink transition-colors hover:bg-cta-hover"><Icon name="Upload" size={15} /> Import script</button>}
        </div>} />}

      {/* element rollup */}
      <Panel className="mb-5">
        <div className="grid grid-cols-4 gap-2.5 sm:grid-cols-8">
          {counts.map(c => (
            <div key={c.id} className="rounded-xl border border-line bg-ink-900 p-2.5 text-center">
              <Icon name={c.icon} size={15} className="mx-auto" style={{ color: c.color }} />
              <div className="mt-1 font-display text-[18px] font-700 leading-none text-head">{c.n}</div>
              <div className="mt-0.5 text-[10px] text-zinc-500">{c.label}</div>
            </div>
          ))}
        </div>
      </Panel>

      {/* importer */}
      {importing && !readOnly && (
        <Panel className="mb-5">
          <div className="mb-2 flex items-center justify-between">
            <h3 className="font-display text-[14px] font-600 text-head">Paste or upload your script</h3>
            <div className="flex items-center gap-2">
              <input ref={fileRef} type="file" accept=".txt,.fountain,.fdx,.md" className="hidden" onChange={onFile} />
              <button onClick={() => fileRef.current && fileRef.current.click()} className="flex items-center gap-1.5 rounded-lg border border-line bg-ink-900 px-2.5 py-1.5 text-[12px] font-600 text-zinc-300 hover:text-zinc-100"><Icon name="FileUp" size={13} /> Upload file</button>
            </div>
          </div>
          <textarea value={draft} onChange={e => setDraft(e.target.value)} rows={8}
            placeholder={'Paste Fountain or plain text…\n\nINT. COCKPIT — DAWN\nAction goes here.'}
            className="w-full resize-y rounded-xl border border-line bg-ink-900 p-3 font-mono text-[12px] leading-relaxed text-zinc-200 outline-none focus:border-electric/60" />
          <div className="mt-2 flex items-center gap-2">
            <button onClick={() => doParse()} disabled={!draft.trim()}
              className={cx('flex items-center gap-2 rounded-lg px-3.5 py-2 text-[12.5px] font-700 transition-colors', draft.trim() ? 'bg-cta text-cta-ink hover:bg-cta-hover' : 'cursor-not-allowed bg-ink-700 text-zinc-500')}>
              <Icon name="Wand2" size={14} /> Parse into scenes</button>
            <button onClick={() => setDraft(SAMPLE_SCRIPT)} className="text-[12px] font-600 text-zinc-500 hover:text-zinc-300">Load sample</button>
            <span className="text-[11.5px] text-zinc-600">Splits on INT./EXT. slug lines · existing tags are kept by heading</span>
          </div>
        </Panel>
      )}

      {/* scenes */}
      {doc.scenes.length === 0 ? (
        <Panel><div className="flex flex-col items-center gap-2 py-10 text-center"><Icon name="ScrollText" size={24} className="text-zinc-600" /><div className="text-[12.5px] text-zinc-500">No script yet — import one to start tagging.</div></div></Panel>
      ) : (
        <div className="space-y-3">
          {doc.scenes.map((s, i) => (
            <ScriptScene key={s.id} s={s} idx={i} readOnly={readOnly} onTag={tag} onUntag={untag} sbNo={scIxToSb[i]} sbScenes={sbScenes} />
          ))}
        </div>
      )}
    </div>
  );
}

window.ScriptModule = ScriptModule;
