/* The six section builders. Each shows how the AI assists the user with that
   specific bible section: agent identity, what it grounds in, open proposals,
   accept/reject flow, quick-prompt chips. */

/* ============ CHARACTERS ============ */
const CharactersBuilder = ({ characters: initial, onOpen, projectId, isLive }) => {
  const [adding, setAdding] = React.useState(false);
  const [draft, setDraft] = React.useState(emptyDraft());
  const [localChars, setLocalChars] = React.useState(initial);
  React.useEffect(() => { setLocalChars(initial); }, [initial]);
  const characters = localChars;

  function emptyDraft() {
    return { name: '', role: 'Supporting', archetype: '', motivation: '', vulnerability: '', voice: '' };
  }

  const onAddCharacter = async () => {
    if (!draft.name.trim() || !draft.motivation.trim()) return;
    const optimistic = {
      id: `char_local_${Date.now().toString(36)}`,
      name: draft.name.trim(),
      role: draft.role || 'Supporting',
      archetype: draft.archetype.trim() || 'new_role',
      age: 0, pronouns: '',
      summary: draft.motivation.trim(),
      motivation: draft.motivation.trim(),
      vulnerability: draft.vulnerability.trim(),
      voice: draft.voice.trim(),
      flaw: '', continuity_notes: [], signature_moves: [], appearance: {},
    };
    setLocalChars((cur) => [...cur, optimistic]);
    setAdding(false);
    const submitted = { ...draft };
    setDraft(emptyDraft());
    if (isLive && projectId) {
      try {
        await window.CinematonAPI.addCharacter(projectId, {
          name: submitted.name.trim(),
          role: submitted.role,
          archetype: submitted.archetype.trim() || undefined,
          motivation: submitted.motivation.trim(),
          vulnerability: submitted.vulnerability.trim() || undefined,
          voice: submitted.voice.trim() || undefined,
        });
        window.dispatchEvent(new CustomEvent('cinematon:refresh-run'));
      } catch (err) {
        alert('Failed to add character on server: ' + err.message);
        setLocalChars((cur) => cur.filter((cc) => cc.id !== optimistic.id));
      }
    }
  };

  const onDeleteCharacter = async (id, name) => {
    if (!confirm(`Delete character "${name}"?`)) return;
    setLocalChars((cur) => cur.filter((c) => c.id !== id));
    if (isLive) {
      try {
        await window.CinematonAPI.deleteCharacter(id);
        window.dispatchEvent(new CustomEvent('cinematon:refresh-run'));
      } catch (err) { alert('Failed to delete character: ' + err.message); }
    }
  };

  return (
  <div>
    <BuilderBar
      agent="Character agent · horror.v1"
      grounded="brief · genre archetypes · v1.3"
      status={{ label: `${characters.length} in canon`, tone: "ok" }}
      placeholder="e.g. 'Add a third character — a former patient who returned'"
    />

    <div style={{ padding: 24 }}>
      <SectionHead eyebrow="In canon" title="Characters" count={characters.length}>
        <button className="btn ghost"><Icon name="regen"/> Repropose all</button>
        <button className="btn" onClick={() => setAdding(true)}><span style={{ fontSize: 14, lineHeight: 1 }}>+</span> New character</button>
      </SectionHead>

      {adding && (
        <div className="card" style={{ marginBottom: 16, borderColor: 'var(--accent-dim)' }}>
          <div className="card-title">New character {isLive ? '· live' : '· fixture'}</div>
          <div className="col gap-sm" style={{ marginTop: 8 }}>
            <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', gap: 8 }}>
              <input className="input" placeholder="Name *" value={draft.name} onChange={(e) => setDraft({...draft, name: e.target.value})} autoFocus/>
              <select className="input" value={draft.role} onChange={(e) => setDraft({...draft, role: e.target.value})}>
                <option>Protagonist</option><option>Supporting</option><option>Antagonist</option><option>Threat</option>
              </select>
              <input className="input" placeholder="Archetype" value={draft.archetype} onChange={(e) => setDraft({...draft, archetype: e.target.value})}/>
            </div>
            <textarea className="textarea" placeholder="Motivation (1-2 sentences) *" rows={2} value={draft.motivation} onChange={(e) => setDraft({...draft, motivation: e.target.value})}/>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
              <input className="input" placeholder="Vulnerability — genre hook" value={draft.vulnerability} onChange={(e) => setDraft({...draft, vulnerability: e.target.value})}/>
              <input className="input" placeholder="Voice — how the audience reads them" value={draft.voice} onChange={(e) => setDraft({...draft, voice: e.target.value})}/>
            </div>
            <div className="row gap-sm" style={{ justifyContent: 'flex-end', marginTop: 6 }}>
              <button className="btn ghost" onClick={() => { setAdding(false); setDraft(emptyDraft()); }}>Cancel</button>
              <button className="btn primary" onClick={onAddCharacter} disabled={!draft.name.trim() || !draft.motivation.trim()}>Create</button>
            </div>
            <div className="dim mono" style={{ fontSize: 10 }}>* required · all other fields editable after creation</div>
          </div>
        </div>
      )}

      <div className="char-grid">
        {characters.map(c => (
          <div key={c.id} className="char-card" style={{ position: 'relative' }}>
            {isLive && (
              <button
                onClick={(e) => { e.stopPropagation(); onDeleteCharacter(c.id, c.name); }}
                title="Delete character"
                style={{ position: 'absolute', top: 8, right: 8, zIndex: 5,
                  background: 'rgba(0,0,0,0.6)', border: 'none', color: 'var(--fg-faint)',
                  cursor: 'pointer', fontSize: 14, padding: '4px 8px', borderRadius: 3 }}>⌫</button>
            )}
            <button className="char-card-inner" onClick={() => onOpen(c)} style={{ all: 'unset', cursor: 'pointer', display: 'contents' }}>
            <div className="char-card-portrait">
              <div className="thumb-placeholder" data-label={c.name.split(" ")[0]}/>
              <div className="thumb-noir"/>
              <span className="char-card-role">{c.role}</span>
            </div>
            <div className="char-card-body">
              <div className="row" style={{ justifyContent: "space-between", alignItems: "baseline", gap: 12 }}>
                <h3 className="serif" style={{ margin: 0, fontSize: 22, fontWeight: 500, whiteSpace: "nowrap" }}>{c.name}</h3>
                <span className="mono dim" style={{ fontSize: 10.5, whiteSpace: "nowrap" }}>{c.id}</span>
              </div>
              <div className="dim mono" style={{ fontSize: 11, margin: "2px 0 8px" }}>
                age {c.age} · {c.archetype.replace(/_/g, " ")}
              </div>
              <p className="char-card-summary">{c.summary}</p>

              <div className="char-card-strip">
                <div><span className="lbl">motivation</span><span>{c.motivation}</span></div>
                <div><span className="lbl">flaw</span><span>{c.flaw}</span></div>
                <div><span className="lbl">voice</span><span className="serif">"{c.voice}"</span></div>
              </div>

              <div className="char-card-foot">
                <span className="tag">{c.continuity_notes.length} continuity notes</span>
                <span className="tag">{c.signature_moves.length} signature moves</span>
                <span className="char-card-cta">Open detail <Icon name="chev-r" size={10}/></span>
              </div>
            </div>
            </button>
          </div>
        ))}

        <button className="char-card add">
          <Sparkle size={18}/>
          <div className="serif" style={{ fontSize: 18, marginTop: 8 }}>Propose a new character</div>
          <div className="dim" style={{ fontSize: 12, marginTop: 4, maxWidth: 260, textAlign: "center" }}>
            The agent will draft a treatment grounded in the brief and the existing cast.
          </div>
        </button>
      </div>

      <hr className="hr" style={{ margin: "28px 0 18px" }}/>

      <SectionHead eyebrow="AI proposals" title="Open" count={2}/>

      <Proposal
        kind="Character agent"
        title="Add a third character: 'The voice'"
        body={
          <p>
            Your brief mentions Mara hearing her own voice in the final image. Right now no character represents that — it's
            attributed to <span className="mono">threat_humming_child</span>. Consider promoting it to a separate character
            <i> "The Voice"</i> (offscreen-only, voice asset, no reference image). Lets you track its lines as dialogue packets
            and bind a distinct TTS reference instead of an SFX.
          </p>
        }
        diff={[
          { op: "add", field: "characters[2].name", value: "The Voice" },
          { op: "add", field: "characters[2].appearance.identifying_marks", value: "(none — heard only)" },
          { op: "add", field: "characters[2].voice_asset_id", value: "voice_mara_v3 (re-pitched -20¢, room reverb)" },
        ]}
        refs={["brief.fear_engine", "screenplay.sc_05", "voice_mara_v3"]}
      />

      <Proposal
        kind="Continuity agent"
        title="Hess's keys are inconsistent in scene 3"
        body={<p>His ring of keys is described as 18 keys with one always missing. Scene 3 dialogue ("checked the door") implies he produces a key. Either the missing key is the door's, or Mara opens it. Pick one to lock to canon.</p>}
        diff={[
          { op: "mod", field: "char_orderly.props[0]", value: "ring of 18 keys, one always missing — the missing one fits Room 4B" },
        ]}
        refs={["sc_03", "char_orderly.props"]}
      />
    </div>
  </div>
  );
};

/* ============ THREAT ============ */
// PRD §12.4 threat_profile: name, type, motivation, visibility_strategy,
// escalation_pattern, limitations. All editable via PATCH /api/threats/:id.
const ThreatBuilder = ({ threat, brief, isLive }) => {
  const [local, setLocal] = React.useState(threat);
  React.useEffect(() => { setLocal(threat); }, [threat?.id]);

  const patch = (field, value) => {
    setLocal((cur) => ({ ...cur, [field]: value }));
    if (isLive && local.id && window.CinematonAPI) {
      window.CinematonAPI.patchThreat(local.id, { [field]: value })
        .catch((err) => console.error('threat patch failed', err));
    }
  };

  return (
    <div>
      <BuilderBar
        agent="Threat agent · horror.v1"
        grounded={`brief · subgenre: ${brief.horror_subgenre || "supernatural"}`}
        status={{ label: isLive ? "live · saves on blur" : "fixture · local only", tone: "ok" }}
        placeholder="e.g. 'Make the rule one sentence shorter'"
      />

      <div style={{ padding: 24 }}>
        <SectionHead eyebrow="Threat profile" title={local.name || "Untitled threat"}>
          <Aperture value={brief.threat_visibility} size={28}/>
          <span className="tag accent">{local.type || "spirit"}</span>
        </SectionHead>

        {/* Name + type — top-line identifiers */}
        <div className="threat-grid" style={{ marginBottom: 18 }}>
          <div className="threat-cell">
            <div className="cd-field-lbl">Name</div>
            <EditableField value={local.name || ""} serif onSave={(v) => patch("name", v)}/>
          </div>
          <div className="threat-cell">
            <div className="cd-field-lbl">Type</div>
            <EditableField
              value={local.type || ""}
              placeholder="spirit | creature | human | environment | technology | unknown | internal"
              onSave={(v) => patch("type", v)}
            />
          </div>
        </div>

        {/* Behavioral fields */}
        <div className="threat-grid">
          <div className="threat-cell">
            <div className="cd-field-lbl">Motivation</div>
            <EditableField value={local.motivation || ""} serif multiline autosize onSave={(v) => patch("motivation", v)}/>
          </div>
          <div className="threat-cell">
            <div className="cd-field-lbl">Visibility strategy</div>
            <EditableField value={local.visibility_strategy || ""} multiline autosize onSave={(v) => patch("visibility_strategy", v)}/>
          </div>
          <div className="threat-cell">
            <div className="cd-field-lbl">Escalation pattern</div>
            <EditableField value={local.escalation || local.escalation_pattern || ""} multiline autosize onSave={(v) => patch("escalation_pattern", v)}/>
          </div>
          <div className="threat-cell">
            <div className="cd-field-lbl">Limitations</div>
            <EditableField value={local.limitations || ""} multiline autosize onSave={(v) => patch("limitations", v)}/>
          </div>
        </div>

        <div className="card" style={{ marginTop: 18 }}>
          <div className="card-title">Escalation curve</div>
          <div className="dim" style={{ fontSize: 11.5, margin: "4px 0 12px" }}>
            Visual reference. Each scene card's tension_curve binds independently — edit per-scene on the Outline screen.
          </div>
          <ol className="threat-rungs">
            <li><span>1</span> Distance — threat felt at the edge of the frame.</li>
            <li><span>2</span> Proximity — threat is on the other side of a barrier.</li>
            <li><span>3</span> Recognition — character sees something they wish they hadn't.</li>
            <li><span>4</span> Address — threat acknowledges the character directly.</li>
          </ol>
        </div>
      </div>
    </div>
  );
};

/* ============ LOCATIONS ============ */
// PRD §13/§18 LocationTreatment: name, description, spatial_notes, continuity_notes.
// Multi-location: full POST/PATCH/DELETE wired.
const LocationBuilder = ({ location, projectId, isLive, locations, refs }) => {
  // `locations` is the full list from the API (or [] in fixture mode);
  // `location` is the SAMPLE-shaped singleton we always have for back-compat.
  const initial = (Array.isArray(locations) && locations.length)
    ? locations
    : (location ? [{
        id: location.id, name: location.name,
        description: location.summary,
        spatial_notes: location.spatial_notes,
        continuity_notes: location.continuity_notes || [],
      }] : []);

  const [list, setList] = React.useState(initial);
  React.useEffect(() => { setList(initial); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [JSON.stringify(initial)]);

  const [adding, setAdding] = React.useState(false);
  const [draft, setDraft] = React.useState({ name: "", description: "" });
  const [regen, setRegen] = React.useState({}); // locId -> boolean

  const regenLocRef = async (id) => {
    if (!isLive || !window.CinematonAPI) return;
    setRegen((r) => ({ ...r, [id]: true }));
    try {
      await window.CinematonAPI.regenLocationReference(id);
      window.dispatchEvent(new CustomEvent('cinematon:refresh-run'));
    } catch (err) { console.error('location reference regen failed', err); }
    finally { setRegen((r) => ({ ...r, [id]: false })); }
  };

  const updateLoc = (id, patch) => {
    setList((cur) => cur.map((l) => l.id === id ? { ...l, ...patch } : l));
    if (isLive && window.CinematonAPI) {
      window.CinematonAPI.patchLocation(id, patch)
        .catch((err) => console.error('location patch failed', err));
    }
  };

  const removeLoc = (id) => {
    if (!confirm('Delete this location?')) return;
    setList((cur) => cur.filter((l) => l.id !== id));
    if (isLive && window.CinematonAPI) {
      window.CinematonAPI.deleteLocation(id)
        .then(() => window.dispatchEvent(new CustomEvent('cinematon:refresh-run')))
        .catch((err) => console.error('location delete failed', err));
    }
  };

  const createLoc = async () => {
    if (!draft.name.trim() || !draft.description.trim()) return;
    const optimistic = {
      id: `loc_local_${Date.now().toString(36)}`,
      name: draft.name.trim(),
      description: draft.description.trim(),
      spatial_notes: "",
      continuity_notes: [],
    };
    setList((cur) => [...cur, optimistic]);
    setAdding(false); setDraft({ name: "", description: "" });
    if (isLive && projectId && window.CinematonAPI) {
      try {
        await window.CinematonAPI.addLocation(projectId, {
          name: optimistic.name, description: optimistic.description,
          continuity_notes: [],
        });
        window.dispatchEvent(new CustomEvent('cinematon:refresh-run'));
      } catch (err) {
        alert('Failed to create location: ' + err.message);
        setList((cur) => cur.filter((l) => l.id !== optimistic.id));
      }
    }
  };

  return (
    <div>
      <BuilderBar
        agent="Location agent · horror.v1"
        grounded="brief · world rules · canon"
        status={{ label: `${list.length} location${list.length === 1 ? "" : "s"} in canon`, tone: "ok" }}
        placeholder="e.g. 'Add a parking-lot location for the cold open'"
      />

      <div style={{ padding: 24 }}>
        <SectionHead eyebrow="Locations" title="In canon" count={list.length}>
          <button className="btn" onClick={() => setAdding(true)}>+ New location</button>
        </SectionHead>

        {adding && (
          <div className="card" style={{ marginBottom: 16, borderColor: "var(--accent-dim)" }}>
            <div className="card-title">New location</div>
            <div className="col gap-sm" style={{ marginTop: 8 }}>
              <input className="input" placeholder="Name *" value={draft.name} onChange={(e) => setDraft({...draft, name: e.target.value})} autoFocus/>
              <textarea className="textarea" placeholder="Description *" rows={2} value={draft.description} onChange={(e) => setDraft({...draft, description: e.target.value})}/>
              <div className="row gap-sm" style={{ justifyContent: "flex-end" }}>
                <button className="btn ghost" onClick={() => { setAdding(false); setDraft({ name: "", description: "" }); }}>Cancel</button>
                <button className="btn primary" onClick={createLoc} disabled={!draft.name.trim() || !draft.description.trim()}>Create</button>
              </div>
            </div>
          </div>
        )}

        {list.length === 0 && (
          <div className="card" style={{ padding: 28, textAlign: "center", color: "var(--fg-muted)" }}>
            No locations yet. Click <em>+ New location</em>.
          </div>
        )}

        {list.map((loc) => {
          const ref = refs && refs[loc.id];
          const isRegen = !!regen[loc.id];
          return (
          <div key={loc.id} className="card" style={{ marginBottom: 16, position: "relative", display: 'grid', gridTemplateColumns: '160px 1fr', gap: 16 }}>
            <button
              onClick={() => removeLoc(loc.id)}
              title="Delete location"
              style={{ position: "absolute", top: 12, right: 12, background: "transparent", border: "none", color: "var(--fg-faint)", cursor: "pointer", fontSize: 14 }}>⌫</button>

            <div>
              <div style={{ width: 160, height: 90, borderRadius: 4, overflow: 'hidden', background: 'var(--bg-inset)', border: '1px solid var(--line)' }}>
                {ref && window.CinematonAPI ? (
                  <img src={window.CinematonAPI.assetFileUrl(ref.asset_id)} alt={`reference for ${loc.name}`}
                       style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
                       onError={(e) => { e.currentTarget.style.display = 'none'; }}/>
                ) : (
                  <div style={{ width: '100%', height: '100%', display: 'grid', placeItems: 'center', color: 'var(--fg-faint)', fontSize: 10, fontFamily: 'var(--font-mono)' }}>NO REFERENCE</div>
                )}
              </div>
              <button className="btn ghost sm" style={{ width: '100%', justifyContent: 'center', marginTop: 6 }}
                      onClick={() => regenLocRef(loc.id)} disabled={!isLive || isRegen}>
                <Icon name="regen" size={10}/> {isRegen ? '…' : 'Regen reference'}
              </button>
            </div>

            <div>
            <div style={{ marginBottom: 10 }}>
              <div className="cd-field-lbl">Name</div>
              <EditableField value={loc.name || ""} serif onSave={(v) => updateLoc(loc.id, { name: v })}/>
              <div className="dim mono" style={{ fontSize: 10, marginTop: 2 }}>{loc.id}</div>
            </div>

            <div style={{ marginBottom: 10 }}>
              <div className="cd-field-lbl">Description</div>
              <EditableField value={loc.description || ""} multiline autosize onSave={(v) => updateLoc(loc.id, { description: v })}/>
            </div>

            <div style={{ marginBottom: 10 }}>
              <div className="cd-field-lbl">Spatial notes</div>
              <EditableField
                value={loc.spatial_notes || ""}
                multiline autosize
                placeholder="Camera angles, blocking constraints"
                onSave={(v) => updateLoc(loc.id, { spatial_notes: v })}/>
            </div>

            <div>
              <div className="cd-field-lbl">Continuity notes</div>
              <EditableField
                value={(loc.continuity_notes || []).join("\n")}
                multiline autosize
                placeholder="One item per line"
                onSave={(v) => updateLoc(loc.id, { continuity_notes: v.split("\n").map((s) => s.trim()).filter(Boolean) })}/>
            </div>
            </div>
          </div>
          );
        })}
      </div>
    </div>
  );
};

/* ============ WORLD RULES ============ */
// Bound to bible.genre_fields.world_rules: string[]. PATCH /api/projects/:id/bible.
const WorldRulesBuilderLive = ({ bible, projectId, isLive }) => {
  const initial = (bible && bible.genre_fields && Array.isArray(bible.genre_fields.world_rules))
    ? bible.genre_fields.world_rules
    : (bible && bible.genre_fields && bible.genre_fields.horror_rule)
      ? [bible.genre_fields.horror_rule]
      : [];
  const [rules, setRules] = React.useState(initial);
  React.useEffect(() => { setRules(initial); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [JSON.stringify(initial)]);

  const persist = (next) => {
    setRules(next);
    if (isLive && projectId && window.CinematonAPI) {
      const merged = { ...((bible && bible.genre_fields) || {}), world_rules: next };
      window.CinematonAPI.patchBible(projectId, { genre_fields: merged })
        .catch((err) => console.error('bible patch failed', err));
    }
  };

  const update = (i, v) => persist(rules.map((r, idx) => idx === i ? v : r));
  const remove = (i) => persist(rules.filter((_, idx) => idx !== i));
  const add    = () => persist([...rules, "New rule — describe what is true in this world."]);

  return (
    <div>
      <BuilderBar
        agent="Mini-bible agent"
        grounded="brief · threat · location"
        status={{ label: `${rules.length} rule${rules.length === 1 ? "" : "s"} in canon`, tone: "ok" }}
        placeholder="e.g. 'Add a rule about the elevator'"
      />
      <div style={{ padding: 24 }}>
        <SectionHead eyebrow="World rules" title="Hard constraints" count={rules.length}>
          <button className="btn" onClick={add}>+ New rule</button>
        </SectionHead>

        <ol className="rule-list">
          {rules.map((rule, i) => (
            <li key={i}>
              <div className="rule-num mono">{String(i + 1).padStart(2, "0")}</div>
              <div className="rule-body">
                <div className="rule-text">
                  <EditableField value={rule} multiline autosize serif onSave={(v) => update(i, v)}/>
                </div>
                <div className="rule-meta">
                  <span className="tag good">in canon</span>
                </div>
              </div>
              <div className="rule-actions">
                <button className="btn ghost sm" onClick={() => remove(i)}>⌫</button>
              </div>
            </li>
          ))}
        </ol>

        {rules.length === 0 && (
          <div className="dim" style={{ padding: 16, fontStyle: "italic" }}>No world rules yet. Click + New rule.</div>
        )}
      </div>
    </div>
  );
};

// Legacy fixture-only signature kept so screen-bible.jsx can still pass nothing.
const WorldRulesBuilder = () => (
  <div>
    <BuilderBar
      agent="Mini-bible agent"
      grounded="brief · threat · location"
      status={{ label: "3 rules in canon · 1 draft", tone: "warn" }}
      placeholder="e.g. 'Add a rule about the elevator' or 'Make rule #2 more specific'"
    />
    <div style={{ padding: 24 }}>
      <SectionHead eyebrow="World rules" title="Hard constraints" count={3}>
        <button className="btn ghost"><Icon name="regen"/> Repropose</button>
        <button className="btn">+ New rule</button>
      </SectionHead>

      <ol className="rule-list">
        <li>
          <div className="rule-num mono">01</div>
          <div className="rule-body">
            <div className="rule-text">The east wing is on the building plans, but Room 4B is not.</div>
            <div className="rule-meta"><span className="tag good">in canon</span> <span className="dim mono">since v1.0.0 · referenced by 14 shots</span></div>
          </div>
          <div className="rule-actions"><button className="btn ghost sm"><Icon name="regen" size={10}/> Refine</button><button className="btn ghost sm">Edit</button></div>
        </li>
        <li>
          <div className="rule-num mono">02</div>
          <div className="rule-body">
            <div className="rule-text">The threat only manifests when the floor is empty.</div>
            <div className="rule-meta"><span className="tag good">in canon</span> <span className="dim mono">since v1.1.0 · referenced by 9 shots</span></div>
          </div>
          <div className="rule-actions"><button className="btn ghost sm"><Icon name="regen" size={10}/> Refine</button><button className="btn ghost sm">Edit</button></div>
        </li>
        <li>
          <div className="rule-num mono">03</div>
          <div className="rule-body">
            <div className="rule-text">No character has died on-screen in the building's recorded history; this is the canon limit.</div>
            <div className="rule-meta"><span className="tag good">in canon</span> <span className="dim mono">since v1.0.0 · referenced by 0 shots</span></div>
          </div>
          <div className="rule-actions"><button className="btn ghost sm"><Icon name="regen" size={10}/> Refine</button><button className="btn ghost sm">Edit</button></div>
        </li>
      </ol>

      <hr className="hr" style={{ margin: "24px 0 16px" }}/>
      <SectionHead eyebrow="Drafts" title="AI proposed" count={1}/>
      <Proposal
        kind="Mini-bible agent"
        title="Propose: rule #4 — the ledger has the last word"
        body={<p>Two scenes hinge on what Mara writes in the paper ledger. Promoting that to a world rule ("only what is written in the ledger is true; what is erased is gone") gives the threat a mechanic — it lives in the gap between a room and its erasure — and it gives Mara a way to act in the climax (write Room 4B back in).</p>}
        diff={[{ op: "add", field: "world_rules[3]", value: "Only what is written in the ledger is true; what is erased is gone." }]}
        refs={["sc_02", "sc_05", "char_mara.props[0]"]}
      />
    </div>
  </div>
);

/* ============ MOTIFS ============ */
// Bound to bible.genre_fields.visual_motifs and sound_motifs (string[]).
const MotifsBuilderLive = ({ bible, projectId, isLive }) => {
  const initVisual = (bible && bible.genre_fields && Array.isArray(bible.genre_fields.visual_motifs))
    ? bible.genre_fields.visual_motifs : [];
  const initSound = (bible && bible.genre_fields && Array.isArray(bible.genre_fields.sound_motifs))
    ? bible.genre_fields.sound_motifs : [];

  const [visual, setVisual] = React.useState(initVisual);
  const [sound, setSound]   = React.useState(initSound);
  React.useEffect(() => { setVisual(initVisual); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [JSON.stringify(initVisual)]);
  React.useEffect(() => { setSound(initSound);   /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [JSON.stringify(initSound)]);

  const persist = (key, list) => {
    if (isLive && projectId && window.CinematonAPI) {
      const merged = { ...((bible && bible.genre_fields) || {}), [key]: list };
      window.CinematonAPI.patchBible(projectId, { genre_fields: merged })
        .catch((err) => console.error('bible patch failed', err));
    }
  };

  const updateVisual = (next) => { setVisual(next); persist('visual_motifs', next); };
  const updateSound  = (next) => { setSound(next);  persist('sound_motifs',  next); };

  return (
    <div>
      <BuilderBar
        agent="Motifs agent"
        grounded="threat · location · screenplay"
        status={{ label: `${visual.length + sound.length} motif${visual.length + sound.length === 1 ? "" : "s"}`, tone: "ok" }}
        placeholder="e.g. 'Find a motif that ties Mara to the threat'"
      />
      <div style={{ padding: 24 }}>
        <SectionHead eyebrow="Visual motifs" title="" count={visual.length}>
          <button className="btn" onClick={() => updateVisual([...visual, "new visual motif"])}>+ Add</button>
        </SectionHead>
        <div className="motif-grid">
          {visual.map((m, i) => (
            <div key={i} className="motif-card visual">
              <div className="motif-glyph">
                <div className="thumb-placeholder" data-label="◯"/>
                <div className="thumb-noir"/>
              </div>
              <div style={{ flex: 1 }}>
                <EditableField value={m} serif onSave={(v) => updateVisual(visual.map((x, idx) => idx === i ? v : x))}/>
              </div>
              <button className="btn ghost sm" onClick={() => updateVisual(visual.filter((_, idx) => idx !== i))}>⌫</button>
            </div>
          ))}
        </div>

        <SectionHead eyebrow="Sound motifs" title="" count={sound.length}>
          <button className="btn" onClick={() => updateSound([...sound, "new sound motif"])}>+ Add</button>
        </SectionHead>
        <div className="motif-list">
          {sound.map((m, i) => (
            <div key={i} className="motif-row">
              <span className="mono dim" style={{ fontSize: 11, minWidth: 36 }}>S{String(i + 1).padStart(2, "0")}</span>
              <div style={{ flex: 1 }}>
                <EditableField value={m} serif onSave={(v) => updateSound(sound.map((x, idx) => idx === i ? v : x))}/>
              </div>
              <button className="btn ghost sm" onClick={() => updateSound(sound.filter((_, idx) => idx !== i))}>⌫</button>
            </div>
          ))}
        </div>

        {(visual.length + sound.length) === 0 && (
          <div className="dim" style={{ padding: 16, fontStyle: "italic" }}>No motifs yet. Add one above.</div>
        )}
      </div>
    </div>
  );
};

// Legacy fixture builder retained for screen-bible.jsx fallback.
const MotifsBuilder = ({ location }) => {
  const visual = ["fluorescent flicker", "wax-yellow exit signs", "shadow under door", "fogged-glass writing", "tile seams that don't align", "still shadows in motion"];
  const sound  = ["child humming", "monitor pings in sequence", "fluorescent ballast hum", "tile-creak under unseen weight", "breath on glass"];
  return (
    <div>
      <BuilderBar
        agent="Motifs agent"
        grounded="threat · location · screenplay v3"
        status={{ label: `${visual.length + sound.length} motifs in canon`, tone: "ok" }}
        placeholder="e.g. 'Find a motif that ties Mara to the threat' or 'Drop the weakest visual motif'"
      />
      <div style={{ padding: 24 }}>
        <SectionHead eyebrow="Visual motifs" title="" count={visual.length}>
          <button className="btn ghost sm"><Icon name="regen" size={10}/> Repropose</button>
        </SectionHead>
        <div className="motif-grid">
          {visual.map(m => (
            <div key={m} className="motif-card visual">
              <div className="motif-glyph">
                <div className="thumb-placeholder" data-label="◯"/>
                <div className="thumb-noir"/>
              </div>
              <div className="serif" style={{ fontSize: 16 }}>{m}</div>
              <div className="dim mono" style={{ fontSize: 10.5, marginTop: 2 }}>used in {2 + Math.floor(Math.random()*5)} shots</div>
            </div>
          ))}
          <div className="motif-card add">
            <Sparkle size={18}/>
            <div style={{ marginTop: 6 }}>Propose visual motif</div>
          </div>
        </div>

        <SectionHead eyebrow="Sound motifs" title="" count={sound.length}>
          <button className="btn ghost sm"><Icon name="regen" size={10}/> Repropose</button>
        </SectionHead>
        <div className="motif-list">
          {sound.map((m, i) => (
            <div key={m} className="motif-row">
              <span className="mono dim" style={{ fontSize: 11 }}>S{String(i+1).padStart(2,"0")}</span>
              <div className="motif-wave">
                {Array.from({ length: 36 }).map((_, j) => {
                  const h = 4 + Math.abs(Math.sin(j * 0.7 + i)) * 14 + Math.abs(Math.cos(j * 0.31 + i)) * 8;
                  return <span key={j} style={{ height: h }}/>;
                })}
              </div>
              <div className="serif" style={{ fontSize: 14, flex: 1 }}>{m}</div>
              <span className="tag">{1 + Math.floor(Math.random()*5)} cues</span>
              <button className="btn ghost sm"><Icon name="play" size={10}/></button>
            </div>
          ))}
        </div>

        <Proposal
          kind="Motifs agent"
          title="One motif is doing two jobs"
          body={<p>"Shadow under door" and "still shadows in motion" overlap. Recommend folding them into a single motif: <i>"shadows that don't follow"</i> — keeps the most distinctive image and reads as one rule for the cinematographer.</p>}
          diff={[
            { op: "del", field: "visual_motifs[5]", value: "still shadows in motion" },
            { op: "mod", field: "visual_motifs[2]", value: "shadows that don't follow" },
          ]}
          refs={["sc_01", "sc_03"]}
        />
      </div>
    </div>
  );
};

/* ============ FORBIDDEN ============ */
// Bound to bible.genre_fields.forbidden_action (singular) + forbidden_changes (array)
// + brief.genre_fields.taboo_constraints / .avoid (read-only).
const ForbiddenBuilderLive = ({ bible, brief, projectId, isLive }) => {
  const gf = (bible && bible.genre_fields) || {};
  const initAction = gf.forbidden_action || "";
  const initChanges = Array.isArray(gf.forbidden_changes) ? gf.forbidden_changes : [];
  const taboo = (brief && brief.genre_fields && Array.isArray(brief.genre_fields.avoid)) ? brief.genre_fields.avoid : [];

  const [action, setAction] = React.useState(initAction);
  const [changes, setChanges] = React.useState(initChanges);
  React.useEffect(() => { setAction(initAction); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [initAction]);
  React.useEffect(() => { setChanges(initChanges); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [JSON.stringify(initChanges)]);

  const persist = (next) => {
    if (isLive && projectId && window.CinematonAPI) {
      const merged = { ...gf, ...next };
      window.CinematonAPI.patchBible(projectId, { genre_fields: merged })
        .catch((err) => console.error('bible patch failed', err));
    }
  };

  const saveAction = (v) => { setAction(v); persist({ forbidden_action: v }); };
  const updateChanges = (next) => { setChanges(next); persist({ forbidden_changes: next }); };

  return (
    <div>
      <BuilderBar
        agent="Continuity & rights agent"
        grounded="brief.taboo_constraints · brief.avoid · canon"
        status={{ label: `${changes.length} hard rule${changes.length === 1 ? "" : "s"}`, tone: "ok" }}
        placeholder="e.g. 'Forbid any music with vocals'"
      />
      <div style={{ padding: 24 }}>
        <SectionHead eyebrow="Forbidden action" title="The line the threat enforces">
        </SectionHead>
        <div className="card" style={{ marginBottom: 18 }}>
          <EditableField
            value={action} serif multiline autosize
            placeholder="e.g. 'Acknowledging the humming makes it follow.'"
            onSave={saveAction}
          />
        </div>

        <SectionHead eyebrow="Forbidden changes" title="Story-level guardrails" count={changes.length}>
          <button className="btn" onClick={() => updateChanges([...changes, "New rule"])}>+ New rule</button>
        </SectionHead>

        <div className="forbid-list">
          {changes.map((rule, i) => (
            <div key={i} className="forbid-row">
              <div className="forbid-mark"><Icon name="warn" size={14}/></div>
              <div className="forbid-body">
                <div className="forbid-text">
                  <EditableField value={rule} serif multiline autosize onSave={(v) => updateChanges(changes.map((r, idx) => idx === i ? v : r))}/>
                </div>
                <div className="forbid-meta">
                  <span className="tag good"><Icon name="check" size={10}/> auto-injected</span>
                </div>
              </div>
              <div className="row gap-sm">
                <button className="btn ghost sm" onClick={() => updateChanges(changes.filter((_, idx) => idx !== i))}>⌫</button>
              </div>
            </div>
          ))}
        </div>

        {changes.length === 0 && (
          <div className="dim" style={{ padding: 16, fontStyle: "italic" }}>No forbidden changes yet. Click + New rule.</div>
        )}

        {taboo.length > 0 && (
          <>
            <hr className="hr" style={{ margin: "24px 0 16px" }}/>
            <SectionHead eyebrow="Brief-level (auto)" title="From your avoid list">
              <span className="dim mono" style={{ fontSize: 11 }}>read-only — edit in brief</span>
            </SectionHead>
            <div className="forbid-tags">
              {taboo.map((t) => <span key={t} className="forbid-tag">{t}</span>)}
            </div>
          </>
        )}
      </div>
    </div>
  );
};

// Legacy fixture builder retained for screen-bible.jsx fallback.
const ForbiddenBuilder = () => (
  <div>
    <BuilderBar
      agent="Continuity & rights agent"
      grounded="brief.taboo_constraints · brief.avoid · canon v1.3"
      status={{ label: "2 hard rules · auto-injected to every prompt", tone: "ok" }}
      placeholder="e.g. 'Forbid any music with vocals' or 'Lock Mara's hair length'"
    />
    <div style={{ padding: 24 }}>
      <SectionHead eyebrow="Forbidden changes" title="Story-level guardrails" count={2}>
        <button className="btn ghost"><Icon name="regen"/> Repropose</button>
        <button className="btn">+ New rule</button>
      </SectionHead>

      <div className="forbid-list">
        <div className="forbid-row">
          <div className="forbid-mark"><Icon name="warn" size={14}/></div>
          <div className="forbid-body">
            <div className="forbid-text"><b>The threat is never seen on-screen.</b> Only heard, shadowed, partially reflected.</div>
            <div className="forbid-meta">
              <span className="tag good"><Icon name="check" size={10}/> auto-injected</span>
              <span className="dim mono">applied to 20 / 20 prompt packets</span>
            </div>
          </div>
          <div className="row gap-sm">
            <button className="btn ghost sm">Edit</button>
            <button className="btn ghost sm"><Icon name="regen" size={10}/></button>
          </div>
        </div>

        <div className="forbid-row">
          <div className="forbid-mark"><Icon name="warn" size={14}/></div>
          <div className="forbid-body">
            <div className="forbid-text"><b>No flashbacks.</b> Time is now-only. The past is referred to, never shown.</div>
            <div className="forbid-meta">
              <span className="tag good"><Icon name="check" size={10}/> auto-injected</span>
              <span className="dim mono">applied to 20 / 20 prompt packets</span>
            </div>
          </div>
          <div className="row gap-sm">
            <button className="btn ghost sm">Edit</button>
            <button className="btn ghost sm"><Icon name="regen" size={10}/></button>
          </div>
        </div>
      </div>

      <hr className="hr" style={{ margin: "24px 0 16px" }}/>
      <SectionHead eyebrow="Brief-level (auto)" title="From your taboo constraints">
        <span className="dim mono" style={{ fontSize: 11 }}>read-only — edit in brief</span>
      </SectionHead>
      <div className="forbid-tags">
        {["no real-person likeness", "no copyrighted music", "no brand logos", "no jump-scare clichés", "no explicit gore", "no child harm onscreen"].map(t => (
          <span key={t} className="forbid-tag">{t}</span>
        ))}
      </div>

      <hr className="hr" style={{ margin: "24px 0 16px" }}/>
      <Proposal
        kind="Continuity agent"
        title="Promote a fragile assumption to a rule"
        body={<p>You've been writing Mara's watch on the inside of her right wrist (3 references match). That's a continuity assumption masquerading as a habit. Promote to a forbidden change so any shot that loses or moves the watch is blocked at validation.</p>}
        diff={[{ op: "add", field: "forbidden_changes[2]", value: "Mara's watch is always on the inside of her right wrist." }]}
        refs={["char_mara.appearance", "ref_mara_001"]}
      />
    </div>
  </div>
);

/* tiny shared subcomponent */
const ProposeBtn = ({ agent, placeholder }) => (
  <button className="threat-propose">
    <Sparkle size={10}/> ask {agent.split(" ")[0].toLowerCase()}: <span className="dim">{placeholder}</span>
  </button>
);

Object.assign(window, { CharactersBuilder, ThreatBuilder, LocationBuilder, WorldRulesBuilder, MotifsBuilder, ForbiddenBuilder });
