"use client";
/* Playground - chat that runs a real workflow over SSE with token-by-token streaming. */
import { useEffect, useMemo, useRef, useState } from "react";
import { Icon } from "../icons";
import { Tile } from "../primitives";
import { api, openSSE, Workflow, ComponentT } from "@/lib/api";
import { fmtUSD, buildNodeLabels, nodeLabel } from "@/lib/data";
import { Markdown } from "../markdown";
import { ComponentRenderer } from "../component-renderer";
import { ReplyAccumulator, type Part, type ComponentInstance } from "@/lib/chat-parts";
import Mustache from "mustache";

interface ChatMsg { role: "user" | "assistant"; content?: string; parts?: Part[] }
interface Step { node: string }
interface Activity { id: string; kind: string; name: string; done: boolean; error?: boolean }

export function PlaygroundScreen({ project }: { project: any }) {
  const [wf, setWf] = useState<Workflow | null>(null);
  const [wfs, setWfs] = useState<Workflow[]>([]);
  const [loadErr, setLoadErr] = useState<string | null>(null);
  const [input, setInput] = useState("");
  const [msgs, setMsgs] = useState<ChatMsg[]>([]);
  const [streaming, setStreaming] = useState("");
  const [steps, setSteps] = useState<Step[]>([]);
  const [activity, setActivity] = useState<Activity[]>([]);
  const [running, setRunning] = useState(false);
  const [meter, setMeter] = useState<{ tokens: number; cost: number } | null>(null);
  const [pendingInterrupt, setPendingInterrupt] = useState<{ runId: string; payload: any } | null>(null);
  const [resuming, setResuming] = useState(false);
  const [compDefs, setCompDefs] = useState<Record<string, ComponentT>>({});
  const [liveParts, setLiveParts] = useState<Part[]>([]);  // in-flight assistant reply parts, rendered live (audit H3)
  // Resolve node ids (what the run stream emits) to the friendly names shown on the canvas, so the
  // Run steps read like the graph the operator built rather than raw ids.
  const nodeLabels = useMemo(() => buildNodeLabels(wf), [wf]);
  const scrollRef = useRef<HTMLDivElement>(null);
  // One backend thread per chat session: the checkpointer holds the conversation, so
  // each turn sends ONLY the new message (no full-transcript replay).
  const threadRef = useRef<string | null>(null);
  // Aborts the in-flight SSE stream when the user hits Stop.
  const abortRef = useRef<AbortController | null>(null);

  useEffect(() => {
    if (!project?.id) return;
    threadRef.current = null;
    setWf(null); setLoadErr(null); setMsgs([]); setSteps([]); setMeter(null);
    api.listComponents(project.id).then((cs) => setCompDefs(Object.fromEntries(cs.map((c) => [c.id, c])))).catch(() => {});
    api.listWorkflows(project.id)
      .then((ws) => {
        setWfs(ws);
        const active = ws.find((w) => w.status === "active") || ws[0] || null;
        setWf(active);
        if (!active) setLoadErr("No workflows in this project yet. Create one in Workflows, or ask the Forge Assistant to build one.");
      })
      .catch((e) => setLoadErr(String(e.message || e)));
  }, [project?.id]);

  useEffect(() => { scrollRef.current?.scrollTo({ top: 1e9, behavior: "smooth" }); }, [msgs, streaming, steps, liveParts]);

  async function send(textArg?: string) {
    const text = (typeof textArg === "string" ? textArg : input).trim();
    if (!text || !wf || running) return;
    if (typeof textArg !== "string") setInput("");
    setMsgs((m) => [...m, { role: "user", content: text }]);
    setStreaming(""); setSteps([]); setActivity([]); setMeter(null); setRunning(true); setLiveParts([]);
    let finalAnswer = "";
    // The reply is an ordered list of parts (text + components). Components are positioned by the
    // [[forge:component:ID]] markers the agent writes into its text - NOT by the order their
    // frames arrive - so a widget lands in its natural place instead of always at the top.
    const acc = new ReplyAccumulator();
    try {
      // The thread's checkpointer holds prior turns, so send only the new message when a
      // thread exists; the first turn establishes the thread. The run always acts as the logged-in
      // operator (the backend sets end_user from the session), so per-user auth providers resolve
      // this user's own connected credential — exactly like the Workflow test panel.
      const run = await api.createRun(
        project.id, wf.id,
        { messages: [{ role: "user", content: text }] },
        threadRef.current || undefined,
      );
      threadRef.current = run.thread_id;
      let interrupted = false;
      const url = api.runStreamUrl(project.id, wf.id, run.id);
      const controller = new AbortController();
      abortRef.current = controller;
      await openSSE(url, (f) => {
        if (f.event === "messages" && f.data?.content) {
          acc.addText(f.data.content);
          setStreaming(acc.text);
          setLiveParts(acc.parts({ streaming: true }));
        } else if ((f.event === "node_start" || f.event === "updates") && f.data) {
          const node = f.event === "node_start" ? f.data.node : Object.keys(f.data || {})[0];
          if (node) setSteps((s) => (s.some((x) => x.node === node) ? s : [...s, { node }]));
        } else if (f.event === "activity" && f.data?.id) {
          const a = f.data;
          if (a.phase === "start") {
            setActivity((xs) => xs.some((x) => x.id === a.id) ? xs : [...xs, { id: a.id, kind: a.kind, name: a.name, done: false }]);
          } else if (a.phase === "end") {
            setActivity((xs) => xs.map((x) => (x.id === a.id ? { ...x, done: true, error: !!a.error } : x)));
          }
        } else if (f.event === "custom" && f.data?.channel === "component" && f.data?.payload) {
          acc.addComponent(f.data.payload as ComponentInstance);
          setLiveParts(acc.parts({ streaming: true }));
        } else if (f.event === "done") {
          finalAnswer = f.data?.answer || "";
          setMeter({ tokens: f.data?.total_tokens ?? 0, cost: f.data?.total_cost_usd ?? 0 });
        } else if (f.event === "interrupt") {
          interrupted = true;
          setPendingInterrupt({ runId: run.id, payload: f.data });
        } else if (f.event === "error") {
          finalAnswer = `⚠ ${f.data?.message || "run failed"}`;
        }
      }, { signal: controller.signal });
      if (interrupted) {
        // Preserve anything streamed before the pause (audit M4) - mirror the finalize commit.
        if (acc.hasComponents() || acc.text.trim()) {
          setMsgs((m) => [...m, acc.hasComponents()
            ? { role: "assistant", parts: acc.parts() }
            : { role: "assistant", content: acc.text }]);
        }
        setStreaming(""); setRunning(false); setLiveParts([]);
        return; // approval card takes over
      }
    } catch (e: any) {
      // A user-initiated Stop aborts the fetch - commit whatever streamed, no error banner.
      if (e?.name !== "AbortError") finalAnswer = `⚠ ${e.message || e}`;
    } finally {
      abortRef.current = null;
      setStreaming("");
      setRunning(false);
      setLiveParts([]);
    }
    // resolveText reconciles the streamed buffer with the authoritative final answer / error
    // (covers non-LLM nodes that don't stream tokens) without dropping it (audit H2).
    const finalText = acc.resolveText(finalAnswer);
    if (acc.hasComponents()) {
      setMsgs((m) => [...m, { role: "assistant", parts: acc.parts({ finalText }) }]);
    } else {
      setMsgs((m) => [...m, { role: "assistant", content: finalText || "(no output)" }]);
    }
  }

  /** Pull the human-facing prompt + decision options out of the interrupt payload.
      Handles both shapes: the human_input node ({prompt, allowed_decisions}) and
      HumanInTheLoopMiddleware (action requests; resume wants {decisions:[{type}]}). */
  function parseInterrupt(payload: any): { prompt: string; decisions: string[]; middleware: boolean } {
    const flat = (x: any): any[] => (Array.isArray(x) ? x.flatMap(flat) : [x]);
    const items = flat(payload).filter(Boolean);
    const values = items.map((i) => (i && typeof i === "object" && "value" in i ? i.value : i));
    for (const v of values) {
      if (v && typeof v === "object" && v.prompt) {
        return { prompt: String(v.prompt), decisions: v.allowed_decisions || ["approve", "reject"], middleware: false };
      }
      if (v && typeof v === "object" && (v.action_requests || v.action_request || v.action)) {
        const reqs = v.action_requests || [v.action_request || v];
        const desc = reqs.map((r: any) => r.description || `${r.action || r.name || "tool"}(${JSON.stringify(r.args || {}).slice(0, 80)})`).join("; ");
        return { prompt: `Approve tool call: ${desc}`, decisions: ["approve", "reject"], middleware: true };
      }
    }
    return { prompt: "This run paused for your approval.", decisions: ["approve", "reject"], middleware: false };
  }

  async function resume(decision: string) {
    if (!pendingInterrupt || !wf || resuming) return;
    const { middleware } = parseInterrupt(pendingInterrupt.payload);
    setResuming(true);
    try {
      const value = middleware ? { decisions: [{ type: decision }] } : decision;
      const res = await api.resumeRun(project.id, wf.id, pendingInterrupt.runId, value);
      const msgsOut = res.messages || [];
      const last = [...msgsOut].reverse().find((m: any) => (m.type === "ai" || m.role === "assistant") && m.content);
      const content = last ? (typeof last.content === "string" ? last.content : JSON.stringify(last.content)) : (res.error || "(resumed)");
      setMsgs((m) => [...m, { role: "assistant", content: res.interrupted ? content + "\n⏸ paused again for another approval - check Traces." : content }]);
    } catch (e: any) {
      setMsgs((m) => [...m, { role: "assistant", content: `⚠ resume failed: ${e.message || e}` }]);
    } finally {
      setPendingInterrupt(null);
      setResuming(false);
    }
  }

  // Abort the in-flight SSE run. The stream reader rejects with AbortError, which send()
  // treats as a graceful stop (partial output is committed, no error banner).
  function stop() {
    abortRef.current?.abort();
  }
  // Reset clears the UI AND starts a fresh conversation thread, so the next message has no
  // server-side history (nulling threadRef) - not just a visual clear.
  function reset() {
    threadRef.current = null;
    setMsgs([]); setSteps([]); setMeter(null); setStreaming(""); setLiveParts([]); setPendingInterrupt(null);
  }

  function handleComponentAction(inst: ComponentInstance, action: string, fields: Record<string, string>) {
    const def = (inst.actions || []).find((a: any) => a.id === action) || {};
    let msg = (def as any).message || (def as any).label || action;
    try { msg = Mustache.render(String(msg), { props: inst.props || {}, fields, action }); } catch {}
    if (msg) send(msg);
  }

  const samples = ["How do I reset my password?", "What can you help me with?"];

  return (
    <div className="col" style={{ flex: 1, minHeight: 0 }}>
      {/* header */}
      <div className="row spread" style={{ padding: "12px 20px", borderBottom: "1px solid var(--line)", flex: "none" }}>
        <div className="row gap2">
          <div>
            <div className="t-display">Playground</div>
            {wfs.length > 1 ? (
              <select
                className="select" disabled={running}
                value={wf?.id || ""}
                style={{ marginTop: 2, height: 24, fontSize: 12, padding: "0 6px", maxWidth: 280 }}
                onChange={(e) => {
                  const next = wfs.find((w) => w.id === e.target.value) || null;
                  threadRef.current = null;
                  setWf(next); setMsgs([]); setSteps([]); setMeter(null); setStreaming("");
                }}>
                {wfs.map((w) => (
                  <option key={w.id} value={w.id}>{w.name}{w.status === "active" ? " · active" : " · draft"}</option>
                ))}
              </select>
            ) : (
              <div className="fg-2 t-caption mono">{wf ? wf.name : "loading…"}{running && " · running"}</div>
            )}
          </div>
        </div>
        <div className="row gap2">
          {meter && (
            <span className="chip chip-mono"><Icon name="bolt" size={13} />{meter.tokens} tok · {fmtUSD(meter.cost)}</span>
          )}
          <span className="chip chip-mono"><Icon name="knowledge" size={12} />grounded</span>
          <button className="btn btn-ghost btn-sm" onClick={reset} disabled={running}>
            <Icon name="refresh" size={14} />Reset
          </button>
        </div>
      </div>

      {/* alignItems:stretch - the global .row centers children, which stops the chat column
          from filling the height: it then sizes to content, overflows the viewport, and the
          scroll area never scrolls. Stretch restores the fixed-height column + inner scroll. */}
      <div className="row" style={{ flex: 1, minHeight: 0, alignItems: "stretch" }}>
        <div className="col grow" style={{ minWidth: 0, minHeight: 0, borderRight: "1px solid var(--line)" }}>
          <div ref={scrollRef} className="scroll-y" style={{ flex: 1, minHeight: 0, padding: "20px 0" }}>
            {/* minHeight:100% + justify-end pins a short conversation to the bottom (chat-style);
                once it outgrows the viewport it scrolls normally. */}
            <div style={{ maxWidth: 720, margin: "0 auto", padding: "0 24px", width: "100%", minHeight: "100%", display: "flex", flexDirection: "column", justifyContent: "flex-end" }}>
              {loadErr && <div className="card" style={{ padding: 14, color: "var(--err)", marginBottom: 12 }}>{loadErr}</div>}
              {msgs.length === 0 && !running && !loadErr && (
                <div className="col center" style={{ minHeight: 300, gap: 10, color: "var(--fg-2)", textAlign: "center", margin: "auto 0" }}>
                  <Tile icon="sparkles" color="var(--accent)" size={44} glow />
                  <div className="t-h2" style={{ color: "var(--fg-1)" }}>Run “{wf?.name || "your workflow"}” live</div>
                  <div className="t-caption">Answers are grounded in this project’s knowledge base & Q&A - it streams token by token.</div>
                  <div className="row gap2 wrap center" style={{ maxWidth: 460, marginTop: 6 }}>
                    {samples.map((s) => (
                      <button key={s} className="chip" style={{ cursor: "pointer" }} onClick={() => setInput(s)}>{s}</button>
                    ))}
                  </div>
                </div>
              )}
              <div className="col gap4">
                {msgs.map((m, i) => (
                  <MessageBlock key={i} role={m.role} content={m.content} parts={m.parts} compDefs={compDefs} onAction={handleComponentAction} />
                ))}
                {(running || liveParts.length > 0) && (
                  <MessageBlock role="assistant" parts={liveParts} streaming compDefs={compDefs} onAction={handleComponentAction} />
                )}
                {pendingInterrupt && (() => {
                  const info = parseInterrupt(pendingInterrupt.payload);
                  return (
                    <div className="card fade-up" style={{ padding: 14, borderLeft: "3px solid var(--warn)" }}>
                      <div className="row gap2" style={{ marginBottom: 6 }}>
                        <Icon name="user" size={15} style={{ color: "var(--warn)" }} />
                        <span className="t-h3">Approval required</span>
                      </div>
                      <div className="t-body-sm fg-1" style={{ marginBottom: 10, whiteSpace: "pre-wrap" }}>{info.prompt}</div>
                      <div className="row gap2">
                        {info.decisions.map((d) => (
                          <button key={d}
                            className={d === "approve" ? "btn btn-primary btn-sm" : "btn btn-secondary btn-sm"}
                            disabled={resuming} onClick={() => resume(d)}>
                            {resuming ? "…" : d}
                          </button>
                        ))}
                      </div>
                    </div>
                  );
                })()}
              </div>
            </div>
          </div>
          {/* composer */}
          <div style={{ padding: "14px 24px", borderTop: "1px solid var(--line)", flex: "none" }}>
            <div style={{ maxWidth: 720, margin: "0 auto" }}>
              <div className="row gap2" style={{ background: "var(--bg-1)", border: "1px solid var(--line-strong)", borderRadius: 12, padding: "7px 7px 7px 14px", boxShadow: "var(--sh-1)" }}>
                <input value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && send()}
                  placeholder="Message the workflow…" disabled={!wf || running}
                  style={{ flex: 1, minWidth: 0, border: "none", background: "none", outline: "none", fontSize: 14, color: "var(--fg-0)", fontFamily: "var(--font-ui)" }} />
                {running ? (
                  <button className="btn btn-secondary" onClick={stop} title="Stop the run">
                    <Icon name="stop" size={15} />Stop
                  </button>
                ) : (
                  <button className="btn btn-primary" onClick={() => send()} disabled={!wf}>
                    <Icon name="play" size={15} />Run
                  </button>
                )}
              </div>
              <div className="fg-2 t-caption" style={{ textAlign: "center", marginTop: 7 }}>Runs against the active workflow · interrupts surface for approval</div>
            </div>
          </div>
        </div>

        {/* Steps column */}
        <div style={{ width: 280, flex: "none", background: "var(--bg-1)", minHeight: 0 }} className="scroll-y">
          <div className="t-micro" style={{ padding: "14px 16px 8px" }}>Run steps</div>
          <div className="col" style={{ padding: "0 12px 12px" }}>
            {steps.length === 0 && <div className="fg-2 t-caption" style={{ padding: "4px 8px" }}>Nodes light up as the graph executes.</div>}
            {steps.map((s, i) => (
              <div key={i} className="row gap2 fade-in" style={{ padding: "8px 8px", borderRadius: 7 }}>
                <div style={{ width: 18, height: 18, borderRadius: "50%", background: "var(--ok-bg)", color: "var(--ok)", display: "flex", alignItems: "center", justifyContent: "center", flex: "none" }}><Icon name="check" size={12} /></div>
                <div className="grow" style={{ minWidth: 0 }}>
                  <div className="t-body-sm truncate" style={{ color: "var(--fg-1)" }}>{nodeLabel(s.node, nodeLabels)}</div>
                  {nodeLabels[s.node] && nodeLabels[s.node].label !== s.node && (
                    <div className="t-caption fg-2 mono truncate">{s.node}</div>
                  )}
                </div>
                <span className="t-caption fg-2">{i + 1}</span>
              </div>
            ))}
            {running && (
              <div className="row gap2" style={{ padding: "8px", color: "var(--accent)" }}>
                <div style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--accent)", animation: "pulse 1s infinite" }} />
                <span className="t-caption">streaming…</span>
              </div>
            )}
            {/* Live agent activity: sub-agent dispatches (prominent) and tool calls (dimmed), so a
                deep_agent's routing is visible as it happens - not just after the run in Traces. */}
            {activity.length > 0 && (
              <>
                <div className="t-micro" style={{ padding: "12px 4px 6px" }}>Agent activity</div>
                {activity.map((a) => {
                  const isSub = a.kind === "subagent";
                  return (
                    <div key={a.id} className="row gap2 fade-in" style={{ padding: "6px 8px", borderRadius: 7 }}>
                      <div style={{ width: 16, height: 16, display: "flex", alignItems: "center", justifyContent: "center", flex: "none" }}>
                        {a.error ? <Icon name="x" size={12} style={{ color: "var(--err)" }} />
                          : a.done ? <Icon name="check" size={12} style={{ color: isSub ? "var(--accent)" : "var(--ok)" }} />
                          : <div style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--accent)", animation: "pulse 1s infinite" }} />}
                      </div>
                      <Icon name={isSub ? "layers" : "sliders"} size={13} style={{ color: isSub ? "var(--accent)" : "var(--fg-2)", flex: "none" }} />
                      <span className="mono-sm grow truncate" style={{ color: isSub ? "var(--fg-0)" : "var(--fg-2)", fontWeight: isSub ? 600 : 400 }} title={a.name}>{a.name}</span>
                    </div>
                  );
                })}
              </>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

/* One chat turn. User turns keep the colored bubble. An assistant turn is ONE flowing reply
   under a single avatar - bare markdown text and inline components in order, with NO bubble
   chrome - so a rendered component reads as part of the reply, not a detached card below it
   (audit Priority C). Missing component defs degrade to a visible notice (audit M5). */
function MessageBlock({ role, content, streaming, parts, compDefs, onAction }: {
  role: "user" | "assistant";
  content?: string;
  streaming?: boolean;
  parts?: Part[];
  compDefs: Record<string, ComponentT>;
  onAction: (inst: ComponentInstance, action: string, fields: Record<string, string>) => void;
}) {
  const user = role === "user";
  if (user) {
    return (
      <div className="row" style={{ gap: 9, alignItems: "flex-start", flexDirection: "row-reverse" }}>
        <Tile icon="user" color="var(--fg-2)" size={28} />
        <div style={{ maxWidth: 560, padding: "10px 13px", borderRadius: 12, borderTopRightRadius: 3, fontSize: 14, lineHeight: "21px", whiteSpace: "pre-wrap", overflowWrap: "anywhere", wordBreak: "break-word", background: "var(--accent)", color: "var(--fg-on-accent)" }}>
          {content}
        </div>
      </div>
    );
  }
  const renderComp = (inst: ComponentInstance, key: number | string) => {
    const def = compDefs[inst.component_id];
    if (!def) {
      return (
        <div key={key} className="card" style={{ padding: "8px 11px", fontSize: 12.5, color: "var(--fg-2)" }}>
          Component “{inst.name || inst.component_id}” is unavailable.
        </div>
      );
    }
    return (
      <ComponentRenderer key={key} def={{ id: def.id, name: def.name, html: def.html, css: def.css, actions: inst.actions || def.actions }} props={inst.props || {}} onAction={(a, f) => onAction(inst, a, f)} />
    );
  };
  const list: Part[] = parts && parts.length ? parts : (content && content.trim() ? [{ kind: "text", text: content }] : []);
  let lastText = -1;
  for (let k = list.length - 1; k >= 0; k--) { if (list[k].kind === "text") { lastText = k; break; } }
  return (
    <div className="row" style={{ gap: 9, alignItems: "flex-start" }}>
      <Tile icon="sparkles" color="var(--accent)" size={28} />
      <div className="col gap2" style={{ minWidth: 0, maxWidth: 620, flex: 1 }}>
        {list.map((p, j) => (p.kind === "text" ? (
          <div key={j} style={{ fontSize: 14, lineHeight: "21px", color: "var(--fg-0)", overflowWrap: "anywhere" }}>
            <Markdown>{p.text}</Markdown>
            {streaming && j === lastText && <span style={{ display: "inline-block", width: 7, height: 14, background: "var(--accent)", verticalAlign: "-2px", animation: "blink 1s steps(1) infinite" }} />}
          </div>
        ) : renderComp(p.inst, j)))}
        {streaming && lastText === -1 && <span className="fg-2" style={{ fontSize: 14 }}>…</span>}
      </div>
    </div>
  );
}
