// Chat — a generic, general-purpose assistant conversation wired to /api/send-stream.
// NOTE: this is intentionally NOT the orchestrator. The "Main Agent / Orchestrator"
// concept lives in Operations (the persistent agent team) and Conductor (missions).
// Chat is its own session ("chat" by default). Previous sessions are listed in the
// in-page history rail (HERMES.listSessions()), not in the app sidebar.
const { useState, useRef, useEffect } = React;

// Generic assistant avatar (neutral — not an agent persona).
function AsstAvatar({ size = 30 }) {
  return (
    <div className="msg-ava" style={{ width: size, height: size, background: "var(--primary-soft)", color: "var(--primary)", border: "1px solid var(--primary-ring)" }}>
      <Icon name="sparkle" size={Math.round(size * 0.52)} />
    </div>
  );
}

function ChatMessage({ m }) {
  const html = (m.text || "")
    .replace(/&/g, "&amp;").replace(/</g, "&lt;")
    .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
    .replace(/`(.+?)`/g, "<code>$1</code>");
  return (
    <div className={cls("msg", m.role)}>
      {m.role === "assistant"
        ? <AsstAvatar size={30} />
        : <div className="msg-ava" style={{ background: "var(--primary)" }}>You</div>}
      <div className="msg-body">
        {m.role === "assistant" && <div className="msg-meta">Assistant</div>}
        <div className="bubble" dangerouslySetInnerHTML={{ __html: html }} />
        {m.tools && m.tools.length > 0 && <div className="flex" style={{ gap: 6 }}>{m.tools.map((t, i) => <span className="tool-chip" key={i}><Icon name="skills" size={11} />{t}</span>)}</div>}
      </div>
    </div>
  );
}

function modelLabel(mo) {
  if (typeof mo === "string") return mo;
  return mo && (mo.id || mo.name || mo.model) || "";
}

// relative time from an ISO/ms timestamp -> "8m ago" style (mirrors page-dashboard.jsx's helper)
function relTime(ts) {
  if (!ts) return "";
  const numeric = typeof ts === "number" ? ts : Number(ts);
  const t = Number.isFinite(numeric) ? (numeric < 1e12 ? numeric * 1000 : numeric) : Date.parse(ts);
  if (!t || isNaN(t)) return "";
  const s = Math.max(0, (Date.now() - t) / 1000);
  if (s < 60) return "just now";
  if (s < 3600) return Math.floor(s / 60) + "m ago";
  if (s < 86400) return Math.floor(s / 3600) + "h ago";
  if (s < 172800) return "yesterday";
  return Math.floor(s / 86400) + "d ago";
}

// Normalize a raw HERMES.listSessions() row into { key, title, time }.
// Titles derived from the first user turn can leak the injected
// <workspace_context .../> XML — strip it (helper lives in ui.jsx).
function normalizeSessionRow(s) {
  return {
    key: s.key || s.id || s.sessionKey,
    title: stripInjectedContext(s.title || s.label || s.derivedTitle || s.friendlyId) || "Session",
    time: relTime(s.updatedAt || s.last_active || s.startedAt || s.started_at),
    model: s.model || "",
  };
}

function chatModelMeta(id) {
  if (!id || id === "hermes-agent") return { id: "", provider: "Hermes Agent", label: "Default model", detail: "Uses the main model configured in Hermes" };
  if (id === "claude-cli") return { id, provider: "Claude subscription", label: "Automation", detail: "Runs a bounded Claude Code CLI job" };
  if (id.startsWith("claude-cli-chat")) {
    const alias = id.replace(/^claude-cli-chat\/?/, "");
    return { id, provider: "Claude subscription", label: alias ? alias[0].toUpperCase() + alias.slice(1) : "Default", detail: "Runs through Claude Code subscription auth" };
  }
  if (id === "codex-cli" || id.startsWith("codex-cli/")) {
    const alias = id.replace(/^codex-cli\/?/, "");
    const match = /^gpt-(\d+\.\d+)-(sol|terra|luna)$/.exec(alias);
    const label = match ? `${match[1]} ${match[2][0].toUpperCase()}${match[2].slice(1)}` : alias || "Default";
    return { id, provider: "Codex subscription", label, detail: "Runs through Codex subscription auth" };
  }
  if (id.startsWith("azure-openai/")) return { id, provider: "Azure OpenAI", label: id.slice("azure-openai/".length), detail: "Uses the configured Azure deployment" };
  if (id.startsWith("openrouter/")) return { id, provider: "OpenRouter", label: id.slice("openrouter/".length), detail: "Uses the configured OpenRouter API key" };
  if (id === "gemini-cli") return { id, provider: "Gemini subscription", label: "Default", detail: "Runs through Gemini CLI auth" };
  return { id, provider: "Hermes models", label: id, detail: "Runs through the Hermes Agent provider" };
}

function uniqueChatModels(ids) {
  const seen = new Set();
  const providerOrder = ["Hermes Agent", "Claude subscription", "Codex subscription", "Azure OpenAI", "OpenRouter", "Gemini subscription", "Hermes models"];
  return ["", ...(ids || [])]
    .map(chatModelMeta)
    .filter((m) => !seen.has(m.id) && seen.add(m.id))
    .sort((a, b) => providerOrder.indexOf(a.provider) - providerOrder.indexOf(b.provider));
}

function normalizeHistory(messages) {
  return (messages || []).map((m) => {
    const role = m.role === "user" ? "user" : "assistant";
    let text = "";
    if (typeof m.content === "string") text = m.content;
    else if (Array.isArray(m.content)) text = m.content.filter((b) => b && (b.type === "text" || b.text)).map((b) => b.text || "").join("");
    else text = m.text || m.message || "";
    return { role, text: stripInjectedContext(text) };
  }).filter((m) => (m.text || "").trim());
}

function PageChat({ toast, sessionId }) {
  const { models: seedModels } = window.DATA;
  // Generic chat session — separate from the Operations orchestrator ("main").
  // activeKey lets the in-page history rail switch sessions without routing
  // through app.jsx (sessionId prop still seeds the initial session).
  const [activeKey, setActiveKey] = useState(sessionId || "chat");
  const sessionKey = activeKey;
  const [msgs, setMsgs] = useState([]);
  const [draft, setDraft] = useState("");
  const [models, setModels] = useState(() => uniqueChatModels(seedModels));
  const [model, setModel] = useState(() => {
    try { return localStorage.getItem("chat.selectedModel") || ""; } catch (e) { return ""; }
  });
  const [menu, setMenu] = useState(false);
  const [typing, setTyping] = useState(false);
  const [live, setLive] = useState(null);
  const [sessions, setSessions] = useState([]);
  const [railOpen, setRailOpen] = useState(true);
  const [historyLoading, setHistoryLoading] = useState(true);
  const [managingHistory, setManagingHistory] = useState(false);
  const [editKey, setEditKey] = useState(null);
  const [editTitle, setEditTitle] = useState("");
  const scrollRef = useRef(null);
  const taRef = useRef(null);
  const streamRef = useRef(null);
  // Tracks the currently displayed session so in-flight stream callbacks from
  // a previous session can't write into a newly loaded transcript. abort()
  // stops future reads, but already-parsed SSE frames may still fire.
  const sessionKeyRef = useRef(sessionKey);
  useEffect(() => { sessionKeyRef.current = sessionKey; }, [sessionKey]);

  useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, [msgs, typing]);

  const loadSessions = () => {
    HERMES.listSessions(50)
      .then((res) => {
        const raw = Array.isArray(res) ? res : (res && res.sessions) || [];
        setSessions(raw.map(normalizeSessionRow).filter((s) => s.key));
      })
      .catch(() => { /* offline — rail stays empty */ });
  };

  useEffect(() => { loadSessions(); }, []);

  useEffect(() => {
    let alive = true;
    const listedSession = sessions.find((s) => s.key === sessionKey);
    setHistoryLoading(true);
    const historyRequest = listedSession ? HERMES.sessionMessages(sessionKey) : HERMES.sessionHistory(sessionKey);
    historyRequest
      .then((res) => {
        if (!alive) return;
        const raw = Array.isArray(res) ? res : (res && res.messages) || [];
        setMsgs(normalizeHistory(raw));
        setLive(true);
        setHistoryLoading(false);
      })
      .catch((e) => { if (alive) { setMsgs([]); setLive(false); setHistoryLoading(false); toast(e.message || "Could not load session history"); } });

    Promise.allSettled([HERMES.gatewayModels(), HERMES.listModels()])
      .then((results) => {
        if (!alive) return;
        const gatewayRes = results[0].status === "fulfilled" ? results[0].value : null;
        const hermesRes = results[1].status === "fulfilled" ? results[1].value : null;
        const gateway = (gatewayRes && gatewayRes.models ? gatewayRes.models : gatewayRes || []).map(modelLabel).filter(Boolean);
        const hermes = (hermesRes && hermesRes.models ? hermesRes.models : hermesRes || []).map(modelLabel).filter(Boolean);
        const list = uniqueChatModels([...gateway, ...hermes, ...seedModels]);
        setModels(list);
        setModel((m) => list.some((x) => x.id === m) ? m : "");
      })
      .catch(() => { /* keep seed models */ });

    return () => { alive = false; if (streamRef.current) { try { streamRef.current.abort(); } catch (e) { /* */ } } };
  }, [sessionKey, sessions]);

  useEffect(() => {
    try { localStorage.setItem("chat.selectedModel", model); } catch (e) { /* private mode */ }
  }, [model]);

  const newChat = () => {
    if (streamRef.current) { try { streamRef.current.abort(); } catch (e) { /* */ } }
    setActiveKey(`chat-${Date.now().toString(36)}`);
    setManagingHistory(false);
    setEditKey(null);
  };

  const selectSession = (key) => {
    if (streamRef.current) { try { streamRef.current.abort(); } catch (e) { /* */ } }
    setTyping(false);
    setMsgs([]);
    setActiveKey(key);
    setEditKey(null);
  };

  const beginRename = (s) => { setEditKey(s.key); setEditTitle(s.title === "Session" ? "" : s.title); };
  const saveRename = async () => {
    const title = editTitle.trim();
    const key = editKey;
    if (!key || !title) { setEditKey(null); return; }
    try {
      await HERMES.renameSession(key, title);
      setSessions((rows) => rows.map((s) => s.key === key ? { ...s, title } : s));
      setEditKey(null);
      toast("Session renamed");
    } catch (e) { toast(e.message || "Could not rename session"); }
  };

  const simulate = () => {
    setTimeout(() => {
      setTyping(false);
      setMsgs((m) => [...m, { role: "assistant", text: "_(Preview mode — sign in and start the stack to chat with a live model.)_" }]);
    }, 900);
  };

  const send = async () => {
    const text = draft.trim();
    if (!text || typing) return;
    setMsgs((m) => [...m, { role: "user", text }]);
    setDraft(""); if (taRef.current) taRef.current.style.height = "auto";
    setTyping(true);

    let acc = "";
    let started = false; // whether a streaming assistant bubble was rendered
    const keyAtSend = sessionKey;
    const stale = () => sessionKeyRef.current !== keyAtSend;
    // Update the trailing streaming bubble, creating it on first write. Pure
    // updaters (no side effects inside setMsgs) so double-invocation is safe.
    const patchBubble = (patch) => setMsgs((m) => {
      const out = m.slice();
      const last = out[out.length - 1];
      if (last && last.role === "assistant" && last.streaming) out[out.length - 1] = patch(last);
      else out.push(patch({ role: "assistant", text: "", tools: [], streaming: true }));
      return out;
    });
    const settleBubble = () => setMsgs((m) => m.map((mm) => (mm.streaming ? { ...mm, streaming: false } : mm)));
    const writeText = (t) => { if (stale()) return; started = true; patchBubble((b) => ({ ...b, text: t })); };
    const addTool = (name) => { if (stale()) return; started = true; patchBubble((b) => ({ ...b, tools: [...(b.tools || []), name] })); };

    try {
      streamRef.current = await HERMES.sendStream(
        { message: text, sessionKey, ...(model ? { model } : {}) },
        {
          started: () => { setTyping(true); },
          chunk: (d) => {
            const piece = (d && d.text) || "";
            if (d && d.fullReplace) acc = piece; else acc += piece;
            writeText(acc);
            setTyping(false);
          },
          tool: (d) => { if (d && (d.phase === "calling" || !d.phase) && d.name) addTool(d.name); },
          done: () => { setTyping(false); streamRef.current = null; settleBubble(); loadSessions(); },
          error: (d) => {
            setTyping(false); streamRef.current = null; settleBubble();
            if (stale()) return;
            if (!acc && !started) { simulate(); setTyping(true); }
            toast((d && d.message) || "Stream error");
          },
          end: () => { setTyping(false); streamRef.current = null; settleBubble(); },
        }
      );
    } catch (err) {
      setLive(false);
      toast("preview mode");
      simulate();
    }
  };

  const onKey = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } };
  const grow = (e) => { e.target.style.height = "auto"; e.target.style.height = Math.min(160, e.target.scrollHeight) + "px"; setDraft(e.target.value); };

  return (
    <div className="page">
      <div className="chat-layout">
        <aside className={cls("chat-rail", !railOpen && "collapsed")}>
          <div className="chat-rail-top">
            <div className={cls("nav-group-head", !railOpen && "closed")} onClick={() => setRailOpen((o) => !o)} style={{ padding: "0 0 6px", flex: 1 }}>
              <span className="nav-section">History</span>
              <Icon name="chevronDown" size={14} className="chev" />
            </div>
            {railOpen && <button className={cls("icon-btn", managingHistory && "active")} style={{ width: 28, height: 28 }} title={managingHistory ? "Done editing history" : "Edit history"} onClick={() => { setManagingHistory((v) => !v); setEditKey(null); }}><Icon name={managingHistory ? "check" : "edit"} size={15} /></button>}
          </div>
          {railOpen && (
            <div className="nav-sessions">
              {!sessions.length && <div className="faint" style={{ fontSize: 12.5, padding: "6px 11px" }}>No previous sessions yet.</div>}
              {sessions.map((s) => (
                <div key={s.key} className={cls("session-edit-row", s.key === sessionKey && "active")}>
                  {editKey === s.key ? (
                    <>
                      <input className="input" autoFocus value={editTitle} placeholder="Session title" onChange={(e) => setEditTitle(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") saveRename(); if (e.key === "Escape") setEditKey(null); }} />
                      <button className="icon-btn" title="Save title" onClick={saveRename}><Icon name="check" size={13} /></button>
                      <button className="icon-btn" title="Cancel" onClick={() => setEditKey(null)}><Icon name="x" size={13} /></button>
                    </>
                  ) : (
                    <>
                      <button className={cls("session-link", s.key === sessionKey && "active")} onClick={() => selectSession(s.key)}>
                        <span className="st">{s.title}</span>
                        {s.time && <span className="faint mono" style={{ fontSize: 10.5, flexShrink: 0 }}>{s.time}</span>}
                      </button>
                      {managingHistory && <button className="icon-btn session-rename" title={`Rename ${s.title}`} onClick={() => beginRename(s)}><Icon name="edit" size={12} /></button>}
                    </>
                  )}
                </div>
              ))}
            </div>
          )}
        </aside>

      <div className="chat-wrap">
        <div className="chat-head">
          <div className="agent-ring"><div className="agent-ava" style={{ background: "var(--primary-soft)", border: "1px solid var(--primary-ring)", color: "var(--primary)" }}><Icon name="sparkle" size={26} /></div></div>
          <h2 style={{ fontSize: 18, fontWeight: 700, margin: "2px 0" }}>Assistant
            {live === true && <span className="badge ok" style={{ marginLeft: 8, verticalAlign: "middle" }}><span className="d" />live</span>}
            {live === false && <span className="badge warn" style={{ marginLeft: 8, verticalAlign: "middle" }}><span className="d" />preview</span>}
          </h2>
          <div className="faint mono" style={{ fontSize: 12 }}>General-purpose chat · {chatModelMeta(model).provider} · {chatModelMeta(model).label}</div>
        </div>

        <div className="chat-scroll" ref={scrollRef}>
          {!msgs.length && !typing && (
            <div className="empty" style={{ margin: "auto" }}>{historyLoading ? "Loading conversation…" : sessions.some((s) => s.key === sessionKey) ? "No messages in this session." : "Ask anything to start a conversation."}</div>
          )}
          {msgs.map((m, i) => <ChatMessage m={m} key={i} />)}
          {typing && (
            <div className="msg assistant">
              <AsstAvatar size={30} />
              <div className="msg-body"><div className="msg-meta">Assistant</div><div className="bubble"><span className="cursor-blink" style={{ background: "var(--muted)" }} /></div></div>
            </div>
          )}
        </div>

        <div style={{ position: "relative" }}>
          <div className="composer">
            <textarea ref={taRef} value={draft} onChange={grow} onKeyDown={onKey} rows={1} placeholder="Ask anything…  (↵ to send · ⇧↵ new line · ⌘⇧M switch model)" />
            <div className="composer-bar">
              <button className="icon-btn" aria-label="Attach a file" style={{ width: 34, height: 34 }} onClick={() => toast("Attach a file")}><Icon name="attach" size={16} /></button>
              <button className="model-pill" onClick={() => setMenu((v) => !v)} title="Choose provider and model"><Icon name="sliders" size={13} />{chatModelMeta(model).provider}: {chatModelMeta(model).label}<Icon name="chevronDown" size={12} /></button>
              <div style={{ flex: 1 }} />
              <button className="icon-btn" aria-label="Voice input" style={{ width: 34, height: 34 }} onClick={() => toast("Voice input")}><Icon name="mic" size={16} /></button>
              <button className="send-btn" aria-label="Send message" onClick={send}><Icon name="send" size={16} /></button>
            </div>
          </div>
          {menu && (
            <div className="card chat-model-menu" style={{ position: "absolute", bottom: "calc(100% + 8px)", left: 50, width: 390, padding: 8, zIndex: 20 }}>
              <div style={{ padding: "6px 9px 8px" }}><b>Provider & model</b><p style={{ marginTop: 3 }}>Choose how this conversation is routed.</p></div>
              {models.map((mo, i) => (
                <React.Fragment key={mo.id || "hermes-default"}>
                  {(i === 0 || models[i - 1].provider !== mo.provider) && <div className="section-title" style={{ padding: "9px 9px 4px" }}>{mo.provider}</div>}
                  <button className={cls("session-link", mo.id === model && "active")} style={{ borderRadius: 8 }} onClick={() => { setModel(mo.id); setMenu(false); }}>
                    <Icon name="bolt" size={13} className="faint" />
                    <span className="st"><b className="mono" style={{ fontSize: 12.5 }}>{mo.label}</b><small className="faint" style={{ display: "block", marginTop: 2 }}>{mo.detail}</small></span>
                    {mo.id === model && <Icon name="check" size={14} style={{ color: "var(--primary)" }} />}
                  </button>
                </React.Fragment>
              ))}
            </div>
          )}
        </div>
      </div>
      </div>
    </div>
  );
}
window.PageChat = PageChat;
