/* ====================================================================
   Prompt Generator — app
   Real OpenRouter generation. The model catalog is fetched live; the
   API key is set in the UI and persisted in localStorage; usage is
   read back from OpenRouter and shown in the header.
   ==================================================================== */
const { useState, useEffect, useRef, useCallback } = React;
const PG = window.PG;

/* ---- tiny inline icons (stroke) ----------------------------------- */
const Icon = ({ d, size = 16, fill = "none" }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill={fill} stroke="currentColor"
       strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    {Array.isArray(d) ? d.map((p, i) => <path key={i} d={p} />) : <path d={d} />}
  </svg>
);
const ICONS = {
  plus: "M12 5v14M5 12h14",
  bolt: "M13 3 4 14h7l-1 7 9-11h-7z",
  reroll: ["M3 12a9 9 0 0 1 15-6.7L21 8", "M21 3v5h-5", "M21 12a9 9 0 0 1-15 6.7L3 16", "M3 21v-5h5"],
  copy: ["M9 9h10v10a2 2 0 0 1-2 2H9z", "M5 15V5a2 2 0 0 1 2-2h10"],
  trash: ["M4 7h16", "M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2", "M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13"],
  check: "M5 12.5 10 17l9-10",
  save: ["M5 4h11l3 3v13H5z", "M8 4v5h7", "M8 20v-6h8v6"],
  menu: ["M4 7h16", "M4 12h16", "M4 17h16"],
  x: ["M6 6l12 12", "M18 6 6 18"],
  caret: "M6 9l6 6 6-6",
  key: ["M15.5 8.5a3.5 3.5 0 1 1-3.4 4.3L7 18l-2 .5L5 16l5.2-5.1A3.5 3.5 0 0 1 15.5 8.5z", "M16.5 9.5h.01"],
  search: ["M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M20 20l-3.2-3.2"],
  refresh: ["M3 12a9 9 0 0 1 15-6.7L21 8", "M21 3v5h-5"]
};

/* ---- localStorage helpers ----------------------------------------- */
const LS = {
  get(k, fallback) { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : fallback; } catch { return fallback; } },
  set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch {} },
  del(k) { try { localStorage.removeItem(k); } catch {} }
};
const uid = () => Math.random().toString(36).slice(2, 9);

// escape + bold the _tokens_ for previews
function highlightTemplate(t) {
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  return esc(t || "").replace(/_([a-zA-Z][a-zA-Z0-9]*)_/g, (m) => "<b>" + esc(m) + "</b>");
}
function fmtUsd(n) {
  if (n == null || isNaN(n)) return null;
  return "$" + Number(n).toFixed(n < 1 ? 3 : 2);
}

/* ==================================================================== */
/*  Model selector (live catalog + search)                              */
/* ==================================================================== */
function ModelSelect({ models, model, setModel, loading, error }) {
  const [open, setOpen] = useState(false);
  const [q, setQ] = useState("");
  const ref = useRef(null);
  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, []);

  const cur = models.find((m) => m.id === model);
  const curLabel = cur ? cur.label : (model || "Select a model");

  const ql = q.trim().toLowerCase();
  const filtered = ql
    ? models.filter((m) => (m.label + " " + m.id + " " + m.provider).toLowerCase().includes(ql))
    : models;
  const groups = filtered.reduce((acc, m) => { (acc[m.provider] = acc[m.provider] || []).push(m); return acc; }, {});
  const provNames = Object.keys(groups).sort((a, b) => a.localeCompare(b));

  return (
    <div className="model-select" ref={ref}>
      <button className="model-trigger" onClick={() => setOpen((o) => !o)} disabled={loading && !models.length}>
        <span className={"dot" + (models.length ? "" : " off")} />
        <span className="label-pre">Model</span>
        <span className="mname">{loading && !models.length ? "loading…" : curLabel}</span>
        <span className="caret"><Icon d={ICONS.caret} size={14} /></span>
      </button>
      {open && (
        <div className="model-menu">
          <div className="model-search">
            <Icon d={ICONS.search} size={14} />
            <input autoFocus value={q} onChange={(e) => setQ(e.target.value)}
                   placeholder={`Search ${models.length} models…`} spellCheck={false} />
          </div>
          {error && <div className="menu-msg err">{error}</div>}
          {!error && filtered.length === 0 && <div className="menu-msg">No models match “{q}”.</div>}
          {provNames.map((prov) => (
            <div key={prov}>
              <div className="model-group-label">{prov}</div>
              {groups[prov].map((m) => (
                <button key={m.id} className={"model-opt" + (m.id === model ? " sel" : "")}
                        onClick={() => { setModel(m.id); setOpen(false); setQ(""); }}>
                  <span className="ml">{m.label}</span>
                  <span className="mid">{m.id}</span>
                  {m.id === model && <span className="check"><Icon d={ICONS.check} size={15} /></span>}
                </button>
              ))}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ==================================================================== */
/*  API key + usage                                                     */
/* ==================================================================== */
function KeyMenu({ apiKey, onSave, onClear, usage, usageErr, usageLoading, onRefresh, open, setOpen }) {
  const ref = useRef(null);
  const [draft, setDraft] = useState(apiKey || "");
  useEffect(() => { setDraft(apiKey || ""); }, [apiKey, open]);
  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [setOpen]);

  const used = usage ? fmtUsd(usage.usage) : null;
  const limit = usage && usage.limit != null ? fmtUsd(usage.limit) : null;
  const trigText = !apiKey ? "Set key" : (used ? (limit ? `${used} / ${limit}` : `${used} used`) : "•••");

  return (
    <div className="key-select" ref={ref}>
      <button className={"model-trigger key-trigger" + (apiKey ? "" : " unset")} onClick={() => setOpen((o) => !o)}>
        <span className=" kicon"><Icon d={ICONS.key} size={14} /></span>
        <span className="mname">{trigText}</span>
        <span className="caret"><Icon d={ICONS.caret} size={14} /></span>
      </button>
      {open && (
        <div className="model-menu key-menu">
          <div className="key-title">OpenRouter API key</div>
          <input className="key-input" type="password" value={draft} spellCheck={false}
                 placeholder="sk-or-v1-…" autoComplete="off"
                 onChange={(e) => setDraft(e.target.value)}
                 onKeyDown={(e) => { if (e.key === "Enter") onSave(draft.trim()); }} />
          <div className="key-actions">
            <button className="key-save" onClick={() => onSave(draft.trim())} disabled={!draft.trim() || draft.trim() === apiKey}>
              <Icon d={ICONS.check} size={14} />Save
            </button>
            {apiKey && <button className="key-clear" onClick={onClear}><Icon d={ICONS.trash} size={14} />Remove</button>}
          </div>

          <div className="key-usage">
            <span className="ku-label">Usage</span>
            {!apiKey ? (
              <span className="ku-muted">add a key to see usage</span>
            ) : usageLoading ? (
              <span className="ku-muted">checking…</span>
            ) : usageErr ? (
              <span className="ku-err">{usageErr}</span>
            ) : usage ? (
              <span className="ku-val">
                <b>{used}</b>{limit ? <span className="ku-muted"> of {limit}</span> : <span className="ku-muted"> spent</span>}
                {usage.is_free_tier ? <span className="ku-tag">free tier</span> : null}
              </span>
            ) : (
              <span className="ku-muted">—</span>
            )}
            {apiKey && (
              <button className="ku-refresh" title="Refresh usage" onClick={onRefresh} disabled={usageLoading}>
                <Icon d={ICONS.refresh} size={13} />
              </button>
            )}
          </div>

          <a className="key-hint" href="https://openrouter.ai/keys" target="_blank" rel="noreferrer">Get a key ↗</a>
          <div className="key-note">Stored only in this browser (localStorage) and sent to OpenRouter to run your prompts.</div>
        </div>
      )}
    </div>
  );
}

/* ==================================================================== */
/*  Library sidebar                                                     */
/* ==================================================================== */
function Sidebar({ prompts, activeId, onLoad, onNew, onDelete }) {
  return (
    <aside className="sidebar">
      <div className="side-head">
        <h2>Prompt Library</h2>
        <button className="new-btn" onClick={onNew}><Icon d={ICONS.plus} size={14} />New</button>
      </div>
      <div className="lib-list">
        {prompts.length === 0 && (
          <div className="lib-empty">No saved prompts yet. Write one and hit <b>Save</b>.</div>
        )}
        {prompts.map((p) => (
          <div key={p.id} className={"lib-item" + (p.id === activeId ? " active" : "")} onClick={() => onLoad(p)}>
            <div className="li-name">{p.name || "Untitled prompt"}</div>
            <div className="li-tmpl" dangerouslySetInnerHTML={{ __html: highlightTemplate(p.template) }} />
            <button className="lib-del" title="Delete"
                    onClick={(e) => { e.stopPropagation(); onDelete(p.id); }}>
              <Icon d={ICONS.trash} size={14} />
            </button>
          </div>
        ))}
      </div>
    </aside>
  );
}

/* ==================================================================== */
/*  Composer                                                            */
/* ==================================================================== */
function Composer({ name, setName, template, setTemplate, batch, setBatch, onRun, onSave, saved, running }) {
  const taRef = useRef(null);
  const [showAll, setShowAll] = useState(false);

  const tokens = PG.detectTokens(template);
  const uniq = [];
  const seen = new Set();
  tokens.forEach((t) => { if (!seen.has(t.category)) { seen.add(t.category); uniq.push(t); } });

  const allCats = PG.CATEGORIES;
  const shownCats = showAll ? allCats : allCats.slice(0, 9);

  const insert = (cat) => {
    const token = "_" + cat + "_";
    const ta = taRef.current;
    if (!ta) { setTemplate((template ? template + " " : "") + token); return; }
    const s = ta.selectionStart ?? template.length;
    const e = ta.selectionEnd ?? template.length;
    const before = template.slice(0, s);
    const after = template.slice(e);
    const needSpace = before && !/\s$/.test(before);
    const ins = (needSpace ? " " : "") + token;
    const next = before + ins + after;
    setTemplate(next);
    requestAnimationFrame(() => { ta.focus(); const pos = (before + ins).length; ta.setSelectionRange(pos, pos); });
  };

  const onKey = (e) => {
    if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); onRun(); }
  };

  return (
    <section className="composer">
      <div className="composer-head">
        <input className="name-input" value={name} placeholder="Untitled prompt"
               onChange={(e) => setName(e.target.value)} />
        <button className={"save-btn" + (saved ? " saved" : "")} onClick={onSave}>
          <Icon d={saved ? ICONS.check : ICONS.save} size={15} />{saved ? "Saved" : "Save"}
        </button>
      </div>

      <div className="editor-wrap">
        <textarea ref={taRef} className="editor" value={template} onChange={(e) => setTemplate(e.target.value)}
                  onKeyDown={onKey} spellCheck={false}
                  placeholder="Write a short joke about a _animal_." />
      </div>

      <div className="token-row">
        <span className="row-label">In use</span>
        {uniq.length === 0 && <span style={{ color: "var(--muted)", fontSize: 12.5 }}>none yet — insert one below</span>}
        {uniq.map((t) => (
          <span key={t.category} className={"chip " + (t.known ? "detected" : "unknown")}>
            {t.raw}{!t.known && " ⚠"}
          </span>
        ))}
      </div>

      <div className="token-row" style={{ paddingTop: 0 }}>
        <span className="row-label">Insert</span>
        {shownCats.map((c) => (
          <button key={c} className="chip" onClick={() => insert(c)}>_{c}_</button>
        ))}
        {allCats.length > 9 && (
          <button className="palette-toggle" onClick={() => setShowAll((s) => !s)}>
            {showAll ? "show less" : "+" + (allCats.length - 9) + " more"}
          </button>
        )}
      </div>

      <div className="composer-foot">
        <div className="batch" title="Run the prompt this many times">
          <span className="blab">Runs</span>
          <button className="bbtn" disabled={batch <= 1} onClick={() => setBatch((b) => Math.max(1, b - 1))}>–</button>
          <span className="bval">{batch}</span>
          <button className="bbtn" disabled={batch >= 8} onClick={() => setBatch((b) => Math.min(8, b + 1))}>+</button>
        </div>
        <div className="foot-spacer" />
        <button className="run-btn" onClick={onRun} disabled={running || !template.trim()}>
          <Icon d={ICONS.bolt} size={16} fill="currentColor" />
          {batch > 1 ? `Generate ${batch}` : "Generate"}
          <kbd>⌘↵</kbd>
        </button>
      </div>
    </section>
  );
}

/* ==================================================================== */
/*  Result card                                                         */
/* ==================================================================== */
function ResultCard({ item, modelLabel, onReroll }) {
  const [copied, setCopied] = useState(false);
  const copy = () => {
    navigator.clipboard?.writeText(item.response || "").then(() => {
      setCopied(true); setTimeout(() => setCopied(false), 1400);
    });
  };
  const thinking = item.status === "thinking";
  const errored = item.status === "error";
  return (
    <article className="card">
      <div className="card-prompt">
        {item.segments && item.segments.length
          ? item.segments.map((s, i) =>
              s.type === "fill"
                ? <span key={i} className="fill" title={"_" + s.category + "_"}>{s.value}</span>
                : <span key={i}>{s.text}</span>)
          : <span dangerouslySetInnerHTML={{ __html: highlightTemplate(item.template) }} />}
      </div>
      <div className="card-body">
        {thinking
          ? <div className="card-resp thinking"><span className="dots"><i /><i /><i /></span></div>
          : <div className={"card-resp" + (errored ? " errored" : "")}>{item.response}</div>}
      </div>
      <div className="card-foot">
        <span className="meta"><b>{modelLabel}</b>{item.ms ? ` · ${(item.ms / 1000).toFixed(1)}s` : ""}{item.cost != null ? ` · ${fmtUsd(item.cost)}` : ""}</span>
        <span className="fspacer" />
        <button className="mini-btn" onClick={onReroll} disabled={thinking} title="New random words & regenerate">
          <Icon d={ICONS.reroll} size={14} />Re-roll
        </button>
        <button className={"mini-btn" + (copied ? " copied" : "")} onClick={copy} disabled={thinking || errored}>
          <Icon d={copied ? ICONS.check : ICONS.copy} size={14} />{copied ? "Copied" : "Copy"}
        </button>
      </div>
    </article>
  );
}

/* ==================================================================== */
/*  App                                                                 */
/* ==================================================================== */
function App() {
  const [prompts, setPrompts] = useState(() => LS.get("pg.prompts", PG.SEED_PROMPTS));
  const [model, setModel] = useState(() => LS.get("pg.model", null));
  const [activeId, setActiveId] = useState(() => LS.get("pg.activeId", null));
  const last = LS.get("pg.composer", { name: "", template: "" });
  const [name, setName] = useState(last.name);
  const [template, setTemplate] = useState(last.template);
  const [batch, setBatch] = useState(1);
  const [runs, setRuns] = useState(() => LS.get("pg.runs", []));
  const [saved, setSaved] = useState(false);
  const [running, setRunning] = useState(false);
  const [drawer, setDrawer] = useState(false);

  // models
  const [models, setModels] = useState([]);
  const [modelsLoading, setModelsLoading] = useState(true);
  const [modelsErr, setModelsErr] = useState(null);

  // api key + usage
  const [apiKey, setApiKey] = useState(() => LS.get("pg.orKey", ""));
  const [keyOpen, setKeyOpen] = useState(false);
  const [usage, setUsage] = useState(null);
  const [usageErr, setUsageErr] = useState(null);
  const [usageLoading, setUsageLoading] = useState(false);

  useEffect(() => LS.set("pg.prompts", prompts), [prompts]);
  useEffect(() => { if (model) LS.set("pg.model", model); }, [model]);
  useEffect(() => LS.set("pg.activeId", activeId), [activeId]);
  useEffect(() => LS.set("pg.composer", { name, template }), [name, template]);
  useEffect(() => LS.set("pg.runs", runs.slice(0, 20)), [runs]);

  // load the live model catalog
  useEffect(() => {
    let alive = true;
    PG.fetchModels().then((ms) => {
      if (!alive) return;
      setModels(ms); setModelsErr(null);
      setModel((cur) => {
        if (cur && ms.some((m) => m.id === cur)) return cur;
        const pref = ms.find((m) => m.id === "anthropic/claude-sonnet-4") || ms.find((m) => /claude/.test(m.id)) || ms[0];
        return pref ? pref.id : cur;
      });
    }).catch((e) => { if (alive) setModelsErr(e.message || "couldn't load models"); })
      .finally(() => { if (alive) setModelsLoading(false); });
    return () => { alive = false; };
  }, []);

  const refreshUsage = useCallback(() => {
    if (!apiKey) { setUsage(null); setUsageErr(null); return; }
    setUsageLoading(true); setUsageErr(null);
    PG.fetchUsage(apiKey)
      .then((info) => { setUsage(info); setUsageErr(null); })
      .catch((e) => { setUsage(null); setUsageErr(e.message || "lookup failed"); })
      .finally(() => setUsageLoading(false));
  }, [apiKey]);

  useEffect(() => { refreshUsage(); }, [refreshUsage]);

  const saveKey = (raw) => {
    const k = PG.cleanKey(raw);
    setApiKey(k);
    if (k) LS.set("pg.orKey", k); else LS.del("pg.orKey");
    setKeyOpen(false);
  };
  const clearKey = () => { setApiKey(""); LS.del("pg.orKey"); setUsage(null); setUsageErr(null); };

  const modelLabel = (models.find((m) => m.id === model) || { label: model || "model" }).label;

  // resolve + generate one item via OpenRouter
  const fillItem = useCallback((runId, itemId, template) => {
    const t0 = (performance && performance.now) ? performance.now() : Date.now();
    setRuns((rs) => rs.map((r) => r.id === runId
      ? { ...r, items: r.items.map((it) => it.id === itemId ? { ...it, status: "thinking", segments: [], response: "", ms: 0, cost: null } : it) }
      : r));
    PG.generate(template, model, apiKey).then((data) => {
      const now = (performance && performance.now) ? performance.now() : Date.now();
      const ms = Math.max(1, Math.round(now - t0));
      const cost = data.usage && data.usage.cost != null ? data.usage.cost : null;
      setRuns((rs) => rs.map((r) => r.id === runId
        ? { ...r, items: r.items.map((it) => it.id === itemId
            ? { ...it, segments: data.segments, resolved: data.resolved, fills: data.fills, response: data.response, status: "done", ms, cost } : it) }
        : r));
      refreshUsage();
    }).catch((e) => {
      setRuns((rs) => rs.map((r) => r.id === runId
        ? { ...r, items: r.items.map((it) => it.id === itemId
            ? { ...it, status: "error", response: "⚠ " + (e.message || "Generation failed."), ms: 0 } : it) }
        : r));
    });
  }, [model, apiKey, refreshUsage]);

  const run = useCallback(() => {
    if (!template.trim()) return;
    if (!apiKey) { setKeyOpen(true); return; }
    setRunning(true);
    const runId = uid();
    const items = Array.from({ length: batch }, () => ({ id: uid(), status: "thinking", segments: [], response: "", template }));
    const newRun = { id: runId, model, modelLabel, template, ts: Date.now(), items };
    setRuns((rs) => [newRun, ...rs]);
    items.forEach((it, idx) => setTimeout(() => fillItem(runId, it.id, template), idx * 90));
    setTimeout(() => setRunning(false), 350);
  }, [template, batch, model, modelLabel, fillItem, apiKey]);

  const rerollItem = (runId, itemId) => {
    if (!apiKey) { setKeyOpen(true); return; }
    const r = runs.find((x) => x.id === runId);
    if (r) fillItem(runId, itemId, r.template);
  };

  const savePrompt = () => {
    if (!template.trim()) return;
    const nm = name.trim() || deriveName(template);
    if (!name.trim()) setName(nm);
    if (activeId && prompts.some((p) => p.id === activeId)) {
      setPrompts((ps) => ps.map((p) => p.id === activeId ? { ...p, name: nm, template } : p));
    } else {
      const id = uid();
      setPrompts((ps) => [{ id, name: nm, template }, ...ps]);
      setActiveId(id);
    }
    setSaved(true); setTimeout(() => setSaved(false), 1500);
  };

  const loadPrompt = (p) => { setName(p.name); setTemplate(p.template); setActiveId(p.id); setDrawer(false); setSaved(false); };
  const newPrompt = () => { setName(""); setTemplate(""); setActiveId(null); setDrawer(false); };
  const deletePrompt = (id) => {
    setPrompts((ps) => ps.filter((p) => p.id !== id));
    if (id === activeId) setActiveId(null);
  };

  const totalResults = runs.reduce((n, r) => n + r.items.length, 0);

  return (
    <div className={"app" + (drawer ? " drawer-open" : "")}>
      <header className="topbar">
        <button className="icon-btn" onClick={() => setDrawer(true)} aria-label="Open library"><Icon d={ICONS.menu} /></button>
        <div className="brand">
          <span className="mark">/</span>
          <span className="title">Prompt&nbsp;Generator <small>· openrouter</small></span>
        </div>
        <span className="spacer" />
        <KeyMenu apiKey={apiKey} onSave={saveKey} onClear={clearKey}
                 usage={usage} usageErr={usageErr} usageLoading={usageLoading} onRefresh={refreshUsage}
                 open={keyOpen} setOpen={setKeyOpen} />
        <ModelSelect models={models} model={model} setModel={setModel} loading={modelsLoading} error={modelsErr} />
      </header>

      <Sidebar prompts={prompts} activeId={activeId} onLoad={loadPrompt} onNew={newPrompt} onDelete={deletePrompt} />
      <div className="scrim" onClick={() => setDrawer(false)} />

      <main className="main">
        <div className="workspace">
          {!apiKey && (
            <div className="keybar">
              <span>Add your OpenRouter API key to start generating.</span>
              <button className="keybar-btn" onClick={() => setKeyOpen(true)}><Icon d={ICONS.key} size={14} />Set key</button>
            </div>
          )}

          <Composer
            name={name} setName={setName}
            template={template} setTemplate={setTemplate}
            batch={batch} setBatch={setBatch}
            onRun={run} onSave={savePrompt} saved={saved} running={running}
          />

          <section className="results">
            {totalResults > 0 && (
              <div className="results-head">
                <h3>Results</h3>
                <span className="count">{totalResults}</span>
                <button className="clear-link" onClick={() => setRuns([])}>Clear all</button>
              </div>
            )}

            {runs.length === 0 ? (
              <div className="empty">
                <div className="big">Nothing generated yet</div>
                <div className="sub">Write a prompt with placeholders like <code>_animal_</code> or <code>_country_</code>,<br/>then hit Generate. Each run swaps in fresh random words.</div>
              </div>
            ) : runs.map((r) => (
              <div className="batch-group" key={r.id}>
                {r.items.length > 1 && (
                  <div className="batch-tag">
                    <Icon d={ICONS.bolt} size={12} fill="currentColor" />
                    batch of {r.items.length} · <span className="bt-model">{r.modelLabel}</span>
                  </div>
                )}
                {r.items.map((it) => (
                  <ResultCard key={it.id} item={it} modelLabel={r.modelLabel}
                              onReroll={() => rerollItem(r.id, it.id)} />
                ))}
              </div>
            ))}
          </section>
        </div>
      </main>
    </div>
  );
}

function deriveName(t) {
  const clean = t.replace(/_([a-zA-Z0-9]+)_/g, "$1").replace(/\s+/g, " ").trim();
  const words = clean.split(" ").slice(0, 5).join(" ");
  return words.charAt(0).toUpperCase() + words.slice(1, 40);
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
