// Shared shell engine: fake filesystem, command parser, history, autocomplete.
// Used by both the tmux.folio concept and the editorial concept.

const P = window.PROFILE;

// ──────────────────────────────────────────────────────────────────────────
// Fake filesystem.
// ──────────────────────────────────────────────────────────────────────────
const FS = {
  type: "dir",
  children: {
    "about.md": { type: "file", lang: "md", content: () => `# ${P.about.title}\n\n${P.about.description}\n\n> "${P.about.quote}" — ${P.about.quoteAuthor}` },
    "skills.json": { type: "file", lang: "json", content: () => JSON.stringify(P.skills, null, 2) },
    "resume.pdf": { type: "file", lang: "pdf", content: () => "[binary] use `resume` to download." },
    projects: {
      type: "dir",
      children: Object.fromEntries(
        P.projects.map((p) => [
          `${p.name}.md`,
          { type: "file", lang: "md", content: () => `# ${p.name}\n\n${p.subtitle}\n\n${p.description}\n\nTech: ${p.tech.join(", ")}\n\nLink: ${p.link || "—"}` },
        ])
      ),
    },
    experience: {
      type: "dir",
      children: Object.fromEntries(
        P.experiences.map((e) => [
          `${e.company.toLowerCase().replace(/[^a-z0-9]/g, "-")}.log`,
          { type: "file", lang: "log", content: () => `[${e.period}] ${e.position} @ ${e.company}\n${e.note}` },
        ])
      ),
    },
    "contact.vcf": { type: "file", lang: "vcf", content: () => `BEGIN:VCARD\nFN:${P.name}\nEMAIL:${P.contact.email}\nURL:${P.contact.linkedin}\nURL:${P.contact.github}\nEND:VCARD` },
  },
};

function fsResolve(path, cwd) {
  let parts;
  if (path.startsWith("/") || path.startsWith("~")) {
    parts = path.replace(/^~\/?/, "").split("/").filter(Boolean);
  } else {
    parts = [...cwd, ...path.split("/")].filter(Boolean);
  }
  const stack = [];
  for (const p of parts) {
    if (p === "." || p === "") continue;
    if (p === "..") stack.pop();
    else stack.push(p);
  }
  let node = FS;
  for (const p of stack) {
    if (node.type !== "dir" || !node.children[p]) return { node: null, path: stack };
    node = node.children[p];
  }
  return { node, path: stack };
}

function pathStr(parts) {
  return "~/" + parts.join("/");
}

// ──────────────────────────────────────────────────────────────────────────
// Command set. Each handler returns React node(s) to render as output.
// ──────────────────────────────────────────────────────────────────────────
const { useState, useEffect, useRef, useMemo, useCallback } = React;

// Public command set — what we surface in `help`. Keep this small and friendly.
const CommandRegistry = {
  help:     { desc: "show available commands" },
  about:    { desc: "who I am" },
  skills:   { desc: "stack & tooling (live)" },
  projects: { desc: "selected work" },
  experience: { desc: "career history" },
  contact:  { desc: "how to reach me" },
  resume:   { desc: "open my CV" },
  calendar: { desc: "book a 15-min call" },
  open:     { desc: "open any file — try `open resume.pdf`" },
  clear:    { desc: "clear screen" },
  exit:     { desc: "switch to readable view" },
};

// Hidden aliases / nerd commands — work but not advertised in help.
const HiddenCommands = new Set([
  "ls", "cd", "cat", "pwd", "tree", "top", "git", "neofetch", "sudo", "rm",
]);

function ColTable({ rows, cols }) {
  return (
    <div className="font-mono text-[13px]">
      <div className="grid" style={{ gridTemplateColumns: cols.map(c => c.w || "1fr").join(" "), gap: "0 1.5rem" }}>
        {cols.map((c, i) => <div key={i} className="text-black/50 border-b border-black pb-1 mb-1 uppercase tracking-wider text-[10px]">{c.label}</div>)}
        {rows.flatMap((r, ri) => cols.map((c, ci) => <div key={`${ri}-${ci}`} className="py-0.5">{r[c.key]}</div>))}
      </div>
    </div>
  );
}

function HelpOutput({ run }) {
  return (
    <div className="space-y-1">
      <div className="text-black/50 uppercase tracking-wider text-[10px] mb-2">commands · click or type</div>
      <div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-0.5">
        {Object.entries(CommandRegistry).map(([cmd, info]) => (
          <React.Fragment key={cmd}>
            <button
              onClick={() => run(cmd)}
              className="text-left underline underline-offset-2 decoration-black/30 hover:decoration-black hover:bg-black hover:text-white px-1 -mx-1 transition-colors"
            >{cmd}</button>
            <div className="text-black/60">{info.desc}</div>
          </React.Fragment>
        ))}
      </div>
    </div>
  );
}

function AboutOutput() {
  return (
    <div className="space-y-3 max-w-2xl">
      <div className="text-2xl font-semibold tracking-tight font-serif" style={{fontFamily:"'GT Sectra', 'Georgia', serif"}}>{P.about.title}</div>
      <div className="leading-relaxed">{P.about.description}</div>
      <blockquote className="border-l-2 border-black pl-3 text-black/70 italic">"{P.about.quote}" — {P.about.quoteAuthor}</blockquote>
    </div>
  );
}

function SkillsOutput() {
  const groups = [
    ["languages", P.skills.languages],
    ["frameworks", P.skills.frameworks],
    ["databases", P.skills.databases],
    ["tools", P.skills.tools],
  ];
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4">
      {groups.map(([label, items]) => (
        <div key={label}>
          <div className="text-[10px] uppercase tracking-[0.2em] text-black/50 border-b border-black/20 pb-1 mb-2">{label}</div>
          <div className="flex flex-wrap gap-x-2 gap-y-1">
            {items.map(s => <span key={s} className="px-1.5 py-0.5 border border-black/30 text-[12px] hover:bg-black hover:text-white transition-colors">{s}</span>)}
          </div>
        </div>
      ))}
    </div>
  );
}

function ProjectsOutput() {
  // Render as `git log --graph --oneline` over a branch tree.
  // Each project is a commit node on a branch; first project is on `main`,
  // others fork onto feature branches that merge back in.
  const branches = ["main", "research/vision", "systems/cpp", "nlp/french", "tools/info"];
  const colors = ["#111", "#a85d2a", "#1f6f4a", "#8a2a8a", "#264a8a"];
  const items = P.projects.map((p, i) => ({
    ...p,
    branch: branches[i % branches.length],
    color: colors[i % colors.length],
    sha: Math.random().toString(16).slice(2, 9),
  }));

  // Each row is fixed height; we draw a small SVG gutter on the left
  // with the commit graph, and the project info to its right.
  const ROW_H = 92;
  const GUTTER_W = 110;
  const LANE_W = 18;

  return (
    <div className="font-mono text-[12px]">
      <div className="text-black/55 text-[11px] mb-2">$ git log --graph --oneline projects/</div>
      <div className="relative" style={{ paddingLeft: 0 }}>
        {/* Gutter graph */}
        <svg
          width={GUTTER_W}
          height={items.length * ROW_H}
          className="absolute left-0 top-0 pointer-events-none"
          style={{ shapeRendering: "geometricPrecision" }}
        >
          {/* Main spine */}
          <line x1={12} y1={0} x2={12} y2={items.length * ROW_H} stroke="#111" strokeWidth={2} />
          {items.map((it, i) => {
            const cy = i * ROW_H + 26;
            const lane = i === 0 ? 0 : ((i - 1) % 3) + 1; // 0..3 lanes; lane 0 = main
            const cx = 12 + lane * LANE_W;
            // Branch out from spine, then back in from next-row spine if not main.
            const branchPath = lane === 0
              ? null
              : `M 12 ${cy - 30} C 12 ${cy - 12}, ${cx} ${cy - 12}, ${cx} ${cy}`;
            const mergePath = lane === 0
              ? null
              : `M ${cx} ${cy} C ${cx} ${cy + 20}, 12 ${cy + 20}, 12 ${cy + 38}`;
            return (
              <g key={i}>
                {branchPath && <path d={branchPath} stroke={it.color} strokeWidth={1.6} fill="none" opacity={0.9} />}
                {mergePath && <path d={mergePath} stroke={it.color} strokeWidth={1.6} fill="none" opacity={0.5} strokeDasharray="2 3" />}
                <circle cx={cx} cy={cy} r={5.5} fill="#fbfaf7" stroke={it.color} strokeWidth={2.5} />
                <circle cx={cx} cy={cy} r={1.8} fill={it.color} />
              </g>
            );
          })}
        </svg>

        {/* Rows */}
        <div style={{ paddingLeft: GUTTER_W }}>
          {items.map((p, i) => (
            <div key={p.name} style={{ height: ROW_H }} className="group flex flex-col justify-start py-1">
              <div className="flex items-baseline gap-2 flex-wrap">
                <span className="font-mono text-[11px] tabular-nums text-black/40">{p.sha}</span>
                <span
                  className="font-mono text-[10px] uppercase tracking-wider px-1.5 py-px"
                  style={{ color: p.color, border: `1px solid ${p.color}40` }}
                >
                  {p.branch}
                </span>
                <a href={p.link} target="_blank" rel="noreferrer" className="font-semibold underline-offset-4 group-hover:underline">{p.name}</a>
                <span className="text-[11px] text-black/50">— {p.subtitle}</span>
              </div>
              <div className="text-[12.5px] text-black/80 mt-0.5 leading-snug">{p.description}</div>
              <div className="text-[11px] text-black/50 mt-1">{p.tech.join(" · ")}</div>
            </div>
          ))}
        </div>
      </div>
      <div className="mt-2 text-black/45 text-[11px]">{items.length} commits · HEAD → main</div>
    </div>
  );
}

function ContactOutput() {
  const rows = [
    ["email", P.contact.email, `mailto:${P.contact.email}`],
    ["linkedin", P.contact.linkedin.replace("https://", ""), P.contact.linkedin],
    ["github", P.contact.github.replace("https://", ""), P.contact.github],
    ["website", P.contact.website.replace("https://", ""), P.contact.website],
    ["calendar", "book a 15-min call", P.contact.calendar],
  ];
  return (
    <div className="space-y-1">
      {rows.map(([k, v, href]) => (
        <div key={k} className="flex gap-4">
          <span className="w-20 text-black/50 uppercase tracking-wider text-[10px] pt-0.5">{k}</span>
          <a href={href} target="_blank" rel="noreferrer" className="underline underline-offset-2 decoration-black/30 hover:decoration-black">{v}</a>
        </div>
      ))}
    </div>
  );
}

function ResumeOutput() {
  return (
    <div className="space-y-2 max-w-2xl">
      <div className="text-black/70">{P.resume.headline}</div>
      <div className="text-[10px] uppercase tracking-[0.2em] text-black/50 border-b border-black/20 pb-1">positions</div>
      {P.experiences.map(e => (
        <div key={e.company} className="grid grid-cols-[120px_1fr] gap-3 text-[13px]">
          <span className="text-black/50 tabular-nums">{e.period}</span>
          <div><strong>{e.position}</strong> · {e.company}<div className="text-black/60 text-[12px]">{e.note}</div></div>
        </div>
      ))}
      <div className="pt-2">
        <a href={P.resume.url} download className="inline-block border border-black px-3 py-1.5 hover:bg-black hover:text-white transition-colors">↓ Download full PDF</a>
        <span className="text-black/50 text-[11px] ml-3">updated {P.resume.lastUpdated}</span>
      </div>
    </div>
  );
}

function TopOutput() {
  // Skills as processes. Bars oscillate 80–100% — these are strong skills.
  const allSkills = [
    ...P.skills.languages.slice(0, 5).map(n => ({ n, kind: "lang" })),
    ...P.skills.frameworks.slice(0, 4).map(n => ({ n, kind: "fw" })),
    ...P.skills.databases.slice(0, 3).map(n => ({ n, kind: "db" })),
  ];
  const procs = useMemo(() => allSkills.map((s, i) => ({
    pid: 1000 + i * 7,
    user: P.handle,
    mem: 2 + ((i * 7) % 50),
    name: s.n,
    kind: s.kind,
    phase: (i * 0.7) % (Math.PI * 2),
    speed: 0.4 + ((i * 0.13) % 0.6),
  })), []);

  const [tick, setTick] = useState(0);
  useEffect(() => { const id = setInterval(() => setTick(t => t + 1), 350); return () => clearInterval(id); }, []);

  return (
    <div className="font-mono text-[12px]">
      <div className="text-black/60 mb-2">top — skills({procs.length}), load avg {(0.85 + Math.sin(tick * 0.2) * 0.1).toFixed(2)}, all systems hot</div>
      <div className="grid grid-cols-[60px_70px_60px_60px_1fr_2fr] gap-x-3 border-b border-black pb-1 mb-1 text-[10px] uppercase tracking-wider text-black/60">
        <span>PID</span><span>USER</span><span>%CPU</span><span>%MEM</span><span>NAME</span><span>USAGE</span>
      </div>
      {procs.map(p => {
        // Oscillate between 80 and 100.
        const live = 90 + Math.sin(tick * p.speed + p.phase) * 10;
        const memLive = Math.max(60, Math.min(99, p.mem + 60 + Math.cos(tick * p.speed * 0.8 + p.phase) * 6));
        return (
          <div key={p.pid} className="grid grid-cols-[60px_70px_60px_60px_1fr_2fr] gap-x-3 py-0.5 items-center">
            <span className="text-black/50">{p.pid}</span>
            <span className="text-black/70">{p.user}</span>
            <span className="tabular-nums">{live.toFixed(1)}</span>
            <span className="tabular-nums text-black/60">{memLive.toFixed(1)}</span>
            <span>{p.name}</span>
            <div className="h-3 bg-black/5 relative overflow-hidden">
              <div className="absolute inset-y-0 left-0 bg-black transition-all duration-300 ease-in-out" style={{ width: `${live}%` }}></div>
            </div>
          </div>
        );
      })}
    </div>
  );
}

// Experience as a friendly list (the public-facing version of `git log`).
function ExperienceOutput() {
  return (
    <div className="space-y-3">
      {P.experiences.map((e, i) => (
        <div key={e.company} className="border-l-2 border-black pl-3">
          <div className="flex items-baseline gap-3 flex-wrap">
            <span className="text-black/40 text-[11px] tabular-nums font-mono">{String(i + 1).padStart(2, "0")}</span>
            <span className="font-semibold">{e.position}</span>
            <span className="text-black/60">· {e.company}</span>
            <span className="text-black/40 text-[11px] font-mono ml-auto">{e.period}</span>
          </div>
          <div className="text-[13px] text-black/70 mt-0.5">{e.note}</div>
        </div>
      ))}
    </div>
  );
}

// `open <thing>` — smart open: maps files → their natural command.
function OpenOutput({ args, cwd, run }) {
  const target = args[0];
  if (!target) return <div>open: missing operand · try <code className="px-1 bg-black/5">open resume.pdf</code></div>;

  // Friendly aliases first — user-facing names route to commands.
  const aliases = {
    "resume": "resume", "resume.pdf": "resume", "cv": "resume", "cv.pdf": "resume",
    "about": "about", "about.md": "about",
    "skills": "skills", "skills.json": "skills",
    "projects": "projects",
    "experience": "experience",
    "contact": "contact", "contact.vcf": "contact",
    "calendar": "calendar", "cal": "calendar",
  };
  const key = target.toLowerCase();
  if (aliases[key]) {
    // Defer so the parent finishes rendering this command's output first.
    setTimeout(() => run(aliases[key]), 0);
    return <div className="text-black/60">opening <span className="text-black">{target}</span> …</div>;
  }

  // Otherwise resolve as a path. If a project file, route to projects view.
  const { node, path } = fsResolve(target, cwd);
  if (!node) return <div>open: {target}: no such file or directory</div>;
  if (node.type === "dir") {
    setTimeout(() => run(`cd ${target}`), 0);
    return <div className="text-black/60">opening directory <span className="text-black">{target}</span> …</div>;
  }
  // It's a file — fall back to printing it.
  return <CatOutput args={[target]} cwd={cwd} />;
}

function GitLogOutput() {
  const commits = P.experiences.map((e, i) => ({
    sha: Math.random().toString(16).slice(2, 9),
    period: e.period,
    msg: `${e.position} @ ${e.company}`,
    note: e.note,
    refs: i === 0 ? ["HEAD", "main"] : [],
  }));
  return (
    <div className="font-mono text-[12px] space-y-3">
      {commits.map((c, i) => (
        <div key={c.sha}>
          <div className="flex items-baseline gap-2">
            <span className="text-black/50">commit</span>
            <span>{c.sha}{Math.random().toString(16).slice(2, 32)}</span>
            {c.refs.length > 0 && <span className="text-black">({c.refs.join(", ")})</span>}
          </div>
          <div className="text-black/60 ml-4">Author: {P.name} &lt;{P.contact.email}&gt;</div>
          <div className="text-black/60 ml-4">Date:   {c.period}</div>
          <div className="ml-8 mt-1 font-semibold">{c.msg}</div>
          <div className="ml-8 text-black/70">{c.note}</div>
        </div>
      ))}
    </div>
  );
}

function CalendarOutput() {
  const today = new Date();
  const days = Array.from({ length: 21 }, (_, i) => {
    const d = new Date(today); d.setDate(d.getDate() + i);
    const free = ![0, 6].includes(d.getDay()) && i % 3 !== 1;
    return { d, free };
  });
  const slots = ["09:30", "11:00", "14:00", "16:30"];
  const [sel, setSel] = useState(null);
  const [slot, setSlot] = useState(null);
  return (
    <div className="space-y-3 max-w-xl">
      <div className="text-black/60">15-minute slot · Europe/Paris · pick a day &amp; time</div>
      <div className="grid grid-cols-7 gap-1">
        {days.map((x, i) => (
          <button key={i} disabled={!x.free}
            onClick={() => { setSel(i); setSlot(null); }}
            className={`p-2 text-[11px] border ${sel === i ? "bg-black text-white border-black" : x.free ? "border-black/30 hover:border-black" : "border-black/10 text-black/30 cursor-not-allowed line-through"}`}>
            <div className="text-[9px] uppercase">{x.d.toLocaleDateString(undefined, { weekday: "short" })}</div>
            <div className="font-semibold">{x.d.getDate()}</div>
          </button>
        ))}
      </div>
      {sel !== null && (
        <div>
          <div className="text-[10px] uppercase tracking-[0.2em] text-black/50 mb-1">slots</div>
          <div className="flex flex-wrap gap-1">
            {slots.map(s => (
              <button key={s} onClick={() => setSlot(s)} className={`px-2 py-1 text-[12px] border ${slot === s ? "bg-black text-white border-black" : "border-black/30 hover:border-black"}`}>{s}</button>
            ))}
          </div>
        </div>
      )}
      {slot && (
        <a href={P.contact.calendar} target="_blank" rel="noreferrer" className="inline-block border border-black px-3 py-1.5 hover:bg-black hover:text-white transition-colors">
          → Confirm {days[sel].d.toDateString()} at {slot}
        </a>
      )}
    </div>
  );
}

function LsOutput({ args, cwd }) {
  const target = args[0] || ".";
  const { node } = fsResolve(target, cwd);
  if (!node) return <div className="text-black">ls: {target}: no such file or directory</div>;
  if (node.type === "file") return <div>{target}</div>;
  return (
    <div className="grid grid-cols-2 md:grid-cols-3 gap-x-4 font-mono text-[13px]">
      {Object.entries(node.children).map(([n, c]) => (
        <span key={n} className={c.type === "dir" ? "font-semibold" : ""}>{c.type === "dir" ? `${n}/` : n}</span>
      ))}
    </div>
  );
}

function CatOutput({ args, cwd }) {
  const target = args[0];
  if (!target) return <div className="text-black">cat: missing operand</div>;
  // Friendly redirects for the headline files.
  const lower = target.toLowerCase();
  if (lower === "resume.pdf" || lower === "cv.pdf" || lower.endsWith("/resume.pdf")) return <ResumeOutput />;
  if (lower === "contact.vcf" || lower.endsWith("/contact.vcf")) return <ContactOutput />;
  const { node } = fsResolve(target, cwd);
  if (!node) return <div>cat: {target}: no such file</div>;
  if (node.type === "dir") return <div>cat: {target}: is a directory</div>;
  return <pre className="whitespace-pre-wrap font-mono text-[12px] leading-relaxed">{node.content()}</pre>;
}

function TreeOutput({ cwd }) {
  function render(node, prefix = "", isLast = true, name = "~") {
    const lines = [];
    const conn = prefix === "" ? "" : (isLast ? "└── " : "├── ");
    lines.push(prefix + conn + name + (node.type === "dir" ? "/" : ""));
    if (node.type === "dir") {
      const entries = Object.entries(node.children);
      entries.forEach(([n, c], i) => {
        const last = i === entries.length - 1;
        const np = prefix + (prefix === "" ? "" : (isLast ? "    " : "│   "));
        lines.push(...render(c, np, last, n));
      });
    }
    return lines;
  }
  return <pre className="font-mono text-[12px] leading-tight">{render(FS).join("\n")}</pre>;
}

function NotFound({ cmd }) {
  return <div>command not found: <strong>{cmd}</strong> · try <code className="px-1 bg-black/5">help</code></div>;
}

// ──────────────────────────────────────────────────────────────────────────
// useShell — central state hook used by both concepts.
// ──────────────────────────────────────────────────────────────────────────
function useShell({ onExit } = {}) {
  const [history, setHistory] = useState([]); // {id, cmd, output, ts, cwd}
  const [cmdHistory, setCmdHistory] = useState([]); // raw inputs for ↑↓
  const [hIdx, setHIdx] = useState(-1);
  const [input, setInput] = useState("");
  const [cwd, setCwd] = useState([]);
  const tabState = useRef({ lastInput: null }); // tracks double-tab to list

  // ── Tab completion engine ──────────────────────────────────────────────
  // Returns { candidates: string[], replaceFrom: number, suffixOnSingle: string }
  // replaceFrom is the index into `input` where the completion should be applied.
  const computeCompletions = useCallback((raw) => {
    const allCommands = [...Object.keys(CommandRegistry), ...HiddenCommands];

    // Find the token under the cursor (we treat as "end of input").
    const lastSpace = raw.lastIndexOf(" ");
    const beforeToken = lastSpace === -1 ? "" : raw.slice(0, lastSpace + 1);
    const token = lastSpace === -1 ? raw : raw.slice(lastSpace + 1);
    const isFirstToken = lastSpace === -1;

    if (isFirstToken) {
      const matches = allCommands.filter(c => c.startsWith(token.toLowerCase())).sort();
      return { candidates: matches, replaceFrom: 0, prefix: beforeToken, token, kind: "cmd" };
    }

    // File/path completion. Split the path token: dirPart / leafPart.
    const slash = token.lastIndexOf("/");
    const dirPart = slash === -1 ? "" : token.slice(0, slash + 1);
    const leafPart = slash === -1 ? token : token.slice(slash + 1);
    const baseSpec = dirPart === "" ? "." : (dirPart.endsWith("/") ? dirPart.slice(0, -1) : dirPart);
    const { node } = fsResolve(baseSpec || ".", cwd);
    if (!node || node.type !== "dir") {
      return { candidates: [], replaceFrom: raw.length - leafPart.length, prefix: beforeToken + dirPart, token: leafPart, kind: "path" };
    }
    const entries = Object.entries(node.children)
      .filter(([n]) => n.toLowerCase().startsWith(leafPart.toLowerCase()))
      .map(([n, c]) => n + (c.type === "dir" ? "/" : ""))
      .sort();
    return { candidates: entries, replaceFrom: raw.length - leafPart.length, prefix: beforeToken + dirPart, token: leafPart, kind: "path" };
  }, [cwd]);

  // Longest common prefix of an array of strings (case-insensitive match against token,
  // but we preserve the original casing of the candidates).
  const longestCommonPrefix = (arr) => {
    if (arr.length === 0) return "";
    let p = arr[0];
    for (let i = 1; i < arr.length; i++) {
      let j = 0;
      while (j < p.length && j < arr[i].length && p[j].toLowerCase() === arr[i][j].toLowerCase()) j++;
      p = p.slice(0, j);
      if (!p) break;
    }
    return p;
  };

  // Suggested completion to render as a ghost after the cursor.
  const ghost = useMemo(() => {
    if (!input) return "";
    const { candidates, token } = computeCompletions(input);
    if (candidates.length === 0) return "";
    const lcp = longestCommonPrefix(candidates);
    if (lcp.length <= token.length) return "";
    return lcp.slice(token.length);
  }, [input, computeCompletions]);

  const run = useCallback((raw) => {
    const trimmed = raw.trim();
    if (!trimmed) return;
    const [cmd, ...args] = trimmed.split(/\s+/);
    const c = cmd.toLowerCase();

    if (c === "clear") { setHistory([]); setInput(""); return; }
    if (c === "exit") { onExit && onExit(); return; }
    if (c === "cd") {
      const target = args[0] || "";
      const { node, path } = fsResolve(target || "~", cwd);
      if (!node || node.type !== "dir") {
        setHistory(h => [...h, { id: Date.now(), cmd: trimmed, cwd, output: <div>cd: {target}: not a directory</div>, ts: new Date() }]);
      } else {
        setCwd(path);
        setHistory(h => [...h, { id: Date.now(), cmd: trimmed, cwd, output: null, ts: new Date() }]);
      }
      setCmdHistory(h => [...h, trimmed]); setHIdx(-1); setInput(""); return;
    }

    let output;
    switch (c) {
      case "help":     output = <HelpOutput run={run} />; break;
      case "about":    output = <AboutOutput />; break;
      case "skills":   output = <TopOutput />; break;
      case "projects": output = <ProjectsOutput />; break;
      case "contact":  output = <ContactOutput />; break;
      case "resume":   output = <ResumeOutput />; break;
      case "top":      output = <TopOutput />; break;
      case "experience": output = <ExperienceOutput />; break;
      case "open":     output = <OpenOutput args={args} cwd={cwd} run={run} />; break;
      case "git":      output = args[0] === "log" ? <GitLogOutput /> : <div>git: try <code>git log</code></div>; break;
      case "calendar": case "cal": output = <CalendarOutput />; break;
      case "ls":       output = <LsOutput args={args} cwd={cwd} />; break;
      case "cat":      output = <CatOutput args={args} cwd={cwd} />; break;
      case "tree":     output = <TreeOutput cwd={cwd} />; break;
      case "pwd":      output = <div>{pathStr(cwd)}</div>; break;
      case "neofetch": output = <NeofetchOutput />; break;
      case "sudo":     output = <div>{P.handle} is not in the sudoers file. This incident will be reported.</div>; break;
      case "rm":       output = args.includes("-rf") ? <div>nice try.</div> : <div>rm: missing operand</div>; break;
      default:         output = <NotFound cmd={cmd} />;
    }
    setHistory(h => [...h, { id: Date.now() + Math.random(), cmd: trimmed, cwd, output, ts: new Date() }]);
    setCmdHistory(h => [...h, trimmed]); setHIdx(-1); setInput("");
  }, [cwd, onExit]);

  const onKey = useCallback((e) => {
    if (e.ctrlKey && e.key === "l") { e.preventDefault(); setHistory([]); return; }
    if (e.ctrlKey && e.key === "c") { e.preventDefault(); setInput(""); return; }
    if (e.key === "Enter") { e.preventDefault(); run(input); return; }
    if (e.key === "Tab") {
      e.preventDefault();
      const { candidates, prefix, token, kind } = computeCompletions(input);
      if (candidates.length === 0) return;
      if (candidates.length === 1) {
        const only = candidates[0];
        const trailing = kind === "cmd"
          ? " "
          : (only.endsWith("/") ? "" : " ");
        setInput(prefix + only + trailing);
        tabState.current.lastInput = null;
        return;
      }
      const lcp = longestCommonPrefix(candidates);
      if (lcp.length > token.length) {
        setInput(prefix + lcp);
        tabState.current.lastInput = prefix + lcp;
        return;
      }
      // No progress possible — second tab on same input lists candidates.
      if (tabState.current.lastInput === input) {
        // Print as a fake history entry that doesn't affect cmdHistory/cwd.
        const list = (
          <div className="font-mono text-[12px]">
            <div className="grid gap-x-4 gap-y-0.5" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))" }}>
              {candidates.map(c => <span key={c} className={c.endsWith("/") ? "font-semibold" : ""}>{c}</span>)}
            </div>
          </div>
        );
        setHistory(h => [...h, { id: Date.now() + Math.random(), cmd: input, cwd, output: list, ts: new Date(), isCompletion: true }]);
        tabState.current.lastInput = null;
      } else {
        tabState.current.lastInput = input;
      }
      return;
    }
    if (e.key === "ArrowRight") {
      // Accept ghost completion when caret is at end of input.
      const el = e.currentTarget;
      const atEnd = el.selectionStart === input.length && el.selectionEnd === input.length;
      if (atEnd && ghost) {
        e.preventDefault();
        setInput(input + ghost);
        return;
      }
    }
    if (e.key === "ArrowUp") {
      e.preventDefault();
      const ni = hIdx === -1 ? cmdHistory.length - 1 : Math.max(0, hIdx - 1);
      if (cmdHistory[ni] !== undefined) { setHIdx(ni); setInput(cmdHistory[ni]); }
    }
    if (e.key === "ArrowDown") {
      e.preventDefault();
      if (hIdx === -1) return;
      const ni = hIdx + 1;
      if (ni >= cmdHistory.length) { setHIdx(-1); setInput(""); }
      else { setHIdx(ni); setInput(cmdHistory[ni]); }
    }
    // Any other typing key resets the double-tab state.
    if (e.key.length === 1 || e.key === "Backspace") tabState.current.lastInput = null;
  }, [input, hIdx, cmdHistory, run, computeCompletions, ghost]);

  return { history, setHistory, input, setInput, onKey, run, cwd, pathStr, ghost };
}

function NeofetchOutput() {
  const ascii = [
    "        █████╗ ███╗   ███╗",
    "       ██╔══██╗████╗ ████║",
    "       ███████║██╔████╔██║",
    "       ██╔══██║██║╚██╔╝██║",
    "       ██║  ██║██║ ╚═╝ ██║",
    "       ╚═╝  ╚═╝╚═╝     ╚═╝",
  ].join("\n");
  const info = [
    ["user", P.handle + "@" + P.host],
    ["os", "BrutalOS 1.0 (mono)"],
    ["host", "Paris × Milano"],
    ["shell", "morvansh 2.6"],
    ["editor", "vim"],
    ["uptime", "23y 4mo"],
    ["packages", `${P.skills.languages.length + P.skills.frameworks.length + P.skills.databases.length + P.skills.tools.length} (skills)`],
    ["projects", `${P.projects.length} listed`],
  ];
  return (
    <div className="grid grid-cols-[auto_1fr] gap-6 font-mono text-[12px]">
      <pre className="leading-tight">{ascii}</pre>
      <div>
        <div className="font-bold border-b border-black pb-0.5 mb-1">{P.handle}@{P.host}</div>
        {info.map(([k, v]) => <div key={k}><span className="text-black/60">{k}:</span> {v}</div>)}
      </div>
    </div>
  );
}

window.useShell = useShell;
window.CommandRegistry = CommandRegistry;
window.fsResolve = fsResolve;
window.pathStr = pathStr;
window.HelpOutput = HelpOutput;
window.NeofetchOutput = NeofetchOutput;
