// Configuration — the full schema-driven agent config editor. Mirrors the Hermes
// Agent dashboard's Settings: it reads the live config (/dash/api/config) + the
// settable schema (/dash/api/config/schema, ~403 fields across 33 categories) and
// renders a typed control per field, grouped by category. Edits build a working
// copy of the *whole* config; Save PUTs it back (unedited keys preserved). This is
// distinct from the app's Settings page (which owns theme/appearance/UI prefs).
const { useState, useEffect, useCallback, useMemo, useRef } = React;

// Pretty category labels (keys = schema categories / category_order).
const CFG_CAT_LABELS = {
  general: "General", agent: "Agent", terminal: "Terminal", web: "Web", browser: "Browser",
  tool_output: "Tool Output", tool_loop_guardrails: "Loop Guardrails", compression: "Compression",
  openrouter: "OpenRouter", bedrock: "Bedrock", auxiliary: "Auxiliary Models", display: "Display",
  security: "Security", tts: "Text-to-Speech", stt: "Speech-to-Text", voice: "Voice", memory: "Memory",
  delegation: "Delegation", curator: "Curator", slack: "Slack", discord: "Discord", mattermost: "Mattermost",
  matrix: "Matrix", kanban: "Kanban", tools: "Tools", logging: "Logging", model_catalog: "Model Catalog",
  gateway: "Gateway", streaming: "Streaming", sessions: "Sessions", lsp: "LSP", x_search: "X Search", secrets: "Secrets",
};
const CFG_CAT_ICONS = {
  general: "sliders", agent: "bolt", terminal: "terminal", web: "external", browser: "external",
  tool_output: "files", tool_loop_guardrails: "refresh", compression: "gauge", openrouter: "providers",
  bedrock: "providers", auxiliary: "cpu", display: "overview", security: "lock", tts: "mic", stt: "mic",
  voice: "mic", memory: "memory", delegation: "swarm", curator: "memory", slack: "chat", discord: "discord",
  mattermost: "chat", matrix: "chat", kanban: "kanban", tools: "skills", logging: "file", model_catalog: "cpu",
  gateway: "providers", streaming: "activity", sessions: "clock", lsp: "git", x_search: "search", secrets: "lock",
};

// ---- dotted-path get/set on a plain object ----
function getByPath(obj, path) {
  let cur = obj;
  for (const k of path.split(".")) {
    if (cur == null || typeof cur !== "object") return undefined;
    cur = cur[k];
  }
  return cur;
}
function setByPath(obj, path, value) {
  const keys = path.split(".");
  let cur = obj;
  for (let i = 0; i < keys.length - 1; i++) {
    const k = keys[i];
    if (cur[k] == null || typeof cur[k] !== "object") cur[k] = {};
    cur = cur[k];
  }
  cur[keys[keys.length - 1]] = value;
  return obj;
}

function prettyField(path) {
  const seg = path.split(".").pop();
  return seg.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
// Auto descriptions look like "Agent → Max Turns"; real ones are sentences.
function helpfulDesc(desc, label) {
  if (!desc) return "";
  if (desc.includes("→")) return "";
  if (desc.trim().toLowerCase() === label.toLowerCase()) return "";
  return desc;
}
const looksSecret = (path) => /(api_key|api-key|token|secret|password|password_hash)$/i.test(path.split(".").pop());

function eq(a, b) {
  if (a === b) return true;
  if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((x, i) => x === b[i]);
  return JSON.stringify(a) === JSON.stringify(b);
}

// One field control, rendered by schema type.
function CfgField({ path, meta, value, defVal, onChange, dirty }) {
  const label = prettyField(path);
  const hint = helpfulDesc(meta.description, label);
  const type = meta.type;
  const reveal = useRef(false);
  const [, force] = useState(0);

  let control;
  if (type === "boolean") {
    control = <Toggle on={!!value} onChange={(v) => onChange(v)} />;
  } else if (type === "select") {
    control = (
      <select className="select" value={value == null ? "" : String(value)} onChange={(e) => onChange(e.target.value)} style={{ maxWidth: 280 }}>
        {(meta.options || []).map((o) => <option key={String(o)} value={String(o)}>{o === "" ? "— (unset)" : String(o)}</option>)}
      </select>
    );
  } else if (type === "number") {
    control = (
      <input className="input mono" type="number" value={value == null ? "" : value} style={{ maxWidth: 200 }}
        onChange={(e) => onChange(e.target.value === "" ? null : Number(e.target.value))} />
    );
  } else if (type === "list") {
    const arr = Array.isArray(value) ? value : (value ? [value] : []);
    control = (
      <textarea className="input mono" rows={Math.min(6, Math.max(2, arr.length + 1))} value={arr.join("\n")} style={{ width: "100%", resize: "vertical", lineHeight: 1.5 }}
        placeholder="one entry per line"
        onChange={(e) => onChange(e.target.value.split("\n").map((s) => s.trim()).filter(Boolean))} />
    );
  } else {
    const secret = looksSecret(path);
    control = (
      <div className="flex" style={{ gap: 6, width: "100%" }}>
        <input className="input mono" type={secret && !reveal.current ? "password" : "text"} value={value == null ? "" : String(value)}
          placeholder={defVal != null && defVal !== "" ? `default: ${defVal}` : "—"}
          onChange={(e) => onChange(e.target.value)} style={{ flex: 1 }} />
        {secret && <button className="icon-btn" type="button" title="Reveal" aria-label={reveal.current ? "Hide secret" : "Reveal secret"} onClick={() => { reveal.current = !reveal.current; force((n) => n + 1); }}><Icon name={reveal.current ? "moon" : "sun"} size={15} /></button>}
      </div>
    );
  }

  const wide = type === "list" || (type === "string" && !looksSecret(path));
  return (
    <div className="cfg-field" style={{ display: "flex", gap: 14, padding: "13px 2px", borderBottom: "1px solid var(--line)", alignItems: wide ? "flex-start" : "center", flexWrap: wide ? "wrap" : "nowrap" }}>
      <div style={{ flex: 1, minWidth: 220 }}>
        <div className="flex" style={{ gap: 8, alignItems: "center" }}>
          <b style={{ fontSize: 13.5, fontWeight: 600 }}>{label}</b>
          {dirty && <span className="badge pri" style={{ fontSize: 10 }}><span className="d" />changed</span>}
        </div>
        <div className="faint mono" style={{ fontSize: 11, marginTop: 2 }}>{path}</div>
        {hint && <div className="faint" style={{ fontSize: 12, marginTop: 3, lineHeight: 1.5 }}>{hint}</div>}
      </div>
      <div style={{ flexShrink: 0, width: wide ? "100%" : "auto", maxWidth: wide ? 520 : "none", minWidth: wide ? 260 : 0 }}>{control}</div>
    </div>
  );
}

function PageConfig({ toast }) {
  const [schema, setSchema] = useState(null);   // {fields, category_order}
  const [orig, setOrig] = useState(null);        // pristine config (from GET)
  const [defs, setDefs] = useState(null);        // defaults
  const [cfg, setCfg] = useState(null);          // working copy
  const [cat, setCat] = useState(null);          // selected category
  const [filter, setFilter] = useState("");
  const [live, setLive] = useState(null);
  const [saving, setSaving] = useState(false);

  const load = useCallback(async () => {
    setLive(null);
    try {
      const [sc, c] = await Promise.all([HERMES.dashConfigSchema(), HERMES.dashConfig()]);
      setSchema(sc);
      setOrig(c);
      setCfg(JSON.parse(JSON.stringify(c)));
      const order = (sc && sc.category_order) || [];
      setCat((prev) => prev || order[0] || "general");
      setLive(true);
      HERMES.dashConfigDefaults().then(setDefs).catch(() => {});
    } catch (e) {
      setLive(false);
    }
  }, []);
  useEffect(() => { load(); }, [load]);

  const fields = (schema && schema.fields) || {};
  const order = useMemo(() => {
    if (schema && schema.category_order && schema.category_order.length) return schema.category_order;
    // derive from fields if category_order missing
    const seen = [];
    Object.values(fields).forEach((m) => { if (m.category && !seen.includes(m.category)) seen.push(m.category); });
    return seen;
  }, [schema]);

  // group field paths by category
  const byCat = useMemo(() => {
    const g = {};
    Object.entries(fields).forEach(([path, m]) => { (g[m.category || "general"] = g[m.category || "general"] || []).push([path, m]); });
    return g;
  }, [fields]);

  // changed-path set
  const changed = useMemo(() => {
    const s = new Set();
    if (!cfg || !orig) return s;
    Object.keys(fields).forEach((p) => { if (!eq(getByPath(cfg, p), getByPath(orig, p))) s.add(p); });
    return s;
  }, [cfg, orig, fields]);

  const setField = useCallback((path, value) => {
    setCfg((prev) => setByPath(JSON.parse(JSON.stringify(prev)), path, value));
  }, []);

  const save = useCallback(async () => {
    if (!cfg) return;
    setSaving(true);
    try {
      await HERMES.dashConfigUpdate(cfg);
      toast(`Saved ${changed.size} change${changed.size === 1 ? "" : "s"}`);
      const fresh = await HERMES.dashConfig();
      setOrig(fresh); setCfg(JSON.parse(JSON.stringify(fresh)));
    } catch (e) { toast(e.message || "Save failed"); }
    setSaving(false);
  }, [cfg, changed, toast]);

  const discard = useCallback(() => { if (orig) setCfg(JSON.parse(JSON.stringify(orig))); toast("Changes discarded"); }, [orig, toast]);

  // search across all fields (by path / label / description)
  const search = filter.trim().toLowerCase();
  const matches = (path, m) => !search || path.toLowerCase().includes(search) || prettyField(path).toLowerCase().includes(search) || (m.description || "").toLowerCase().includes(search);

  const visiblePaths = search
    ? Object.entries(fields).filter(([p, m]) => matches(p, m))
    : (byCat[cat] || []);

  if (live === false) {
    return (
      <div className="page">
        <PageHead eyebrow="System" title="Configuration" sub="Full agent configuration — every settable option." />
        <div className="card"><div className="empty">Configuration is only available when signed in and connected to the Hermes Agent dashboard.</div></div>
      </div>
    );
  }

  return (
    <div className="page">
      <PageHead eyebrow="System" title="Configuration" sub="Every settable Hermes Agent option, grouped by category. Edits apply to new sessions.">
        {live === true && <span className="badge ok"><span className="d" />live</span>}
        {live === null && <span className="badge"><span className="d" />loading…</span>}
        <button className="btn btn-ghost" onClick={load} disabled={saving}><Icon name="refresh" size={16} />Reload</button>
      </PageHead>

      {/* search */}
      <div className="card" style={{ marginBottom: "var(--gap)", padding: 12 }}>
        <div className="search" style={{ width: "100%" }}>
          <Icon name="search" size={16} />
          <input placeholder="Search all options (e.g. timeout, reasoning, discord)…" value={filter} onChange={(e) => setFilter(e.target.value)} />
          {filter && <button className="icon-btn" aria-label="Clear configuration search" onClick={() => setFilter("")}><Icon name="x" size={15} /></button>}
        </div>
      </div>

      <div className="cfg-layout" style={{ display: "grid", gridTemplateColumns: "230px 1fr", gap: "var(--gap)", alignItems: "start" }}>
        {/* category nav */}
        {!search && (
          <div className="card" style={{ padding: 8, position: "sticky", top: 12, maxHeight: "calc(var(--app-h, 100vh) - 120px)", overflow: "auto" }}>
            {order.map((c) => {
              const n = (byCat[c] || []).length;
              if (!n) return null;
              const dirtyN = (byCat[c] || []).filter(([p]) => changed.has(p)).length;
              return (
                <button key={c} className={cls("session-link", c === cat && "active")} style={{ borderRadius: 8 }} onClick={() => setCat(c)}>
                  <Icon name={CFG_CAT_ICONS[c] || "sliders"} size={16} className="faint" />
                  <span className="st">{CFG_CAT_LABELS[c] || prettyField(c)}</span>
                  {dirtyN > 0 ? <span className="badge pri" style={{ fontSize: 10 }}>{dirtyN}</span> : <span className="faint mono" style={{ fontSize: 11 }}>{n}</span>}
                </button>
              );
            })}
          </div>
        )}

        {/* fields */}
        <div className="card" style={{ gridColumn: search ? "1 / -1" : "auto" }}>
          <div className="card-head">
            <div>
              <h2>{search ? `Search results` : (CFG_CAT_LABELS[cat] || prettyField(cat || ""))}</h2>
              <p>{search ? `${visiblePaths.length} option${visiblePaths.length === 1 ? "" : "s"} match “${filter}”.` : (`${visiblePaths.length} option${visiblePaths.length === 1 ? "" : "s"} in this category.`)}</p>
            </div>
          </div>

          {!visiblePaths.length ? (
            <div className="empty">No options.</div>
          ) : (
            <div>
              {visiblePaths.map(([path, m]) => (
                <CfgField key={path} path={path} meta={m}
                  value={getByPath(cfg, path)} defVal={defs ? getByPath(defs, path) : undefined}
                  dirty={changed.has(path)} onChange={(v) => setField(path, v)} />
              ))}
            </div>
          )}
        </div>
      </div>

      {/* sticky save bar */}
      {changed.size > 0 && (
        <div style={{ position: "sticky", bottom: 12, marginTop: "var(--gap)", display: "flex", justifyContent: "center", zIndex: 30 }}>
          <div className="card" style={{ display: "flex", alignItems: "center", gap: 14, padding: "10px 16px", boxShadow: "var(--shadow)", border: "1px solid var(--primary-ring)" }}>
            <span style={{ fontSize: 13.5, fontWeight: 600 }}>{changed.size} unsaved change{changed.size === 1 ? "" : "s"}</span>
            <button className="btn btn-ghost btn-sm" onClick={discard} disabled={saving}>Discard</button>
            <button className="btn btn-primary btn-sm" onClick={save} disabled={saving}><Icon name="check" size={14} />{saving ? "Saving…" : "Save changes"}</button>
          </div>
        </div>
      )}
    </div>
  );
}
window.PageConfig = PageConfig;
