/* Composer — the primary screen. Two-pane: form (left) + live preview and
   rule panel (right). Everything writes straight through to the store so
   previews, rules and persistence stay in lock-step. */
(function () {
  const D = window.CROPR_DATA;
  const R = window.CroprRules;
  const S = window.CroprStore; // module-scoped store handle (used by Composer + helpers)

  const opt = (list) => list.map((x) => ({ value: x.id, label: x.label }));
  const PLAT_ICON = { x: "at-sign", linkedin: "briefcase", telegram: "send" };
  const PLAT_SHORT = { x: "X", linkedin: "in", telegram: "TG" };

  function CharRing({ n, min = 180, max = 220, hard = 280 }) {
    const pct = Math.min(n / hard, 1);
    const col = n > hard ? "#F04438" : (n < min || n > max) && n > 0 ? "#F79009" : "#12B76A";
    const r = 9, c = 2 * Math.PI * r;
    return (
      <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
        <svg width="24" height="24" style={{ transform: "rotate(-90deg)" }}>
          <circle cx="12" cy="12" r={r} fill="none" stroke="var(--border)" strokeWidth="2.5" />
          <circle cx="12" cy="12" r={r} fill="none" stroke={col} strokeWidth="2.5" strokeDasharray={c} strokeDashoffset={c * (1 - pct)} strokeLinecap="round" />
        </svg>
        <span style={{ fontSize: 11.5, fontWeight: 600, color: col, fontVariantNumeric: "tabular-nums" }}>{n}</span>
      </span>
    );
  }

  function ChannelToggle({ selected, onToggle }) {
    return (
      <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
        {D.CHANNEL_ORDER.map((ch) => {
          const m = D.CHANNELS[ch]; const on = selected.includes(ch);
          return (
            <button key={ch} onClick={() => onToggle(ch)} style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer",
              padding: "7px 12px 7px 8px", borderRadius: 999, fontFamily: "inherit", fontSize: 13, fontWeight: 600,
              border: `1px solid ${on ? "var(--accent)" : "var(--border-strong)"}`,
              background: on ? "var(--accent-weak)" : "var(--surface)", color: on ? "var(--accent)" : "var(--fg2)", transition: "all 140ms" }}>
              <Avatar color={m.avatar} initials={m.initials} src={m.icon} size={22} />
              {m.name}<PlatformLogo platform={m.platform} size={15} />
              {on && <Icon name="check" size={15} color="var(--accent)" />}
            </button>
          );
        })}
      </div>
    );
  }

  function MediaChips({ media, onChange }) {
    const S = window.CroprStore;
    const list = Array.isArray(media) ? media : [];
    const inputRef = React.useRef(null);
    const [pickerOpen, setPickerOpen] = React.useState(false);
    const onFiles = async (e) => {
      const files = Array.from(e.target.files || []);
      e.target.value = "";
      if (!files.length) return;
      // Host each upload → a public URL (offline/failure keeps the data URL).
      const added = [];
      for (const f of files) {
        const m = await window.LeapUpload.readAndHost(f);
        if (m) added.push({ kind: m.kind, altText: m.name, status: "ready", url: m.url });
      }
      if (added.length) onChange([...list, ...added]);
    };
    const toggleAsset = (a) => {
      if (list.some((m) => m.url === a.url)) { onChange(list.filter((m) => m.url !== a.url)); return; }
      if (S.useAsset) S.useAsset(a.id);
      onChange([...list, { kind: a.kind === "video" ? "video" : "image", altText: a.name, status: "ready", url: a.url, assetId: a.id }]);
    };
    return (
      <div style={{ marginTop: 10 }}>
        <input ref={inputRef} type="file" accept="image/*,video/*" multiple style={{ display: "none" }} onChange={onFiles} />
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
          <Button size="sm" variant="primary" icon="images" onClick={() => setPickerOpen(true)}>Choose from Assets</Button>
          <Button size="sm" variant="secondary" icon="cloud-upload" onClick={() => inputRef.current && inputRef.current.click()}>Upload</Button>
          <span style={{ fontSize: 10.5, color: "var(--fg4)" }}>reuse a library asset, or upload a new file</span>
        </div>
        <AssetPicker open={pickerOpen} onClose={() => setPickerOpen(false)} onToggle={toggleAsset} selectedUrls={list.map((m) => m.url)} />
        {list.length > 0 && (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 10 }}>
            {list.map((mm, i) => (
              <div key={i} style={{ width: 132, border: "1px solid var(--border)", borderRadius: 10, overflow: "hidden", background: "var(--bg-subtle)" }}>
                <div style={{ height: 74, background: "linear-gradient(135deg,#38BDF822,#FCA5A533)", display: "grid", placeItems: "center", position: "relative" }}>
                  {mm.url
                    ? (mm.kind === "video" ? <video src={mm.url} muted style={{ width: "100%", height: "100%", objectFit: "cover" }} /> : <img src={mm.url} alt="" style={{ width: "100%", height: "100%", objectFit: "cover" }} />)
                    : <Icon name={mm.kind === "video" ? "video" : "image"} size={20} color="var(--accent)" />}
                  {mm.kind === "video" && <span style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center" }}><Icon name="play" size={22} color="#fff" /></span>}
                  <button onClick={() => onChange(list.filter((_, j) => j !== i))} style={{ position: "absolute", top: 4, right: 4, border: 0, background: "rgba(0,0,0,.55)", borderRadius: 999, width: 20, height: 20, cursor: "pointer", display: "grid", placeItems: "center" }}><Icon name="x" size={12} color="#fff" /></button>
                </div>
                <input value={mm.altText} onChange={(e) => { const n = list.slice(); n[i] = { ...mm, altText: e.target.value }; onChange(n); }} placeholder="Alt text" style={{ width: "100%", boxSizing: "border-box", border: 0, background: "transparent", fontFamily: "inherit", fontSize: 11.5, color: "var(--fg1)", outline: "none", padding: "6px 8px" }} />
              </div>
            ))}
          </div>
        )}
      </div>
    );
  }

  function VariantEditor({ variant, onChange, hookType, pillar, series, brief, cta, postScheduleAt, hideChannelOptions, onToast }) {
    const m = D.CHANNELS[variant.channel];
    const plat = m.platform;
    const fmt = variant.format || D.defaultFormat(plat);
    const formats = D.FORMATS[plat] || D.FORMATS.x;
    const [gen, setGen] = React.useState({ cands: null, i: 0 });
    const [open, setOpen] = React.useState(true);

    const setFormat = (v) => {
      const patch = { format: v };
      if (v === "series" && !Array.isArray(variant.threadParts)) patch.threadParts = variant.body ? [variant.body] : [""];
      if (v === "article" && !variant.article) patch.article = { title: "", body: variant.body || "" };
      onChange(patch);
    };
    const insertHook = () => {
      const tpl = R.HOOK_TEMPLATES.find((h) => h.id === hookType || h.type === hookType) || R.HOOK_TEMPLATES[0];
      if (fmt === "series") { const parts = (variant.threadParts || [""]).slice(); parts[0] = tpl.template + (parts[0] ? "\n" + parts[0] : ""); onChange({ threadParts: parts }); }
      else if (fmt === "article") { const a = variant.article || { title: "", body: "" }; onChange({ article: { ...a, body: tpl.template + (a.body ? "\n\n" + a.body : "") } }); }
      else onChange({ body: tpl.template + (variant.body ? "\n\n" + variant.body : "") });
    };
    const applyCand = (cands, i) => {
      const c = cands[i]; if (!c) return;
      if (fmt === "series") onChange({ threadParts: c.threadParts });
      else if (fmt === "article") onChange({ article: c.article });
      else onChange({ body: c.body });
    };
    const generate = () => {
      const res = window.CroprStore.generate({ kind: "post", format: fmt, channel: variant.channel, pillar, series, brief, cta, hookType, n: 8 });
      setGen({ cands: res.candidates, i: 0 });
      applyCand(res.candidates, 0);
      onToast && onToast(`Drafted ${res.candidates.length} brief-driven options (simulated) — each a different hook, rule-checked`, "ok");
    };
    const tryAnother = () => { if (!gen.cands) return; const ni = (gen.i + 1) % gen.cands.length; setGen({ ...gen, i: ni }); applyCand(gen.cands, ni); };
    const active = gen.cands ? gen.cands[gen.i] : null;

    const genVersions = () => {
      const res = window.CroprStore.generate({ kind: "linkedin_post_from_article", articleBody: (variant.article || {}).body, pillar });
      onChange({ derivedPost: { candidates: res.candidates, selected: 0 } });
      onToast && onToast("Generated companion-post versions — pick the best fit");
    };

    // YouTube publishing metadata (title, category, visibility, license, flags)
    const yt = { ...D.YT_DEFAULTS, ...(variant.youtube || {}) };
    const setYt = (p) => onChange({ youtube: { ...yt, ...p } });
    const ytCheck = (field, label) => (
      <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer" }}>
        <input type="checkbox" checked={!!yt[field]} onChange={(e) => setYt({ [field]: e.target.checked })} style={{ width: 16, height: 16, accentColor: "var(--accent)", flex: "none" }} />{label}
      </label>
    );
    const isCaption = plat !== "youtube" && fmt !== "series" && fmt !== "article";
    const capPh = fmt === "story" ? "Story caption / text overlay — keep it short." : fmt === "reel" ? "Reel caption — hook first, one CTA." : plat === "x" ? "Standalone posts target 180–220 characters. One CTA, at the end." : plat === "telegram" ? "Channel message — no hashtags, one CTA." : plat === "pinterest" ? "Pin description — what it's about, one CTA." : plat === "tiktok" ? "Caption — hook first, one CTA." : plat === "linkedin" ? "LinkedIn — same rules, longer allowed, still no hashtags." : "Caption — no hashtags, one CTA.";
    // header label + collapsed summary
    const platLabel = (D.PLATFORMS.find((p) => p.id === plat) || {}).label || m.name;
    const fmtLabel = (formats.find((f) => f.id === fmt) || {}).label || fmt;
    const summaryChars = R.countChars(plat === "youtube" ? ((yt.title || "") + " " + (variant.body || "")) : fmt === "series" ? (variant.threadParts || []).join(" ") : fmt === "article" ? ((variant.article || {}).body || "") : (variant.body || ""));
    const mediaN = (variant.media || []).length;
    const summary = (summaryChars || mediaN) ? (summaryChars + " chars" + (mediaN ? " · " + mediaN + " media" : "")) : "empty";

    return (
      <div style={{ border: "1px solid var(--border)", borderRadius: 14, overflow: "hidden", marginBottom: 12 }}>
        <div onClick={() => setOpen((o) => !o)} style={{ display: "flex", alignItems: "center", gap: 9, padding: "10px 12px", background: "var(--bg-subtle)", borderBottom: open ? "1px solid var(--border)" : 0, cursor: "pointer", flexWrap: "wrap" }}>
          <PlatformLogo platform={plat} size={22} style={{ borderRadius: 6 }} />
          <span style={{ fontWeight: 700, fontSize: 13 }}>{platLabel}</span>
          <span style={{ fontSize: 12, color: "var(--fg3)" }}>{m.handle}</span>
          <div style={{ flex: 1 }} />
          <Badge color="var(--fg3)">{fmtLabel}</Badge>
          {!open && <span style={{ fontSize: 11.5, color: "var(--fg4)", fontVariantNumeric: "tabular-nums" }}>{summary}</span>}
          <Icon name={open ? "chevron-up" : "chevron-down"} size={18} color="var(--fg3)" />
        </div>
        {open && (
        <div style={{ padding: 12 }}>
          {formats.length > 1 && <div style={{ marginBottom: 10 }}><Segmented value={fmt} onChange={setFormat} options={formats.map((f) => ({ value: f.id, label: f.label, icon: f.icon }))} /></div>}
          <div style={{ display: "flex", gap: 8, marginBottom: 10 }}>
            <Button size="sm" variant="brand" icon="sparkles" onClick={generate}>Generate</Button>
            {gen.cands && gen.cands.length > 1 && <Button size="sm" variant="ghost" icon="shuffle" onClick={tryAnother}>Try another ({gen.i + 1}/{gen.cands.length})</Button>}
            <Button size="sm" variant="ghost" icon="wand-2" onClick={insertHook}>Insert hook</Button>
          </div>

          {plat === "youtube" && (
            <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
              <div>
                <div style={{ display: "flex", alignItems: "center", marginBottom: 5 }}>
                  <span style={{ fontSize: 12, fontWeight: 600, color: "var(--fg2)" }}>Title</span><div style={{ flex: 1 }} /><EmojiPicker onPick={(e) => setYt({ title: (yt.title || "") + e })} />
                </div>
                <TextInput value={yt.title} onChange={(v) => setYt({ title: v })} placeholder="Video title" />
              </div>
              <div>
                <div style={{ display: "flex", alignItems: "center", marginBottom: 5 }}>
                  <span style={{ fontSize: 12, fontWeight: 600, color: "var(--fg2)" }}>Description</span><div style={{ flex: 1 }} /><EmojiPicker onPick={(e) => onChange({ body: (variant.body || "") + e })} />
                </div>
                <TextArea value={variant.body} onChange={(v) => onChange({ body: v })} rows={5} placeholder="Video description — no hashtags, one CTA at the end." />
              </div>
              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }} className="profile-grid">
                <Field label="Category"><Select value={yt.category} onChange={(v) => setYt({ category: v })} options={D.YT_CATEGORIES.map((c) => ({ value: c, label: c }))} /></Field>
                <Field label="License"><Select value={yt.license} onChange={(v) => setYt({ license: v })} options={D.YT_LICENSES.map((l) => ({ value: l.id, label: l.label }))} /></Field>
              </div>
              <div>
                <span style={{ fontSize: 12, fontWeight: 600, color: "var(--fg2)", display: "block", marginBottom: 6 }}>Visibility</span>
                <Segmented value={yt.visibility} onChange={(v) => setYt({ visibility: v })} options={D.YT_VISIBILITY.map((x) => ({ value: x.id, label: x.label }))} />
              </div>
              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "10px 16px", padding: "10px 0 2px" }}>
                {ytCheck("notifySubscribers", "Notify subscribers")}
                {ytCheck("allowEmbedding", "Allow embedding")}
                {ytCheck("madeForKids", "Made for kids")}
                {ytCheck("aiContent", "AI-generated content")}
              </div>
            </div>
          )}

          {isCaption && (
            <div>
              <div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 5 }}><EmojiPicker onPick={(e) => onChange({ body: (variant.body || "") + e })} /></div>
              <TextArea value={variant.body} onChange={(v) => onChange({ body: v })} rows={5} placeholder={capPh} />
              <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 6 }}>
                {plat === "x" ? <CharRing n={R.countChars(variant.body)} /> : <span style={{ fontSize: 11.5, color: "var(--fg3)", fontVariantNumeric: "tabular-nums" }}>{R.countChars(variant.body)} chars</span>}
              </div>
            </div>
          )}

          {fmt === "series" && (
            <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
              {(variant.threadParts || []).map((p, i) => (
                <div key={i} style={{ display: "flex", gap: 8, alignItems: "flex-start" }}>
                  <span style={{ marginTop: 8, fontSize: 12, fontWeight: 700, color: "var(--accent)", width: 24, textAlign: "right", flex: "none" }}>{i + 1}/</span>
                  <div style={{ flex: 1 }}>
                    <TextArea value={p} rows={3} onChange={(v) => { const parts = variant.threadParts.slice(); parts[i] = v; onChange({ threadParts: parts }); }} placeholder={`Part ${i + 1}`} />
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 4 }}>
                      <span style={{ fontSize: 11, color: R.countChars(p) > 280 ? "#F04438" : "var(--fg4)", fontVariantNumeric: "tabular-nums" }}>{R.countChars(p)}/280</span>
                      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                        <EmojiPicker onPick={(e) => { const parts = variant.threadParts.slice(); parts[i] = (parts[i] || "") + e; onChange({ threadParts: parts }); }} />
                        {variant.threadParts.length > 1 && <button onClick={() => { const parts = variant.threadParts.filter((_, j) => j !== i); onChange({ threadParts: parts }); }} style={{ border: 0, background: "none", cursor: "pointer", color: "var(--fg3)", fontSize: 11.5, display: "flex", alignItems: "center", gap: 3 }}><Icon name="trash-2" size={13} />Remove</button>}
                      </div>
                    </div>
                  </div>
                </div>
              ))}
              <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                <Button size="sm" variant="ghost" icon="plus" onClick={() => onChange({ threadParts: [...(variant.threadParts || []), ""] })} disabled={(variant.threadParts || []).length >= 8}>Add part</Button>
                <span style={{ fontSize: 11, color: (variant.threadParts || []).length > 5 ? "#B25E09" : "var(--fg4)" }}>{(variant.threadParts || []).length} parts · cap 4–5</span>
              </div>
            </div>
          )}

          {fmt === "article" && (
            <div>
              <TextInput value={(variant.article || {}).title || ""} onChange={(v) => onChange({ article: { ...(variant.article || {}), title: v } })} placeholder="Article title" style={{ marginBottom: 8, fontWeight: 600 }} />
              <div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 5 }}><EmojiPicker onPick={(e) => onChange({ article: { ...(variant.article || {}), body: ((variant.article || {}).body || "") + e } })} /></div>
              <TextArea value={(variant.article || {}).body || ""} onChange={(v) => onChange({ article: { ...(variant.article || {}), body: v } })} rows={7} placeholder="Long-form body. No char target — still no hashtags, one CTA." />
              <div style={{ fontSize: 11, color: "var(--fg3)", marginTop: 6, display: "flex", gap: 6, alignItems: "center" }}>
                <Icon name="info" size={13} color="var(--fg3)" />
                {plat === "linkedin" ? "Article is composed + stored; the companion post auto-publishes. Article body is manual/export (decision B-i)." : "Article is composed + stored; the thread/post auto-publishes. Article is manual/export (decision B-ii)."}
              </div>
              {plat === "linkedin" && (
                <div style={{ marginTop: 10, border: "1px solid var(--border)", borderRadius: 10, padding: 10 }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
                    <span style={{ fontSize: 12, fontWeight: 700 }}>Companion post</span>
                    <div style={{ flex: 1 }} />
                    <Button size="sm" variant="secondary" icon="sparkles" onClick={genVersions}>Generate versions</Button>
                  </div>
                  {variant.derivedPost && variant.derivedPost.candidates
                    ? variant.derivedPost.candidates.map((c, i) => {
                        const sel = (variant.derivedPost.selected || 0) === i;
                        return (
                          <label key={i} style={{ display: "flex", gap: 8, padding: "8px 10px", borderRadius: 9, cursor: "pointer", marginBottom: 6, border: `1px solid ${sel ? "var(--accent)" : "var(--border)"}`, background: sel ? "var(--accent-weak)" : "transparent" }}>
                            <input type="radio" checked={sel} onChange={() => onChange({ derivedPost: { ...variant.derivedPost, selected: i } })} style={{ marginTop: 3, accentColor: "var(--accent)" }} />
                            <span style={{ fontSize: 12.5, color: "var(--fg1)", whiteSpace: "pre-wrap", lineHeight: 1.4 }}>{c}</span>
                          </label>
                        );
                      })
                    : <div style={{ fontSize: 11.5, color: "var(--fg3)" }}>Generate versions, then pick the best fit — that post auto-publishes.</div>}
                </div>
              )}
            </div>
          )}

          {active && active.hookName && (
            <div style={{ marginTop: 12, border: "1px solid var(--accent)", background: "var(--accent-weak)", borderRadius: 12, padding: "11px 13px" }}>
              <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
                <Icon name="sparkles" size={15} color="var(--accent)" />
                <span style={{ fontSize: 12, fontWeight: 700, color: "var(--accent)" }}>{active.hookName} hook</span>
                <div style={{ flex: 1 }} />
                <span style={{ fontSize: 11, color: "var(--fg3)" }}>Option {gen.i + 1} of {gen.cands.length}</span>
              </div>
              <div style={{ fontSize: 12, color: "var(--fg2)", lineHeight: 1.5 }}><b style={{ color: "var(--fg1)" }}>Approach:</b> {active.approach}</div>
              <div style={{ fontSize: 12, color: "var(--fg2)", lineHeight: 1.5, marginTop: 3 }}><b style={{ color: "var(--fg1)" }}>Why it works:</b> {active.why}</div>
            </div>
          )}

          <div style={{ marginTop: 14, paddingTop: 12, borderTop: "1px solid var(--border)" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
              <Icon name="message-circle" size={14} color="var(--fg3)" />
              <span style={{ fontSize: 12, fontWeight: 600, color: "var(--fg2)" }}>First comment</span>
              <span style={{ fontSize: 11, color: "var(--fg4)" }}>· auto-posted right after</span>
              <div style={{ flex: 1 }} />
              <Select value={variant.firstCommentType || "none"} onChange={(v) => onChange({ firstCommentType: v, firstComment: v === "none" ? "" : variant.firstComment })}
                options={D.FIRST_COMMENT_TYPES.map((f) => ({ value: f.id, label: f.label }))} style={{ width: 210 }} />
            </div>
            {(variant.firstCommentType && variant.firstCommentType !== "none") && (
              <div style={{ marginTop: 8 }}>
                <div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 4 }}>
                  <EmojiPicker onPick={(e) => onChange({ firstComment: (variant.firstComment || "") + e })} />
                </div>
                <TextArea value={variant.firstComment || ""} onChange={(v) => onChange({ firstComment: v })} rows={2} placeholder="The AI writes this to match the strategy on Generate — edit as needed." />
              </div>
            )}
          </div>

          {variant.visualNote && (
            <div style={{ marginTop: 12, background: "var(--bg-subtle)", border: "1px solid var(--border)", borderRadius: 10, padding: "8px 10px", display: "flex", gap: 8 }}>
              <Icon name="image" size={14} color="var(--fg3)" />
              <div style={{ fontSize: 12, color: "var(--fg2)", lineHeight: 1.45 }}><b style={{ color: "var(--fg1)" }}>Visual for {platLabel}: </b>{variant.visualNote}</div>
            </div>
          )}

          <MediaChips media={variant.media} onChange={(mm) => onChange({ media: mm })} />

          {/* per-channel options: include CTA / tags, and an optional own schedule
             (hidden when the host — e.g. a campaign — supplies these itself) */}
          {!hideChannelOptions && (
          <div style={{ marginTop: 14, paddingTop: 12, borderTop: "1px solid var(--border)", display: "flex", flexDirection: "column", gap: 10 }}>
            <div style={{ display: "flex", gap: 18, flexWrap: "wrap" }}>
              <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer" }}>
                <input type="checkbox" checked={variant.addCta !== false} onChange={(e) => onChange({ addCta: e.target.checked })} style={{ width: 16, height: 16, accentColor: "var(--accent)", flex: "none" }} />Add call to action
              </label>
              <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer" }}>
                <input type="checkbox" checked={variant.addTags !== false} onChange={(e) => onChange({ addTags: e.target.checked })} style={{ width: 16, height: 16, accentColor: "var(--accent)", flex: "none" }} />Add tags
              </label>
            </div>
            <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer" }}>
              <input type="checkbox" checked={!!variant.scheduleAt} onChange={(e) => onChange({ scheduleAt: e.target.checked ? (variant.scheduleAt || postScheduleAt || new Date().toISOString()) : null })} style={{ width: 16, height: 16, accentColor: "var(--accent)", flex: "none" }} />
              <Icon name="calendar-clock" size={14} color="var(--fg3)" />Schedule this channel separately
            </label>
            {variant.scheduleAt
              ? <input type="datetime-local" value={toLocalDT(variant.scheduleAt)} onChange={(e) => onChange({ scheduleAt: fromLocalDT(e.target.value) })}
                  style={{ width: "100%", maxWidth: 260, boxSizing: "border-box", fontFamily: "inherit", fontSize: 13, color: "var(--fg1)", background: "var(--surface)", border: "1px solid var(--border-strong)", borderRadius: 10, padding: "7px 10px" }} />
              : <span style={{ fontSize: 11.5, color: "var(--fg4)", marginLeft: 24 }}>Posts at the shared post schedule{postScheduleAt ? " — " + new Date(postScheduleAt).toLocaleString("en-GB", { weekday: "short", day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }) : ""}.</span>}
          </div>
          )}
        </div>
        )}
      </div>
    );
  }

  const toLocalDT = (iso) => { if (!iso) return ""; const d = new Date(iso); const p = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`; };
  const fromLocalDT = (v) => v ? new Date(v).toISOString() : null;

  function Composer({ post, onToast, onNav, locked, onReuse }) {
    const patch = (p) => window.CroprStore.update(post.id, p);
    const setChannels = (chs) => {
      if (!chs.length) return;
      const content = chs.map((ch) => post.content.find((v) => v.channel === ch) || window.CroprStore.newVariant(ch));
      patch({ channels: chs, content });
    };
    const toggleChannel = (ch) => {
      const has = post.channels.includes(ch);
      setChannels(has ? post.channels.filter((c) => c !== ch) : [...post.channels, ch]);
      if (!has) setPreview(ch);
    };
    const updateVariant = (ch, vp) => patch({ content: post.content.map((v) => v.channel === ch ? { ...v, ...vp } : v) });

    const [preview, setPreview] = React.useState(post.channels[0] || "x_cropr");
    React.useEffect(() => { if (!post.channels.includes(preview)) setPreview(post.channels[0] || "x_cropr"); }, [post.channels.join()]);
    // tags input keeps its own raw text so typing commas/spaces isn't stripped mid-edit
    const [tagsText, setTagsText] = React.useState(null);
    React.useEffect(() => { setTagsText(null); }, [post.id]);
    const tagsValue = tagsText !== null ? tagsText : (post.tags || []).join(", ");
    const setTags = (v) => { setTagsText(v); patch({ tags: v.split(",").map((s) => s.trim().replace(/^@/, "")).filter(Boolean) }); };

    // ---- AI post composer (brand-aware) --------------------------------------
    // Turn the brief into a post idea + visual idea + channel-tailored drafts.
    // Uses the server LLM when live+configured, else the local CroprGen fallback.
    const [gen, setGen] = React.useState({ busy: false, variant: 0, tried: false });
    React.useEffect(() => { setGen({ busy: false, variant: 0, tried: false }); }, [post.id]);
    const A = window.LeapAPI;
    const liveAI = () => Boolean(window.LEAP && window.LEAP.live && A && A.assistant);
    const resolveOrgId = () => { try { const c = S.getActiveClient && S.getActiveClient(); return (c && c._orgId) ? c._orgId : (window.LeapGate && window.LeapGate.orgId ? window.LeapGate.orgId() : null); } catch (e) { return null; } };
    const buildComposeInput = (variant) => {
      const brand = (S.getBrand && S.getBrand()) || {};
      return {
        brief: post.brief || "",
        brandBrief: brand.brandBrief || "",
        brandVoice: brand.brandVoice || "",
        category: D.label(D.PILLARS, post.pillar) || "",
        series: D.label(D.SERIES, post.series) || "",
        intent: post.intent ? (S.intentLabel ? S.intentLabel(post.intent) : post.intent) : "",
        tone: post.tone ? (S.toneLabel ? S.toneLabel(post.tone) : post.tone) : "",
        hookType: post.hookType ? (D.label(D.HOOK_TYPES, post.hookType) || post.hookType) : "",
        variant: variant,
        channels: (post.channels || []).map((ch) => {
          const plat = (D.CHANNELS[ch] || {}).platform || "x";
          const v = (post.content || []).find((x) => x.channel === ch) || {};
          return { id: ch, platform: plat, name: (D.CHANNELS[ch] || {}).name || ch, formats: (D.FORMATS[plat] || []).map((f) => ({ id: f.id, label: f.label })), firstCommentType: v.firstCommentType || "none" };
        }),
      };
    };
    const runGenerate = async (variant) => {
      if (!(post.brief || "").trim()) { onToast("Add a brief first — tell me your post idea", "error"); return; }
      if (!(post.channels || []).length) { onToast("Pick at least one channel first", "error"); return; }
      setGen((g) => ({ ...g, busy: true }));
      let composed = null, viaAI = false;
      try {
        const input = buildComposeInput(variant);
        if (liveAI()) {
          const orgId = resolveOrgId();
          if (orgId) { try { const r = await A.assistant.composePost(orgId, input); if (r && r.enabled) { composed = r; viaAI = true; } } catch (e) { /* fall through */ } }
        }
        if (!composed) composed = window.CroprGen.composePost(input);
        window.CroprStore.applyComposedPost(post.id, composed);
        onToast(viaAI ? "Drafted with AI from your brief + brand voice" : "Drafted from your brief — connect the AI in the cockpit for richer results", "ok");
      } catch (e) {
        onToast("Couldn't generate just now — try again", "error");
      } finally {
        setGen({ busy: false, variant: variant, tried: true });
      }
    };

    const rc = R.validatePost(post);
    const proofFlag = post.status === "in_draft" ? R.transitionProofCheck(post) : null;
    const pMeta = D.CHANNELS[preview] || D.CHANNELS.x_cropr;

    // Apply the CTA + @tags to each channel per that channel's tickboxes — add when
    // ticked (as the final lines, so the single_cta rule sees one CTA at the end),
    // remove when unticked. Handles single/caption, series and article formats.
    const tagsLine = () => (post.tags || []).map((t) => "@" + t.replace(/^@/, "")).join(" ");
    const applyChannels = () => {
      const cta = (post.cta || "").trim();
      const tline = tagsLine();
      if (!cta && !tline) { onToast("Add a call to action or tags first", "error"); return; }
      const isExtra = (l) => { const t = (l || "").trim(); return !t || (cta && t === cta) || (tline && t === tline) || /→|https?:\/\//i.test(t); };
      const build = (text, v) => {
        const lines = (text || "").replace(/\s+$/, "").split("\n");
        while (lines.length && isExtra(lines[lines.length - 1])) lines.pop();
        if (v.addCta !== false && cta) lines.push(cta);
        if (v.addTags !== false && tline) lines.push(tline);
        return lines.join("\n");
      };
      const content = post.content.map((v) => {
        if (v.format === "series") { const parts = (v.threadParts && v.threadParts.length ? v.threadParts : [""]).slice(); parts[parts.length - 1] = build(parts[parts.length - 1], v); return { ...v, threadParts: parts }; }
        if (v.format === "article") { const a = v.article || { title: "", body: "" }; return { ...v, article: { ...a, body: build(a.body, v) } }; }
        return { ...v, body: build(v.body, v) };
      });
      patch({ content });
      onToast("Applied CTA & tags to the ticked channels");
    };

    return (
      <div style={{ display: "grid", gridTemplateColumns: "minmax(0,1.05fr) minmax(0,1fr)", gap: 22, alignItems: "start" }} className="composer-grid">
        {/* ---------------- FORM ---------------- */}
        <div>
          {locked && (
            <div style={{ display: "flex", alignItems: "center", gap: 10, background: "var(--accent-weak)", border: "1px solid var(--accent)", borderRadius: 12, padding: "11px 14px", marginBottom: 12 }}>
              <Icon name="lock" size={16} color="var(--accent)" />
              <span style={{ fontSize: 12.5, color: "var(--fg2)", flex: 1 }}>This post has been published — the fields are read-only. Reuse it to edit a fresh copy.</span>
              {onReuse && <Button size="sm" variant="primary" icon="recycle" onClick={() => onReuse(post.id)}>Reuse to edit</Button>}
            </div>
          )}
          <div style={{ position: "relative" }}>
          <Card pad={22}>
            <div ref={(el) => { if (el) el.inert = !!locked; }} style={{ opacity: locked ? 0.72 : 1 }}>
            <Field label="Internal title" hint="Not published — for the team and ClickUp.">
              <TextInput value={post.title} onChange={(v) => patch({ title: v })} placeholder="e.g. Weekly Pulse — new feature" />
            </Field>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
              <Field label="Category" hint="Add your own — used to group posts.">
                <TextInput value={D.label(D.PILLARS, post.pillar)} onChange={(v) => patch({ pillar: v })} placeholder="e.g. Product, Behind the scenes…" />
              </Field>
              <Field label="Series" hint="Optional — a recurring series name.">
                <TextInput value={D.label(D.SERIES, post.series)} onChange={(v) => patch({ series: v })} placeholder="e.g. Brew guide, Origins…" />
              </Field>
            </div>

            {/* Intent + tone — steer what the post is FOR and how it SOUNDS. Both feed
                the AI. Manage the option lists in Settings → Post setup. */}
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
              <Field label="Intent" hint="What this post is trying to achieve.">
                <Select value={post.intent || ""} onChange={(v) => patch({ intent: v || null })}
                  options={[{ value: "", label: "— pick an intent" }, ...(S.getIntents ? S.getIntents() : []).map((x) => ({ value: x.id, label: x.label }))]} />
              </Field>
              <Field label="Tone" hint="How it should sound.">
                <Select value={post.tone || ""} onChange={(v) => patch({ tone: v || null })}
                  options={[{ value: "", label: "— pick a tone" }, ...(S.getTones ? S.getTones() : []).map((x) => ({ value: x.id, label: x.label }))]} />
              </Field>
            </div>

            <Field label="Post to" hint="X sub-targets post from that account. Telegram posts to the channel via the Bot API.">
              <ChannelToggle selected={post.channels} onToggle={toggleChannel} />
            </Field>

            <Field label="Brief" hint="Your idea. The more specific you are, the sharper the draft — and name the format if you have one in mind.">
              <TextArea value={post.brief} onChange={(v) => patch({ brief: v })} rows={3}
                placeholder="What's your next post idea? The more specific you are, the more specific I can make it with you. Tell me if it should be a simple post, series, article, reel or story — I'll read your brand brief + voice from Settings and draft each channel for you." />
            </Field>

            {/* Generate: reads the brief + brand brief/voice, picks a format per
                channel, and fills every selected channel. Try another re-rolls it. */}
            <div style={{ display: "flex", alignItems: "center", gap: 10, margin: "-4px 0 14px", flexWrap: "wrap" }}>
              <Button variant="brand" icon="sparkles" disabled={gen.busy} onClick={() => runGenerate(0)}>
                {gen.busy ? "Generating…" : (gen.tried ? "Regenerate from brief" : "Generate with AI")}
              </Button>
              {gen.tried && !gen.busy && (
                <Button variant="secondary" icon="shuffle" onClick={() => runGenerate(gen.variant + 1)}>Try another idea</Button>
              )}
              <span style={{ fontSize: 11, color: "var(--fg4)" }}>
                {liveAI() ? "Tailored to each channel from your brand voice." : "Offline draft — connect the AI in the cockpit for richer results."}
              </span>
            </div>

            <Field label="Post idea" hint="The angle the AI is running with — edit to steer it, then regenerate.">
              <TextArea value={post.postIdea || ""} onChange={(v) => patch({ postIdea: v })} rows={3} placeholder="Generated from your brief — the core angle of this post." />
            </Field>
            {post.whyItWorks && (
              <div style={{ margin: "-6px 0 14px", background: "var(--accent-weak)", borderRadius: 12, padding: "10px 12px", display: "flex", gap: 8 }}>
                <Icon name="sparkles" size={15} color="var(--accent)" />
                <div style={{ fontSize: 12.5, color: "var(--fg2)", lineHeight: 1.5 }}><b style={{ color: "var(--fg1)" }}>Why it works: </b>{post.whyItWorks}</div>
              </div>
            )}
            <Field label="Visual idea" hint="What to shoot or design to pair with it — the scene for reels & stories.">
              <TextArea value={post.visualIdea || ""} onChange={(v) => patch({ visualIdea: v })} rows={4} placeholder="Generated visual concept — subject, composition, lighting, mood. For a reel or story, the scene beat by beat." />
            </Field>

            <Field label="Hook type" hint="How each channel's copy opens — change it and regenerate to apply it to every channel.">
              <Segmented value={post.hookType || "specific_claim"} onChange={(v) => patch({ hookType: v })}
                options={D.HOOK_TYPES.map((h) => ({ value: h.id, label: h.label }))} />
            </Field>
            {(post.hookApproach || post.hookWhy) && (
              <div style={{ margin: "-6px 0 14px", background: "var(--bg-subtle)", border: "1px solid var(--border)", borderRadius: 12, padding: "10px 12px" }}>
                {post.hookApproach && <div style={{ fontSize: 12.5, color: "var(--fg2)", lineHeight: 1.5 }}><b style={{ color: "var(--fg1)" }}>Approach: </b>{post.hookApproach}</div>}
                {post.hookWhy && <div style={{ fontSize: 12.5, color: "var(--fg2)", lineHeight: 1.5, marginTop: 3 }}><b style={{ color: "var(--fg1)" }}>Why it works: </b>{post.hookWhy}</div>}
                <div style={{ fontSize: 11, color: "var(--fg4)", marginTop: 5 }}>Applied to every channel's post text.</div>
              </div>
            )}

            <SectionTitle icon="pen-line">Content</SectionTitle>
            {post.content.map((v) => <VariantEditor key={v.channel} variant={v} hookType={post.hookType} pillar={post.pillar} series={post.series} brief={post.brief} cta={post.cta} postScheduleAt={post.scheduleAt} onToast={onToast} onChange={(vp) => updateVariant(v.channel, vp)} />)}

            <Field label="Call to action" hint="Added to each channel that has “Add call to action” ticked (as the final line).">
              <TextInput value={post.cta} onChange={(v) => patch({ cta: v })} placeholder="e.g. Plan your week → leap.app" />
            </Field>
            <Field label="Tags to @" hint="Comma-separated accounts / partners to mention — the @ is added automatically.">
              <TextInput value={tagsValue} onChange={setTags} placeholder="riversidecoffee, barista, partner" />
            </Field>
            {(post.tags || []).length > 0 && (
              <div style={{ display: "flex", flexWrap: "wrap", gap: 6, margin: "-6px 0 10px" }}>
                {post.tags.map((t, i) => <span key={i} style={{ fontSize: 11.5, fontWeight: 600, color: "var(--accent)", background: "var(--accent-weak)", borderRadius: 999, padding: "3px 9px" }}>@{t.replace(/^@/, "")}</span>)}
              </div>
            )}
            <div style={{ marginBottom: 14 }}>
              <Button size="sm" variant="secondary" icon="corner-down-left" onClick={applyChannels} disabled={!(post.cta || "").trim() && !(post.tags || []).length}>Apply CTA &amp; tags to channels</Button>
              <span style={{ fontSize: 11, color: "var(--fg4)", marginLeft: 10 }}>Uses each channel's “Add CTA / Add tags” tickboxes.</span>
            </div>

            <SectionTitle icon="calendar-clock">Schedule & details</SectionTitle>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
              <Field label="Schedule · Europe/Vienna" hint="Auto-publishes at this time once the post is approved.">
                <input type="datetime-local" value={toLocalDT(post.scheduleAt)} onChange={(e) => patch({ scheduleAt: fromLocalDT(e.target.value) })}
                  style={{ width: "100%", boxSizing: "border-box", fontFamily: "inherit", fontSize: 13.5, color: "var(--fg1)", background: "var(--surface)", border: "1px solid var(--border-strong)", borderRadius: 10, padding: "8px 11px" }} />
              </Field>
              <Field label="Campaign"><TextInput value={post.campaign} onChange={(v) => patch({ campaign: v })} placeholder="Optional" /></Field>
            </div>
            <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", fontSize: 13, color: "var(--fg2)", padding: "4px 0" }}>
              <input type="checkbox" checked={post.proofMarked} onChange={(e) => patch({ proofMarked: e.target.checked })} style={{ width: 16, height: 16, accentColor: "var(--accent)" }} />
              Mark as proof (satisfies the “proof before draft” gate without media or a metric)
            </label>
            </div>
          </Card>
          {locked && <div title="Read-only — click ‘Reuse to edit’ to edit a copy" onClick={() => onToast && onToast("This post is read-only — click ‘Reuse to edit’ to edit a copy", "error")} style={{ position: "absolute", inset: 0, borderRadius: 16, cursor: "not-allowed", zIndex: 6 }} />}
          </div>
        </div>

        {/* ---------------- PREVIEW + RULES + WORKFLOW ---------------- */}
        <div style={{ position: "sticky", top: 0, display: "flex", flexDirection: "column", gap: 16 }}>
          <div>
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }}>
              <Segmented value={preview} onChange={setPreview}
                options={post.channels.map((ch) => ({ value: ch, label: D.CHANNELS[ch].name, iconNode: <PlatformLogo platform={D.CHANNELS[ch].platform} size={15} /> }))} />
              <span style={{ fontSize: 11, color: "var(--fg3)" }}>Live preview</span>
            </div>
            {pMeta.platform === "x" ? <PreviewX post={post} channel={preview} />
              : pMeta.platform === "telegram" ? <PreviewTelegram post={post} channel={preview} />
              : <PreviewLinkedIn post={post} channel={preview} />}
          </div>

          <Card pad={16}><RulePanel ruleCheck={rc} proofFlag={proofFlag} /></Card>

          <Card pad={16}><WorkflowRail post={post} ruleCheck={rc} onToast={onToast} onNav={onNav} /></Card>

          {(post.status === "published" || post.status === "amplify" || post.status === "tracked") && (
            <Card pad={16}><AmplifyPanel post={post} onToast={onToast} /></Card>
          )}
        </div>
      </div>
    );
  }
  window.Composer = Composer;
  window.VariantEditor = VariantEditor; // reused by the Campaigns sequence planner
})();
