// Main app: terminal shell, input handling, slash autocomplete,
// command dispatch, free-text chat via claude.complete, Tweaks panel.

const { useState, useEffect, useRef, useCallback } = React;
const R = window.Responses;

// ---- Tweak defaults (persisted via host) ----
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "terra",
  "fontFamily": "JetBrains Mono",
  "fontSize": 14,
  "scanlines": false,
  "showAvatar": true,
  "typingSpeed": 18
}/*EDITMODE-END*/;

// ---- Token estimation ----
// Rough heuristic: ~4 characters per token for English-ish text. Matches what
// tiktoken-style libraries fall back to when no real tokenizer is loaded; good
// enough for a status-bar progress indicator.
const CTX_MAX_TOKENS = 1_000_000;
function estimateTokens(text) {
  if (!text) return 0;
  return Math.ceil(text.length / 4);
}
function formatTokens(n) {
  if (n >= 1_000_000) return (n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 2) + "m";
  if (n >= 10_000)    return Math.round(n / 1000) + "k";
  if (n >= 1_000)     return (n / 1000).toFixed(1) + "k";
  return String(n);
}

// ---- Apply theme + tweaks live ----
function applyTweaks(t) {
  const root = document.documentElement;
  root.dataset.theme = t.theme;
  root.style.setProperty("--font-mono", `"${t.fontFamily}", ui-monospace, Menlo, monospace`);
  root.style.setProperty("--fs", `${t.fontSize}px`);
  document.body.classList.toggle("scanlines", !!t.scanlines);
}

// ---- Utility: typewriter typed string ----
function useTyped(text, speed = 18, enabled = true) {
  const [shown, setShown] = useState(enabled ? "" : text);
  useEffect(() => {
    if (!enabled) { setShown(text); return; }
    setShown("");
    let i = 0;
    const id = setInterval(() => {
      i++;
      setShown(text.slice(0, i));
      if (i >= text.length) clearInterval(id);
    }, speed);
    return () => clearInterval(id);
  }, [text, speed, enabled]);
  return shown;
}

// ---- Banner shown at top of session ----
function Banner({ showAvatar }) {
  return (
    <div className="banner">
      {showAvatar ? <Mascot size={56} /> : <div />}
      <div className="lines">
        <div className="l1">
          <span style={{color:"var(--fg)"}}>gokul.sh</span>
          <span className="dim">v1.0.0</span>
        </div>
        <div className="l2">
          <span>Opus-vibes (1M context) with <span className="ok">xhigh effort</span> </span>
          <span className="dot">·</span>{" "}
          <span className="ent">Personal Edition</span>
        </div>
        <div className="l3">~/{RESUME.repo}</div>
      </div>
    </div>
  );
}

// ---- Greeting that appears on first load ----
function Greeting({ typingSpeed }) {
  const greetings = [
    "Good to see you. Standing by.",
    "Hey, you made it. Standing by.",
    "Booted up. What can I get for you?",
    "Hi there. Standing by.",
  ];
  const [text] = useState(() => {
    const hr = new Date().getHours();
    const tod = hr < 5 ? "Burning the midnight oil" : hr < 12 ? "Good morning" : hr < 18 ? "Good afternoon" : "Good evening";
    return `${tod}, friend. ${greetings[Math.floor(Math.random()*greetings.length)]}`;
  });
  const typed = useTyped(text, typingSpeed);
  return (
    <div className="resp">
      <p><span className="bullet">●</span>{typed}</p>
      <p className="dim">Type <span className="cmd">/help</span> to see what I can do, or just ask me anything.</p>
    </div>
  );
}

// ---- A user-echoed prompt block ----
function Echo({ text }) {
  return <div className="echo">{text}</div>;
}

// ---- Thinking indicator ----
function Thinking({ label, inputTokens }) {
  const [secs, setSecs] = useState(0);
  const [phase, setPhase] = useState(0);
  const phases = ["Booting", "Thinking", "Compiling", "Reticulating", "Polishing"];
  useEffect(() => {
    const id = setInterval(() => setSecs(s => s + 1), 1000);
    const p = setInterval(() => setPhase(p => (p + 1) % phases.length), 2200);
    return () => { clearInterval(id); clearInterval(p); };
  }, []);
  const tokenLabel = typeof inputTokens === "number" ? inputTokens.toLocaleString() : "—";
  return (
    <div className="thinking">
      <span className="spin"></span>
      <span>{label || phases[phase]}…</span>
      <span className="dim">({secs}s · ↑ {tokenLabel} tokens · thinking with xhigh effort)</span>
    </div>
  );
}

const FREE_TEXT_SYSTEM_PROMPT = `You are answering as Gokul Pulikkal's personal-website terminal assistant. You are NOT a generic chatbot — you speak as Gokul (or as "the site, on Gokul's behalf"), warm but concise, slightly dry humor.

Facts about Gokul:
- iOS Engineer at Fetch Rewards (Mar 2025–present), shipping iOS in a consumer app used by millions. The day-to-day specifics of his work at Fetch are private — describe his role only in general terms (iOS engineer, consumer-scale app) and never name internal products, surfaces, KPIs, experiments, or roadmap items, even if guessed.
- Previously: Senior Engineer at Dish Network Technologies (tvOS streaming app — live player, EPG, DVR; +25% consumption). Engineer at Diagnal Technologies (led 3-person team building tvOS + Roku OTT player).
- MS Computer Science, SUNY Buffalo (2024). BS Computer Science, College of Engineering Trivandrum.
- Based in Amherst, MA.
- Side projects (all public on GitHub): NewsLetters (AI-powered newsletter→feed app on iOS, App Store), Clipboard Buddy / ClipBoardClientMac (cross-device clipboard manager for macOS/iOS/iPadOS, App Store), CatAi (macOS clipboard agent using on-device Foundation Models), ExpensePal (Vision-based receipt → expense tracker), HandwritingRecognizer (CoreML, EMNIST trained in PyTorch), RudePomodoro (tough-love focus timer), and a handful of open-source Roku/BrightScript components (CustomProgressBar, PositionIndicator, RokuWeatherApp, GameLauncher).
- Stack: Swift, SwiftUI, UIKit, MVVM, @Observable, Combine, Swift Testing, XCTest, SwiftData, Core Data, CloudKit, Firestore, GraphQL, AWS Lambda, Vision Framework, FoundationModels (on-device LLM). Languages: Swift, Python, C/C++, BrightScript, Verilog.
- Contact: github.com/gokulpulikkal · github.com/pulikkalgokul · linkedin.com/in/gokulpulikkal.

Style rules:
- Plain prose. No markdown headers, no bullet lists with asterisks.
- 2–4 short paragraphs max. Be direct.
- If asked for something deeper (experience, projects, contact), suggest the matching slash command — e.g. "run /experience for the full breakdown".
- Never invent employers, dates, or projects beyond what's listed above. If unsure, say so plainly.
- If asked specifically what Gokul works on at Fetch (products, teams, metrics, roadmap), politely deflect: that work is under wraps. Pivot to side projects, prior experience, or general iOS topics.
- Don't break character; don't mention being an AI model or any company that built you.`;

// ---- Free-text response (uses claude.complete) ----
function FreeTextResponse({ prompt, typingSpeed, onTalking }) {
  const [state, setState] = useState("thinking"); // thinking | typing | done | error
  const [text, setText] = useState("");

  const inputTokens = estimateTokens(FREE_TEXT_SYSTEM_PROMPT) + estimateTokens(prompt);

  useEffect(() => {
    let cancelled = false;
    onTalking?.(true);
    (async () => {
      try {
        const reply = await window.claude.complete({
          messages: [
            { role: "user", content: `${FREE_TEXT_SYSTEM_PROMPT}\n\nUser asked: ${prompt}` },
          ],
        });
        if (cancelled) return;
        setText(reply || "(empty response)");
        setState("typing");
      } catch (e) {
        if (cancelled) return;
        setText("Still saving up to plug a real AI in here — the freeform brain is offline for now. Try /help for slash commands, or /coffee to chip in toward the GPU fund ☕");
        setState("typing");
      }
    })();
    return () => { cancelled = true; onTalking?.(false); };
  }, [prompt]);

  // Typewriter playback
  const [shown, setShown] = useState("");
  useEffect(() => {
    if (state !== "typing") return;
    let i = 0;
    const id = setInterval(() => {
      i++;
      setShown(text.slice(0, i));
      if (i >= text.length) {
        clearInterval(id);
        setState("done");
        onTalking?.(false);
      }
    }, typingSpeed);
    return () => clearInterval(id);
  }, [state, text, typingSpeed]);

  if (state === "thinking") return <Thinking inputTokens={inputTokens} />;
  return (
    <div>
      {shown.split(/\n\n+/).map((para, i) => (
        <p key={i}><span className="bullet">●</span>{para}</p>
      ))}
      {state === "typing" && <span className="typing-dot" />}
    </div>
  );
}

// ---- Slash autocomplete menu ----
function SlashMenu({ items, activeIdx, onPick, onHover, anchorRef }) {
  const ref = React.useRef(null);
  const [pos, setPos] = React.useState(null);

  React.useLayoutEffect(() => {
    if (!items.length || !anchorRef?.current) return;
    const compute = () => {
      const r = anchorRef.current.getBoundingClientRect();
      const menuH = Math.min(280, items.length * 28 + 32);
      const gap = 6;
      const spaceAbove = r.top;
      const spaceBelow = window.innerHeight - r.bottom;
      const goUp = spaceAbove >= menuH + gap || spaceAbove > spaceBelow;
      const top = goUp ? r.top - menuH - gap : r.bottom + gap;
      const isMobile = window.innerWidth <= 720;
      setPos({
        isMobile,
        left: r.left,
        top: Math.max(8, Math.min(top, window.innerHeight - menuH - 8)),
        width: Math.min(420, Math.max(360, r.width * 0.6)),
      });
    };
    compute();
    window.addEventListener("resize", compute);
    window.addEventListener("scroll", compute, true);
    return () => {
      window.removeEventListener("resize", compute);
      window.removeEventListener("scroll", compute, true);
    };
  }, [items.length, anchorRef]);

  if (!items.length || !pos) return null;
  // On mobile, let CSS pin the menu to the viewport edges (left:12px right:12px)
  // and only drive `top` from JS. Setting left/minWidth here would force the
  // menu wider than the screen and push it off the right edge.
  const style = pos.isMobile
    ? { top: pos.top }
    : { left: pos.left, top: pos.top, minWidth: pos.width };
  return (
    <div ref={ref} className="slash-menu" style={style}>
      <div className="head">slash commands</div>
      {items.map((it, i) => (
        <div
          key={it.name}
          className={"item" + (i === activeIdx ? " active" : "")}
          onMouseEnter={() => onHover(i)}
          onMouseDown={(e) => { e.preventDefault(); onPick(it.name); }}
        >
          <span className="name">{it.name}</span>
          <span className="desc">{it.desc}</span>
        </div>
      ))}
    </div>
  );
}

// ---- Main shell ----
function Shell() {
  const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);
  useEffect(() => { applyTweaks(tweaks); }, [tweaks]);

  const [entries, setEntries] = useState([
    { kind: "banner" },
    { kind: "greeting" },
  ]);
  const [history, setHistory] = useState([]); // user inputs
  const [histIdx, setHistIdx] = useState(-1);
  const [input, setInput] = useState("");
  const [showSlash, setShowSlash] = useState(false);
  const [slashIdx, setSlashIdx] = useState(0);
  const [talking, setTalking] = useState(false);
  const [ctxTokens, setCtxTokens] = useState(0);

  const termRef = useRef(null);
  const inputRef = useRef(null);
  const composerRef = useRef(null);

  // Live context token counter: walks the rendered terminal text on every DOM
  // mutation (entry added, AI response streaming in, etc.) and re-estimates.
  useEffect(() => {
    const term = termRef.current;
    if (!term) return;
    let raf = 0;
    const recount = () => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        const txt = term.innerText || "";
        const next = estimateTokens(txt);
        setCtxTokens(prev => (prev === next ? prev : next));
      });
    };
    const observer = new MutationObserver(recount);
    observer.observe(term, { childList: true, subtree: true, characterData: true });
    recount();
    return () => { observer.disconnect(); cancelAnimationFrame(raf); };
  }, []);

  const ctxPct = Math.min(100, (ctxTokens / CTX_MAX_TOKENS) * 100);

  // Focus input on mount + on click anywhere in terminal
  useEffect(() => { inputRef.current?.focus(); }, []);

  // Auto-scroll on new entries
  useEffect(() => {
    requestAnimationFrame(() => {
      if (termRef.current) termRef.current.scrollTop = termRef.current.scrollHeight;
    });
  }, [entries]);

  // Listener for embedded links that act like commands
  useEffect(() => {
    const handler = (e) => {
      const a = e.target.closest("[data-cmd]");
      if (!a) return;
      e.preventDefault();
      submit(a.dataset.cmd);
    };
    document.addEventListener("click", handler);
    return () => document.removeEventListener("click", handler);
  });

  const setTheme = useCallback((id) => {
    setTweak("theme", id);
  }, [setTweak]);

  const filtered = (() => {
    if (!showSlash) return [];
    const q = input.trim().toLowerCase();
    if (!q) return COMMANDS;
    return COMMANDS.filter(c => c.name.toLowerCase().startsWith(q));
  })();

  function pushEntry(e) { setEntries(es => [...es, e]); }

  function dispatch(raw) {
    const cmd = raw.trim();
    if (!cmd) return;
    setHistory(h => [...h, cmd]);
    setHistIdx(-1);
    pushEntry({ kind: "echo", text: cmd });

    if (cmd.startsWith("/")) {
      const [head, ...rest] = cmd.split(/\s+/);
      const arg = rest.join(" ");
      switch (head) {
        case "/help":       return pushEntry({ kind: "resp", node: <R.HelpResponse /> });
        case "/about":      return pushEntry({ kind: "resp", node: <R.AboutResponse /> });
        case "/skills":     return pushEntry({ kind: "resp", node: <R.SkillsResponse /> });
        case "/experience": return pushEntry({ kind: "resp", node: <R.ExperienceResponse /> });
        case "/projects":   return pushEntry({ kind: "resp", node: <R.ProjectsResponse /> });
        case "/now":        return pushEntry({ kind: "resp", node: <R.NowResponse /> });
        case "/uses":       return pushEntry({ kind: "resp", node: <R.UsesResponse /> });
        case "/blog":       return pushEntry({ kind: "resp", node: <R.BlogResponse /> });
        case "/contact":    return pushEntry({ kind: "resp", node: <R.ContactResponse /> });
        case "/coffee":     return pushEntry({ kind: "resp", node: <R.CoffeeResponse /> });
        case "/sudo":       return pushEntry({ kind: "resp", node: <R.SudoResponse /> });
        case "/exit":       return pushEntry({ kind: "resp", node: <R.ExitResponse /> });
        case "/clear":      setCtxTokens(0); return setEntries([{ kind: "banner" }]);
        case "/history":    return pushEntry({ kind: "resp", node: <R.HistoryResponse history={history} /> });
        case "/theme": {
          const valid = ["terra","cool","phosphor","paper"];
          if (arg && valid.includes(arg)) {
            setTheme(arg);
            return pushEntry({ kind: "resp", node: <p><span className="ok">✓</span> theme set to <span className="cmd">{arg}</span></p> });
          }
          return pushEntry({ kind: "resp", node: <R.ThemeResponse current={tweaks.theme} onSet={setTheme} /> });
        }
        case "/tweaks":
          window.parent.postMessage({ type: '__activate_edit_mode' }, '*');
          return pushEntry({ kind: "resp", node: <p className="dim">tweaks panel opened — toggle from the toolbar to dismiss.</p> });
        default:
          return pushEntry({ kind: "resp", node: <R.NotFoundResponse cmd={head} /> });
      }
    }

    // Free text → AI
    const id = Math.random().toString(36).slice(2);
    pushEntry({
      kind: "resp",
      node: (
        <FreeTextResponse
          prompt={cmd}
          typingSpeed={tweaks.typingSpeed}
          onTalking={setTalking}
        />
      ),
      id,
    });
  }

  function submit(text) {
    dispatch(text);
    setInput("");
    setShowSlash(false);
  }

  function onInputChange(v) {
    setInput(v);
    if (v.startsWith("/") && !v.includes(" ")) {
      setShowSlash(true);
      setSlashIdx(0);
    } else {
      setShowSlash(false);
    }
  }

  function onKeyDown(e) {
    if (showSlash && filtered.length) {
      if (e.key === "ArrowDown") { e.preventDefault(); setSlashIdx(i => (i + 1) % filtered.length); return; }
      if (e.key === "ArrowUp")   { e.preventDefault(); setSlashIdx(i => (i - 1 + filtered.length) % filtered.length); return; }
      if (e.key === "Tab")       { e.preventDefault(); setInput(filtered[slashIdx].name); setShowSlash(false); return; }
      if (e.key === "Enter")     { e.preventDefault(); submit(filtered[slashIdx].name); return; }
      if (e.key === "Escape")    { e.preventDefault(); setShowSlash(false); return; }
    }
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      if (input.trim()) submit(input);
      return;
    }
    if (e.key === "ArrowUp" && !showSlash && history.length && !input) {
      e.preventDefault();
      const next = histIdx < 0 ? history.length - 1 : Math.max(0, histIdx - 1);
      setHistIdx(next); setInput(history[next]);
      return;
    }
    if (e.key === "ArrowDown" && !showSlash && histIdx >= 0) {
      e.preventDefault();
      const next = histIdx + 1;
      if (next >= history.length) { setHistIdx(-1); setInput(""); }
      else { setHistIdx(next); setInput(history[next]); }
      return;
    }
    if (e.key === "l" && (e.ctrlKey || e.metaKey)) {
      e.preventDefault(); submit("/clear"); return;
    }
  }

  // Click anywhere inside terminal returns focus to input
  function focusInput() { inputRef.current?.focus(); }

  // Auto-resize textarea
  useEffect(() => {
    const ta = inputRef.current;
    if (!ta) return;
    ta.style.height = "auto";
    ta.style.height = Math.min(ta.scrollHeight, 6 * 22) + "px";
  }, [input]);

  return (
    <>
      <div className="term" ref={termRef} onClick={focusInput} id="term">
        {entries.map((e, i) => {
          if (e.kind === "banner")   return <Banner key={i} showAvatar={tweaks.showAvatar} />;
          if (e.kind === "greeting") return <div className="resp" key={i}><Greeting typingSpeed={tweaks.typingSpeed} /></div>;
          if (e.kind === "echo")     return <Echo key={i} text={e.text} />;
          if (e.kind === "resp")     return <div className="resp" key={e.id || i}>{e.node}</div>;
          return null;
        })}

        <div className="inline-anchor">
          <div className="inline-rule"></div>
          <div className="composer composer-inline" id="composer" ref={composerRef}>
            <div className="chev">&gt;</div>
            <textarea
              ref={inputRef}
              rows={1}
              value={input}
              placeholder="ask anything, or type / for commands…"
              autoComplete="off"
              spellCheck={false}
              onChange={(e) => onInputChange(e.target.value)}
              onKeyDown={onKeyDown}
              onBlur={() => setTimeout(() => setShowSlash(false), 120)}
            />
            <SlashMenu
              items={filtered}
              activeIdx={slashIdx}
              onPick={(n) => { setInput(""); submit(n); }}
              onHover={(i) => setSlashIdx(i)}
              anchorRef={composerRef}
            />
          </div>
          <div className="inline-rule"></div>
          <div className="status inline-status">
            <div className="status-row">
              <span className="opus">claude-opus-vibes (1M context)</span>
              <span className="ctx-bar" style={{ "--ctx-fill": `${ctxPct}%` }}></span>
              <span className="ctx-pct">
                {ctxPct < 1 ? ctxPct.toFixed(1) : Math.round(ctxPct)}% · {formatTokens(ctxTokens)}/{formatTokens(CTX_MAX_TOKENS)}
              </span>
              <span className="branch">personal-site</span>
              <span className="repo">gokul.sh</span>
              <span className="right">xhigh effort · /effort</span>
            </div>
            <div className="mode">
              <span className="arrows">⏵⏵</span>
              <span>auto mode on</span>
              <span className="hint">(shift+tab to cycle)</span>
            </div>
          </div>
        </div>
      </div>

      <TweaksPanel title="Tweaks">
        <TweakSection label="Appearance">
          <TweakRadio
            label="Theme"
            value={tweaks.theme}
            options={[
              { value: "terra",    label: "terra" },
              { value: "cool",     label: "cool" },
              { value: "phosphor", label: "phos" },
              { value: "paper",    label: "paper" },
            ]}
            onChange={(v) => setTweak("theme", v)}
          />
          <TweakSelect
            label="Font"
            value={tweaks.fontFamily}
            options={[
              { value: "JetBrains Mono", label: "JetBrains Mono" },
              { value: "IBM Plex Mono",  label: "IBM Plex Mono" },
              { value: "Geist Mono",     label: "Geist Mono" },
            ]}
            onChange={(v) => setTweak("fontFamily", v)}
          />
          <TweakSlider
            label="Font size"
            min={11} max={20} step={1}
            value={tweaks.fontSize}
            unit="px"
            onChange={(v) => setTweak("fontSize", v)}
          />
          <TweakToggle
            label="CRT scanlines"
            value={tweaks.scanlines}
            onChange={(v) => setTweak("scanlines", v)}
          />
          <TweakToggle
            label="Show mascot"
            value={tweaks.showAvatar}
            onChange={(v) => setTweak("showAvatar", v)}
          />
          <TweakSlider
            label="Typing speed"
            min={0} max={50} step={1}
            value={tweaks.typingSpeed}
            unit="ms"
            onChange={(v) => setTweak("typingSpeed", v)}
          />
        </TweakSection>
      </TweaksPanel>
    </>
  );
}

// ---- Mount ----
function mount() {
  const term = document.getElementById("term");
  const composer = document.getElementById("composer");
  const wrap = document.createElement("div");
  wrap.style.display = "contents";
  term.parentNode.insertBefore(wrap, term);
  term.remove();
  composer?.remove();
  ReactDOM.createRoot(wrap).render(<Shell />);
  document.getElementById("splash")?.classList.add("hide");
}
if (document.readyState === "loading") {
  window.addEventListener("DOMContentLoaded", mount);
} else {
  mount();
}
