// projects-shared.jsx — pieces used by BOTH the public project-detail view
// (rendered inside the Hisui//OS centre tile via components.jsx)
// AND the /Admin/Projects React editor (admin-projects.jsx).
//
// Ported ~verbatim from Design_Chapters/ProjectsEditor/reference/components.jsx.
// All exports are attached to `window` so the other Babel script files can pick them up.

const { useState: pUseState, useRef: pUseRef, useEffect: pUseEffect, Fragment: PFragment } = React;

// ─── Tiny utilities ──────────────────────────────────────────────────
function pClasses(...xs) { return xs.filter(Boolean).join(' '); }

// ─── JSON syntax highlighter — keys distinct from string values ──────
function HiJson({ text }) {
  if (!text) return null;
  const tokens = [];
  const re = /("(?:\\.|[^"\\])*")(\s*:)?|(-?\b\d+(?:\.\d+)?\b)|\b(true|false|null)\b|([{}\[\],])/g;
  let last = 0; let m;
  while ((m = re.exec(text)) !== null) {
    if (m.index > last) tokens.push({ k: 'ws', v: text.slice(last, m.index) });
    if (m[1]) {
      if (m[2]) { tokens.push({ k: 'j-key', v: m[1] }); tokens.push({ k: 'j-pun', v: m[2] }); }
      else tokens.push({ k: 'j-str', v: m[1] });
    } else if (m[3]) tokens.push({ k: 'j-num', v: m[3] });
    else if (m[4]) tokens.push({ k: m[4] === 'null' ? 'j-null' : 'j-bool', v: m[4] });
    else if (m[5]) tokens.push({ k: 'j-pun', v: m[5] });
    last = re.lastIndex;
  }
  if (last < text.length) tokens.push({ k: 'ws', v: text.slice(last) });
  return tokens.map((t, i) => t.k === 'ws' ? t.v : <span key={i} className={t.k}>{t.v}</span>);
}

// ─── Multi-language code highlighter — greedy, priority-ordered ──────
const LANG_RULES = {
  bash: [
    { k: 'com', re: /#[^\n]*/g },
    { k: 'str', re: /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g },
    { k: 'kw',  re: /\b(curl|dotnet|git|cd|echo|export|sudo|npm|yarn|pnpm|node|brew|kubectl|docker|bash|sh|wget|jq)\b/g },
    { k: 'pun', re: /\\\n|\\$|&&|\|\|/g },
    { k: 'num', re: /\b\d+(?:\.\d+)?\b/g },
  ],
  shell: [],
  csharp: [
    { k: 'com', re: /\/\/[^\n]*/g },
    { k: 'str', re: /\$?@?"(?:\\.|[^"\\])*"/g },
    { k: 'kw',  re: /\b(using|var|await|async|new|class|public|private|protected|internal|static|readonly|void|int|long|short|byte|string|bool|float|double|decimal|true|false|null|return|if|else|foreach|for|while|in|throw|namespace|record|sealed|override|virtual|abstract|interface|enum|struct|this|base|out|ref)\b/g },
    { k: 'fn',  re: /\b[A-Z][A-Za-z0-9_]*\b/g },
    { k: 'num', re: /\b\d+(?:\.\d+)?\b/g },
  ],
  javascript: [
    { k: 'com', re: /\/\/[^\n]*/g },
    { k: 'str', re: /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g },
    { k: 'kw',  re: /\b(const|let|var|async|await|function|return|if|else|new|class|export|import|from|true|false|null|undefined|of|in|throw|try|catch|finally|for|while|do|switch|case|default|break|continue|typeof|instanceof)\b/g },
    { k: 'fn',  re: /\b[A-Z][A-Za-z0-9_]*\b/g },
    { k: 'num', re: /\b\d+(?:\.\d+)?\b/g },
  ],
  python: [
    { k: 'com', re: /#[^\n]*/g },
    { k: 'str', re: /"""[\s\S]*?"""|'''[\s\S]*?'''|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g },
    { k: 'kw',  re: /\b(import|from|def|class|if|elif|else|for|in|return|True|False|None|async|await|with|as|lambda|try|except|finally|raise|pass|yield|not|and|or|is)\b/g },
    { k: 'fn',  re: /\b[A-Z][A-Za-z0-9_]*\b/g },
    { k: 'num', re: /\b\d+(?:\.\d+)?\b/g },
  ],
  http: [
    { k: 'kw',  re: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/g },
    { k: 'url', re: /https?:\/\/\S+/g },
    { k: 'str', re: /"(?:\\.|[^"\\])*"/g },
  ],
};
LANG_RULES.shell = LANG_RULES.bash;
LANG_RULES.typescript = LANG_RULES.javascript;
LANG_RULES.ts = LANG_RULES.javascript;
LANG_RULES.js = LANG_RULES.javascript;
LANG_RULES.cs = LANG_RULES.csharp;
LANG_RULES.py = LANG_RULES.python;

function tokenizeCode(text, rules) {
  const out = [];
  let i = 0;
  while (i < text.length) {
    let best = null;
    for (const r of rules) {
      r.re.lastIndex = i;
      const m = r.re.exec(text);
      if (m && m.index === i) { best = { v: m[0], k: r.k }; break; }
    }
    if (best) { out.push(best); i += best.v.length; }
    else {
      let next = text.length;
      for (const r of rules) {
        r.re.lastIndex = i;
        const m = r.re.exec(text);
        if (m && m.index < next) next = m.index;
      }
      if (next <= i) next = i + 1;
      out.push({ k: 'ws', v: text.slice(i, next) });
      i = next;
    }
  }
  return out;
}

function HiCode({ lang, text }) {
  if ((lang || '').toLowerCase() === 'json') return <HiJson text={text || ''} />;
  const base = LANG_RULES[(lang || 'bash').toLowerCase()] || LANG_RULES.bash;
  const com = base.filter((r) => r.k === 'com');
  const rest = base.filter((r) => r.k !== 'com');
  const merged = [...com, { k: 'url', re: /https?:\/\/\S+/g }, ...rest];
  const tokens = tokenizeCode(text || '', merged);
  return tokens.map((t, i) => t.k === 'ws' ? t.v : <span key={i} className={t.k}>{t.v}</span>);
}

// ─── Hero ─────────────────────────────────────────────────────────────
function ProjectHero({ p }) {
  return (
    <div className="pd-hero">
      <div className="pd-hero-row">
        <h1>// {p.title}{p.version && <span className="ver">{p.version}</span>}</h1>
        <div className="pd-tags">
          {(p.tags || []).map((t, i) => (
            <span key={i} className={pClasses('tag', t.kind)}>{t.label}</span>
          ))}
        </div>
      </div>
      <p className="pd-desc">{p.description}</p>
      <div className="pd-hero-meta">
        {p.meta?.repo && (<><span>repo · <b>{p.meta.repo}</b></span><span className="sep">·</span></>)}
        {p.meta?.base && (<><span>base · <b>{p.meta.base}</b></span><span className="sep">·</span></>)}
        <span>updated <b>{p.meta?.updated || ''}</b></span>
      </div>
    </div>
  );
}

// ─── Section header ───────────────────────────────────────────────────
function ProjectSectionHead({ label, count, children }) {
  return (
    <div className="pd-section-head">
      {label}
      {count != null && <span className="count">· {count}</span>}
      {children}
      <span className="line"></span>
    </div>
  );
}

// ─── Snippets — tabbed block ──────────────────────────────────────────
function ProjectSnippets({ snippets }) {
  const list = snippets || [];
  const [active, setActive] = pUseState(list[0]?.id);
  const cur = list.find((s) => s.id === active) || list[0];
  const [copied, setCopied] = pUseState(false);
  if (!list.length) return null;
  function copy() {
    try { navigator.clipboard.writeText(cur.body || ''); } catch (e) {}
    setCopied(true);
    setTimeout(() => setCopied(false), 1100);
  }
  return (
    <div className="snippets snippets-tabs">
      <div className="snip-tabbed-view">
        <div className="snip-tabbed">
          <div className="snip-tabs">
            {list.map((s) => (
              <div key={s.id}
                className={pClasses('tab', s.id === active && 'active')}
                onClick={() => setActive(s.id)}>
                {s.label}
              </div>
            ))}
            <div className="right-actions">
              <button className={pClasses('copy', copied && 'done')} type="button" onClick={copy}>
                {copied ? 'copied' : 'copy'}
              </button>
            </div>
          </div>
          <div className="snip-panes">
            <div className="pane active"><HiCode lang={cur?.lang} text={cur?.body || ''} /></div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Method badge ─────────────────────────────────────────────────────
function MethodBadge({ method }) {
  const m = (method || 'GET').toUpperCase();
  const cls = ({ GET: 'get', POST: 'post', PUT: 'put', DELETE: 'del', DEL: 'del', PATCH: 'patch' })[m] || 'get';
  const label = m === 'DELETE' ? 'DEL' : m;
  return <span className={`method ${cls}`}>{label}</span>;
}

// ─── Status pill ──────────────────────────────────────────────────────
function StatusPill({ status }) {
  const n = parseInt(status, 10);
  const cls = n >= 500 ? 's5' : n >= 400 ? 's4' : '';
  return <span className={`resp-status ${cls}`}>{status}</span>;
}

// ─── Try-it form for one endpoint ─────────────────────────────────────
function TryIt({ ep, slug, baseUrl }) {
  const initial = {};
  (ep.params || []).forEach((p) => { initial[p.name] = p.example ?? p.default ?? ''; });
  const [vals, setVals] = pUseState(initial);
  const [exec, setExec] = pUseState(null);
  const [busy, setBusy] = pUseState(false);
  const [copiedCurl, setCopiedCurl] = pUseState(false);

  function buildPath() {
    let path = ep.path || '';
    (ep.params || []).filter((p) => p.loc === 'path').forEach((p) => {
      const v = vals[p.name];
      if (v) path = path.replace('{' + p.name + '}', encodeURIComponent(v));
    });
    const qs = (ep.params || [])
      .filter((p) => p.loc === 'query' && vals[p.name] !== '' && vals[p.name] != null)
      .map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(vals[p.name])}`)
      .join('&');
    return path + (qs ? '?' + qs : '');
  }
  function fullUrl() {
    const base = (baseUrl || '').replace(/\/$/, '');
    const path = buildPath();
    return base + (path.startsWith('/') ? path : '/' + path);
  }

  async function execute() {
    setBusy(true);
    setExec(null);
    const sentAt = new Date();
    try {
      const xsrf = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
      const epId = parseInt(String(ep.id).replace(/^ep_/, ''), 10);
      const body = {
        pathParams: Object.fromEntries((ep.params || []).filter((p) => p.loc === 'path').map((p) => [p.name, String(vals[p.name] ?? '')])),
        queryParams: Object.fromEntries((ep.params || []).filter((p) => p.loc === 'query').map((p) => [p.name, String(vals[p.name] ?? '')])),
      };
      const r = await fetch(`/api/projects/${encodeURIComponent(slug)}/test/${epId}`, {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json', ...(xsrf ? { 'X-CSRF-TOKEN': xsrf } : {}) },
        body: JSON.stringify(body),
      });
      const result = await r.json();
      const status = result.status || r.status;
      const statusText = result.statusText || r.statusText;
      const prettyBody = prettyJson(result.body || '');
      setExec({
        status: `${status} ${statusText}`,
        timeMs: result.timeMs ?? 0,
        sizeB: result.sizeB ?? 0,
        body: prettyBody,
        sentAt,
      });
    } catch (e) {
      setExec({
        status: 'network error',
        timeMs: 0, sizeB: 0,
        body: '{"error":"' + (e?.message || 'unknown').replace(/"/g, "'") + '"}',
        sentAt,
        netErr: true,
      });
    }
    setBusy(false);
  }

  function copyCurl() {
    try { navigator.clipboard.writeText(`curl "${fullUrl()}"`); } catch (e) {}
    setCopiedCurl(true);
    setTimeout(() => setCopiedCurl(false), 1100);
  }

  const qReq = (ep.params || []).filter((p) => p.required).length;
  const qOpt = (ep.params || []).filter((p) => !p.required).length;

  const Params = (
    <div className="params">
      <div className="params-group-title">query · {qReq} required · {qOpt} optional</div>
      {(ep.params || []).map((p) => (
        <div key={p.name} className="param-row">
          <div className="pk">
            <span className="name">{p.name}{p.required && <span className="req">*</span>}</span>
            <span className="ptype">{p.loc} · {p.type}</span>
          </div>
          <div className="pv">
            {p.type === 'enum' ? (
              <select value={vals[p.name] ?? ''} onChange={(e) => setVals({ ...vals, [p.name]: e.target.value })}>
                {(p.enum || []).map((v) => <option key={v}>{v}</option>)}
              </select>
            ) : (
              <input className="text" type="text"
                value={vals[p.name] ?? ''}
                placeholder={p.example}
                onChange={(e) => setVals({ ...vals, [p.name]: e.target.value })} />
            )}
            <span className="ex">
              {p.required ? 'required' : 'optional'}
              {p.default && <> · default <code>{p.default}</code></>}
              {p.example && !p.default && <> · e.g. <code>{p.example}</code></>}
              {p.description && <> · {p.description}</>}
            </span>
          </div>
        </div>
      ))}
    </div>
  );

  const Actions = (
    <div className="try-actions">
      <div className="left">
        <button className="btn-exec" type="button" disabled={busy} onClick={execute}>
          {busy ? 'executing…' : 'execute ↗'}
        </button>
        <button className={pClasses('btn-secondary', copiedCurl && 'done')} type="button" onClick={copyCurl}>
          {copiedCurl ? 'copied' : 'copy as curl'}
        </button>
      </div>
      <div className="right"><span>// rate-limit · 60 / 1m · ip</span></div>
    </div>
  );

  const Response = exec ? (
    <div className="resp">
      <div className="resp-bar">
        <StatusPill status={exec.status} />
        <span className="meta">time · <b>{exec.timeMs}ms</b></span>
        <span className="meta">size · <b>{exec.sizeB} B</b></span>
        <span className="meta">at · <b>{exec.sentAt.toTimeString().slice(0, 8)}</b></span>
      </div>
      <div className="resp-block"><HiJson text={exec.body} /></div>
    </div>
  ) : (
    <div className="resp-empty">
      // press <span style={{ color: 'var(--teal)' }}>execute ↗</span> to send the request · response renders here
    </div>
  );

  return (
    <div className="try-it">
      <div className="left-col">
        {Params}
        {Actions}
      </div>
      <div className="right-col">
        {Response}
      </div>
    </div>
  );
}

function prettyJson(raw) {
  if (!raw) return '';
  try { return JSON.stringify(JSON.parse(raw), null, 2); }
  catch (e) { return raw; }
}

// ─── Accordion API tester ────────────────────────────────────────────
function ApiTester({ endpoints, slug, baseUrl }) {
  const list = endpoints || [];
  const [openId, setOpenId] = pUseState(list[0]?.id);
  if (!list.length) {
    return (
      <div className="tester" style={{ padding: 18, color: 'var(--fg-3)', fontFamily: 'var(--font-mono)', fontSize: 11 }}>
        // no anon-allow endpoints exposed yet
      </div>
    );
  }
  return (
    <div className="tester">
      {list.map((ep) => {
        const open = ep.id === openId;
        const pathParts = (ep.path || '').split(/(\{[^}]+\})/g);
        return (
          <div key={ep.id} className={pClasses('ep', open && 'open')}>
            <div className="ep-head" onClick={() => setOpenId(open ? null : ep.id)}>
              <span className="twist">▸</span>
              <MethodBadge method={ep.method} />
              <span className="ep-path">
                {pathParts.map((seg, i) => seg.startsWith('{')
                  ? <span key={i} className="param-token">{seg}</span>
                  : <PFragment key={i}>{seg}</PFragment>)}
              </span>
              <span className="ep-summary">{ep.summary}</span>
            </div>
            {open && (
              <div className="ep-body">
                {ep.description && (
                  <p style={{ margin: '0 0 12px', fontSize: 12, color: 'var(--fg-1)', lineHeight: 1.5 }}>
                    {ep.description}
                  </p>
                )}
                <TryIt ep={ep} slug={slug} baseUrl={baseUrl} />
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

Object.assign(window, {
  pClasses, HiJson, HiCode, LANG_RULES, tokenizeCode,
  ProjectHero, ProjectSectionHead, ProjectSnippets,
  MethodBadge, StatusPill, TryIt, ApiTester, prettyJson,
});
