// Files explorer + Terminal
const { useState, useRef, useEffect, useCallback } = React;

const isDirEntry = (e) => e && (e.type === "dir" || e.type === "directory" || e.type === "folder" || e.isDir === true);
const joinPath = (base, name) => (base ? base.replace(/\/+$/, "") + "/" : "") + name;

// Live tree node — lazy-loads its children from HERMES on expand.
function LiveTreeNode({ node, depth, activePath, onPick }) {
  const [open, setOpen] = useState(depth < 1);
  const [children, setChildren] = useState(node.children || null);
  const [loading, setLoading] = useState(false);
  const [err, setErr] = useState(false);
  const pad = 8 + depth * 14;

  const loadChildren = useCallback(() => {
    if (children || loading) return;
    setLoading(true); setErr(false);
    HERMES.filesList(node.path)
      .then((res) => {
        const kids = (res?.entries || []).map((e) => ({
          name: e.name, type: isDirEntry(e) ? "dir" : "file",
          size: e.size, path: e.path || joinPath(node.path, e.name),
        })).sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1));
        setChildren(kids);
      })
      .catch(() => { setChildren([]); setErr(true); })
      .finally(() => setLoading(false));
  }, [children, loading, node.path]);

  // Auto-load children when a directory starts expanded (the root level opens by
  // default via depth < 1) so it shows its contents instead of a stale "empty".
  useEffect(() => { if (open && node.type === "dir") loadChildren(); }, []);

  if (node.type === "dir") {
    const toggle = () => {
      setOpen((o) => { const next = !o; if (next) loadChildren(); return next; });
    };
    return (
      <div>
        <button type="button" className="tree-row" style={{ paddingLeft: pad }} aria-expanded={open} onClick={toggle}>
          <Icon name="chevron" size={13} style={{ transform: open ? "rotate(90deg)" : "none", transition: "transform .15s", color: "var(--faint)" }} />
          <Icon name="folder" size={15} style={{ color: "var(--primary)" }} />{node.name}
        </button>
        {open && (
          loading ? <div className="tree-row faint" style={{ paddingLeft: pad + 16, cursor: "default" }}>loading…</div>
          : err ? <div className="tree-row faint" style={{ paddingLeft: pad + 16, cursor: "default" }}>unreadable</div>
          : (children || []).length === 0 ? <div className="tree-row faint" style={{ paddingLeft: pad + 16, cursor: "default" }}>empty</div>
          : children.map((c, i) => <LiveTreeNode key={c.path || i} node={c} depth={depth + 1} activePath={activePath} onPick={onPick} />)
        )}
      </div>
    );
  }
  return (
    <button type="button" className={cls("tree-row", "file", activePath === node.path && "active")} aria-current={activePath === node.path ? "true" : undefined} style={{ paddingLeft: pad + 16 }} onClick={() => onPick(node)}>
      <Icon name="file" size={14} className="faint" />{node.name}
    </button>
  );
}

// Offline tree node — renders nested children from window.DATA (no lazy load).
function StaticTreeNode({ node, depth, activePath, onPick }) {
  const [open, setOpen] = useState(depth < 1);
  const pad = 8 + depth * 14;
  if (node.type === "dir") {
    return (
      <div>
        <button type="button" className="tree-row" style={{ paddingLeft: pad }} aria-expanded={open} onClick={() => setOpen((o) => !o)}>
          <Icon name="chevron" size={13} style={{ transform: open ? "rotate(90deg)" : "none", transition: "transform .15s", color: "var(--faint)" }} />
          <Icon name="folder" size={15} style={{ color: "var(--primary)" }} />{node.name}
        </button>
        {open && (node.children || []).map((c, i) => <StaticTreeNode key={i} node={{ ...c, path: c.name }} depth={depth + 1} activePath={activePath} onPick={onPick} />)}
      </div>
    );
  }
  return (
    <button type="button" className={cls("tree-row", "file", activePath === node.name && "active")} aria-current={activePath === node.name ? "true" : undefined} style={{ paddingLeft: pad + 16 }} onClick={() => onPick(node)}>
      <Icon name="file" size={14} className="faint" />{node.name}
    </button>
  );
}

function PageFiles({ toast }) {
  const { fileTree, fileSample } = window.DATA;
  const [live, setLive] = useState(null);   // null=loading, true=backend, false=offline
  const [roots, setRoots] = useState(null); // top-level entries
  const [active, setActive] = useState(null); // {name, path}
  const [file, setFile] = useState(null);     // {type:"text"|"image", content, size}
  const [reading, setReading] = useState(false);

  // Load the real workspace root, fall back to design data offline.
  useEffect(() => {
    let alive = true;
    HERMES.filesList("")
      .then((res) => {
        if (!alive) return;
        const entries = (res?.entries || []).map((e) => ({
          name: e.name, type: isDirEntry(e) ? "dir" : "file",
          size: e.size, path: e.path || e.name,
        })).sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1));
        setRoots(entries); setLive(true);
      })
      .catch(() => { if (alive) { setLive(false); } });
    return () => { alive = false; };
  }, []);

  const pickFile = useCallback((node) => {
    setActive({ name: node.name, path: node.path || node.name });
    if (live === false) {
      // offline: show the design sample
      setFile({ type: "text", content: fileSample, size: node.size });
      return;
    }
    setReading(true); setFile(null);
    HERMES.fileRead(node.path)
      .then((res) => {
        // {type:"text",content} | {type:"image",content:dataURI}
        if (res?.type === "image") setFile({ type: "image", content: res.content, size: node.size });
        else setFile({ type: "text", content: typeof res === "string" ? res : (res?.content ?? ""), size: node.size });
      })
      .catch((e) => { setFile({ type: "text", content: "// Unable to read file: " + (e?.message || "error"), size: node.size }); toast && toast(e?.message || "Read failed"); })
      .finally(() => setReading(false));
  }, [live, fileSample, toast]);

  const badge = live === null
    ? <span className="badge"><span className="d" />…</span>
    : live
      ? <span className="badge ok"><span className="d" />live</span>
      : <span className="badge warn"><span className="d" />preview data</span>;

  const lines = file?.type === "text" ? (file.content || "").split("\n") : [];

  return (
    <div className="page">
      <PageHead eyebrow="Workspace" title="Files" sub="Browse and inspect everything mounted in the agent workspace.">
        {badge}
        <button className="btn btn-ghost" onClick={() => toast("Uploaded")}><Icon name="plus" size={16} />Upload</button>
      </PageHead>
      <div className="explorer">
        <div className="tree">
          <div className="mono faint" style={{ fontSize: 11, padding: "2px 8px 8px", letterSpacing: ".08em", textTransform: "uppercase" }}>/workspace</div>
          {live === null && <div className="tree-row faint" style={{ cursor: "default" }}>loading…</div>}
          {live === true && (roots || []).map((n, i) => <LiveTreeNode key={n.path || i} node={n} depth={0} activePath={active?.path} onPick={pickFile} />)}
          {live === false && fileTree.map((n, i) => <StaticTreeNode key={i} node={{ ...n, path: n.name }} depth={0} activePath={active?.path} onPick={pickFile} />)}
        </div>
        <div className="code-pane">
          <div className="code-head">
            <span><Icon name="file" size={13} style={{ display: "inline", verticalAlign: "-2px", marginRight: 6 }} />{active?.name || "Select a file"}</span>
            <div className="flex" style={{ gap: 8 }}>
              {active && <span className="tag">{file?.type === "image" ? "Image" : "Text"}{file?.size ? " · " + file.size : ""}</span>}
              <button className="btn btn-subtle btn-sm" onClick={() => toast("Opened in editor")} disabled={!active}><Icon name="edit" size={13} />Edit</button>
            </div>
          </div>
          <div className="code-body">
            {!active && <div className="empty">Pick a file from the tree to preview it.</div>}
            {active && reading && <div className="empty">Reading…</div>}
            {active && !reading && file?.type === "image" && (
              <img src={file.content} alt={active.name} style={{ maxWidth: "100%", borderRadius: "var(--r-md)", display: "block" }} />
            )}
            {active && !reading && file?.type === "text" && (
              <pre>{lines.map((l, i) => <div key={i}><span className="gutter">{i + 1}</span>{l}</div>)}</pre>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}
window.PageFiles = PageFiles;

// Real PTY terminal: xterm.js wired to /api/terminal-stream|input|resize|close,
// rendered inside the existing .term box (design unchanged).
function PageTerminal({ toast }) {
  const wrapRef = useRef(null);
  const termRef = useRef(null);
  const fitRef = useRef(null);
  const sessionRef = useRef(null);
  const readerRef = useRef(null);
  const [status, setStatus] = useState("connecting"); // connecting | live | error | unsupported
  const [msg, setMsg] = useState("");

  useEffect(() => {
    const Term = window.Terminal;
    if (!Term) { setStatus("unsupported"); setMsg("xterm.js not loaded"); return; }
    const FitCtor = window.FitAddon && window.FitAddon.FitAddon;
    const LinksCtor = window.WebLinksAddon && window.WebLinksAddon.WebLinksAddon;

    const term = new Term({
      cursorBlink: true,
      fontSize: 13,
      fontFamily: 'JetBrains Mono, Menlo, Monaco, Consolas, monospace',
      theme: { background: "#07070b", foreground: "#e6e6e6", cursor: "#ea580c", selectionBackground: "#2b2b2b" },
      scrollback: 5000,
    });
    const fit = FitCtor ? new FitCtor() : null;
    if (fit) term.loadAddon(fit);
    if (LinksCtor) term.loadAddon(new LinksCtor());
    term.open(wrapRef.current);
    try { fit && fit.fit(); } catch { /* */ }
    termRef.current = term; fitRef.current = fit;

    let disposed = false;
    const post = (path, body) => fetch(path, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });

    term.onData((d) => { const sid = sessionRef.current; if (sid) post("/api/terminal-input", { sessionId: sid, data: d }).catch(() => {}); });

    (async () => {
      try {
        const res = await fetch("/api/terminal-stream", {
          method: "POST", credentials: "same-origin",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({ cols: term.cols || 80, rows: term.rows || 24 }),
        });
        if (!res.ok || !res.body) {
          setStatus("error"); setMsg("backend " + res.status);
          term.write("\r\n\x1b[33m⚠ Terminal backend unavailable (HTTP " + res.status + "). Start the stack / sign in to use a live shell.\x1b[0m\r\n");
          return;
        }
        setStatus("live");
        const reader = res.body.getReader(); readerRef.current = reader;
        const dec = new TextDecoder(); let buf = "";
        for (;;) {
          const { value, done } = await reader.read();
          if (done || disposed) break;
          buf += dec.decode(value, { stream: true });
          let i;
          while ((i = buf.indexOf("\n\n")) >= 0) {
            const frame = buf.slice(0, i); buf = buf.slice(i + 2);
            if (!frame.trim() || frame.startsWith(":")) continue;
            let ev = "message", data = "";
            for (const line of frame.split("\n")) {
              if (line.startsWith("event:")) ev = line.slice(6).trim();
              else if (line.startsWith("data:")) data += line.slice(5);
            }
            let p; try { p = data ? JSON.parse(data) : {}; } catch { p = {}; }
            if (ev === "session") sessionRef.current = p.sessionId;
            else if (ev === "data") term.write(p.data || "");
            else if (ev === "exit") term.write("\r\n\x1b[90m[process exited" + (p.code != null ? " " + p.code : "") + "]\x1b[0m\r\n");
            else if (ev === "error") term.write("\r\n\x1b[31m" + (p.message || "stream error") + "\x1b[0m\r\n");
            // ping/close: ignore
          }
        }
      } catch (e) {
        if (!disposed) { setStatus("error"); setMsg(e.message); term.write("\r\n\x1b[31m" + e.message + "\x1b[0m\r\n"); }
      }
    })();

    const onResize = () => {
      if (!fit) return;
      try { fit.fit(); } catch { /* */ }
      const sid = sessionRef.current;
      if (sid) post("/api/terminal-resize", { sessionId: sid, cols: term.cols, rows: term.rows }).catch(() => {});
    };
    window.addEventListener("resize", onResize);
    const fitT = setTimeout(onResize, 150); // re-fit once fonts settle

    return () => {
      disposed = true;
      clearTimeout(fitT);
      window.removeEventListener("resize", onResize);
      try { readerRef.current && readerRef.current.cancel(); } catch { /* */ }
      const sid = sessionRef.current;
      if (sid) post("/api/terminal-close", { sessionId: sid }).catch(() => {});
      try { term.dispose(); } catch { /* */ }
    };
  }, []);

  const badge = status === "live" ? <span className="badge ok"><span className="d" />live shell</span>
    : status === "error" || status === "unsupported" ? <span className="badge bad"><span className="d" />{msg || "unavailable"}</span>
    : <span className="badge"><span className="dotpulse" />connecting…</span>;

  return (
    <div className="page">
      <PageHead eyebrow="Workspace" title="Terminal" sub="A live shell into the agent workspace runtime (PTY).">
        {badge}
        <button className="btn btn-ghost" onClick={() => { try { fitRef.current && fitRef.current.fit(); termRef.current && termRef.current.focus(); } catch { /* */ } }}><Icon name="refresh" size={16} />Fit</button>
      </PageHead>
      <div className="term">
        <div className="term-head">
          <div className="term-dots"><i style={{ background: "#ff5f57" }} /><i style={{ background: "#febc2e" }} /><i style={{ background: "#28c840" }} /></div>
          <span className="mono faint" style={{ fontSize: 12, marginLeft: 6 }}>workspace@hermes — /workspace</span>
        </div>
        <div className="term-xterm" ref={wrapRef} onClick={() => termRef.current && termRef.current.focus()} />
      </div>
    </div>
  );
}
window.PageTerminal = PageTerminal;
