"use client";

import {
  useEffect,
  useRef,
  useState,
  useCallback,
  type ReactNode,
  type CSSProperties,
} from "react";
import Link from "next/link";
import { useSession } from "next-auth/react";
import { useParams, usePathname } from "next/navigation";
import {
  MessageSquare,
  X,
  ChevronRight,
  Send,
  Plus,
  Loader2,
  Wrench,
  ChevronDown,
  Trash2,
  Square,
  MousePointerClick,
  Sparkles,
  Compass,
  Cpu,
} from "lucide-react";
import { ProjectIconRail } from "@/components/project/project-icon-rail";
import {
  PreviewBridgeProvider,
  previewMessagePrepRef,
  usePreviewBridge,
} from "@/components/project/preview-bridge-context";

// ── Types ─────────────────────────────────────────────────────────────────────

interface Thread {
  id: string;
  title: string;
  updatedAt: string;
}

interface Message {
  id?: string;
  role: "user" | "assistant" | "tool";
  content: string;
  toolCalls?: { id: string; name: string; args: Record<string, unknown> }[];
  toolName?: string;
  createdAt?: string;
  /**
   * Chronological turn timeline interleaving the model's thinking
   * narration and the tool calls it fired. Rendered as a stack of
   * pills INSIDE the bubble above the final text content, so the
   * user sees the actual flow:
   *   [thought] [tool ×N] [thought] [tool] ... [summary]
   * Each thought is its own collapsed pill (click to expand);
   * adjacent runs of the same tool name collapse into one pill
   * with a ×N counter. The final assistant text is rendered
   * separately, below the timeline.
   */
  timeline?: TimelineEntry[];
}

type TimelineEntry =
  | { kind: "thought"; text: string }
  | { kind: "tool"; name: string; status: "running" | "done"; result?: string }
  // A text segment from one round of the assistant's tool loop.
  // Each text SSE event from the server starts a new entry; subsequent
  // streaming chunks for that same round append to the most-recent
  // text entry. Tool/thought entries between text segments break the
  // accumulation so multi-round turns render as separate bubbles.
  | { kind: "text"; text: string };

// ── Helpers ───────────────────────────────────────────────────────────────────

function getFriendlyCategory(name: string): string {
  if (
    name.includes("fs.edit") ||
    name.includes("fs.write") ||
    name.includes("fs_edit") ||
    name.includes("fs_write")
  )
    return "Writing code";
  if (
    name.includes("fs.read") ||
    name.includes("fs.list") ||
    name.includes("fs.grep") ||
    name.includes("fs.tree") ||
    name.includes("fs_read") ||
    name.includes("fs_list") ||
    name.includes("fs_grep") ||
    name.includes("fs_tree")
  )
    return "Reading codebase";
  if (name.includes("shell.exec") || name.includes("shell_exec"))
    return "Running terminal commands";
  if (name.includes("dev_server.start") || name.includes("dev_server_start"))
    return "Starting dev server";
  if (name.includes("dev_server.logs") || name.includes("dev_server_logs"))
    return "Checking server logs";
  if (
    name.includes("browser.navigate") ||
    name.includes("browser.console") ||
    name.includes("browser_navigate") ||
    name.includes("browser_console")
  )
    return "Checking browser preview";
  if (name.includes("ship")) return "Shipping code to production";

  return name;
}

function timeAgo(dateStr?: string): string {
  if (!dateStr) return "";
  const diff = (Date.now() - new Date(dateStr).getTime()) / 1000;
  if (diff < 60) return "just now";
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
  return `${Math.floor(diff / 86400)}d ago`;
}

function friendlyToolName(name: string): string {
  return name
    .replace(/_/g, ".")
    .replace("projects.list", "listing projects")
    .replace("apps.list", "listing apps")
    .replace("apps.create", "deploying app")
    .replace("apps.templates.list", "listing templates")
    .replace("apps.templates.search", "searching templates")
    .replace("domains.register", "registering domain")
    .replace("domains.list", "listing domains")
    .replace("apps.logs", "fetching logs");
}

// ── Markdown-lite renderer ────────────────────────────────────────────────────

function escapeHtmlAttr(s: string): string {
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
}

const LINK_STYLE =
  "color:#4338ca;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere;word-break:break-all";

/** [label](https://...) — href restricted to http(s) */
function markdownLinksToHtml(s: string): string {
  return s.replace(
    /\[([^\]]+)\]\((https?:\/\/[^\s)<>]+)\)/gi,
    (_m, label: string, url: string) => {
      return `<a href="${escapeHtmlAttr(url)}" target="_blank" rel="noopener noreferrer" style="${LINK_STYLE}">${label}</a>`;
    },
  );
}

/** Bare https:// in prose (skips when prefix is `>` so href=/code aren't touched) */
function autoLinkBareUrls(s: string): string {
  return s.replace(
    /(^|[\s\-—:(\[{])(https?:\/\/[^\s<>"']+)/gi,
    (match, pre: string, url: string) =>
      `${pre}<a href="${escapeHtmlAttr(url)}" target="_blank" rel="noopener noreferrer" style="${LINK_STYLE}">${url}</a>`,
  );
}

function renderMarkdown(text: string): string {
  const codeBlocks: string[] = [];
  let s = text;

  // Extract triple backtick code blocks first to protect them from other formatting
  s = s.replace(/```(\w*)[\s\n]([\s\S]*?)```/g, (match, lang, code) => {
    const id = `___CODE_BLOCK_${codeBlocks.length}___`;
    const escapedCode = code
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
    const languageLabel = lang ? lang.toUpperCase() : "CODE";

    const containerStyle = `
      margin: 12px 0;
      border: 1px solid #e8e4dc;
      border-radius: 8px;
      overflow: hidden;
      background: #faf8f5;
      box-shadow: 0 1px 3px rgba(1a,1a,1a,0.02);
      font-family: var(--font-ibm-plex-mono), SFMono-Regular, Consolas, monospace;
    `
      .trim()
      .replace(/\s+/g, " ");

    const headerStyle = `
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 6px 12px;
      background: #f0ede8;
      border-bottom: 1px solid #e8e4dc;
      font-size: 0.68rem;
      font-weight: 600;
      color: #6b6560;
    `
      .trim()
      .replace(/\s+/g, " ");

    const preStyle = `
      display: block;
      padding: 12px;
      margin: 0;
      overflow-x: auto;
      font-size: 0.78rem;
      line-height: 1.55;
      color: #1a1a1a;
      white-space: pre;
    `
      .trim()
      .replace(/\s+/g, " ");

    const buttonStyle = `
      background: #ffffff;
      border: 1px solid #e8e4dc;
      border-radius: 4px;
      padding: 2px 6px;
      font-size: 0.65rem;
      cursor: pointer;
      color: #6b6560;
      display: flex;
      align-items: center;
      gap: 4px;
      font-family: var(--font-inter), ui-sans-serif, sans-serif;
      transition: all 0.1s ease;
    `
      .trim()
      .replace(/\s+/g, " ");

    const copyId = `btn-copy-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
    const textId = `txt-code-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;

    const copyScript = `
      navigator.clipboard.writeText(document.getElementById('${textId}').innerText)
        .then(() => {
          const btn = document.getElementById('${copyId}');
          btn.innerHTML = 'Copied ✓';
          setTimeout(() => { btn.innerHTML = 'Copy'; }, 1500);
        });
    `
      .trim()
      .replace(/\s+/g, " ");

    const blockHtml = `
      <div style="${containerStyle}">
        <div style="${headerStyle}">
          <span>${languageLabel}</span>
          <button id="${copyId}" onclick="${copyScript}" style="${buttonStyle}">Copy</button>
        </div>
        <pre id="${textId}" style="${preStyle}"><code>${escapedCode}</code></pre>
      </div>
    `
      .trim()
      .replace(/\s*\n\s*/g, "");

    codeBlocks.push(blockHtml);
    return id;
  });

  // Safe escape of remaining content
  s = s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

  s = markdownLinksToHtml(s);

  s = s
    .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
    .replace(
      /`([^`]+)`/g,
      '<code style="background:#f0ede8;padding:1px 5px;border-radius:3px;font-family:var(--font-ibm-plex-mono),monospace;font-size:0.85em;overflow-wrap:anywhere;word-break:break-word">$1</code>',
    )
    .replace(
      /^### (.+)$/gm,
      '<h3 style="font-weight:600;margin:12px 0 4px;font-size:0.88rem;overflow-wrap:anywhere;word-break:break-word">$1</h3>',
    )
    .replace(
      /^## (.+)$/gm,
      '<h2 style="font-weight:600;margin:14px 0 4px;font-size:0.9rem;overflow-wrap:anywhere;word-break:break-word">$1</h2>',
    )
    .replace(
      /^- (.+)$/gm,
      '<li style="margin-left:16px;list-style:disc;overflow-wrap:anywhere;word-break:break-word">$1</li>',
    )
    .replace(
      /(<li[^>]*>.*<\/li>\n?)+/g,
      (m) => `<ul style="margin:6px 0">${m}</ul>`,
    )
    .replace(
      /\n\n/g,
      '</p><p style="margin:0 0 8px;overflow-wrap:anywhere;word-break:break-word">',
    )
    .replace(/\n/g, "<br>");

  s = autoLinkBareUrls(s);

  // Restore the formatted code blocks
  codeBlocks.forEach((html, index) => {
    s = s.replace(`___CODE_BLOCK_${index}___`, html);
  });

  return s;
}

// ── Message bubble ────────────────────────────────────────────────────────────

function ThinkingBubble({ thoughts }: { thoughts: string }) {
  const [expanded, setExpanded] = useState(false);
  if (!thoughts) return null;

  // Split thoughts into phrases, take the last one as the "current" action
  const lines = thoughts
    .split(/[.!?\n]/)
    .map((l) => l.trim())
    .filter(Boolean);
  const currentAction = lines[lines.length - 1];

  if (!currentAction) return null;

  return (
    <div
      style={{
        margin: "4px 0",
        background: "#faf8f5",
        border: "1px dashed #e8e4dc",
        borderRadius: 8,
        fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
      }}
    >
      <button
        type="button"
        onClick={() => setExpanded(!expanded)}
        style={{
          display: "flex",
          alignItems: "center",
          width: "100%",
          gap: 8,
          padding: "6px 12px",
          background: "none",
          border: "none",
          fontSize: "0.75rem",
          color: "#8c8580",
          cursor: "pointer",
          textAlign: "left",
        }}
      >
        <span style={{ width: 14, display: "flex", justifyContent: "center" }}>
          <Sparkles
            style={{ width: 12, height: 12, color: "#d4a04a" }}
            className="animate-pulse"
          />
        </span>
        <span style={{ flex: 1, fontStyle: "italic" }}>
          {expanded ? "Thinking Process" : `${currentAction}...`}
        </span>
        <span
          style={{
            transform: expanded ? "rotate(180deg)" : "none",
            transition: "transform 0.15s ease",
            opacity: 0.5,
          }}
        >
          <ChevronDown style={{ width: 12, height: 12 }} />
        </span>
      </button>

      {expanded && (
        <div
          style={{
            padding: "0 12px 10px 34px",
            fontSize: "0.74rem",
            color: "#6b6560",
            lineHeight: 1.55,
            whiteSpace: "pre-wrap",
            borderTop: "1px solid #f0ede8",
            marginTop: 4,
            paddingTop: 8,
          }}
        >
          {thoughts}
        </div>
      )}
    </div>
  );
}

function MessageBubble({ msg }: { msg: Message }) {
  const isUser = msg.role === "user";
  const proseWrap: React.CSSProperties = {
    overflowWrap: "anywhere",
    wordBreak: "break-word",
    minWidth: 0,
  };
  return (
    <div
      style={{
        display: "flex",
        justifyContent: isUser ? "flex-end" : "flex-start",
        marginBottom: 12,
        minWidth: 0,
      }}
    >
      {!isUser && (
        <div
          style={{
            width: 24,
            height: 24,
            borderRadius: "50%",
            background: "#1a1a1a",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            marginRight: 8,
            flexShrink: 0,
            marginTop: 2,
          }}
        >
          <span
            style={{
              color: "#fff",
              fontSize: "0.6rem",
              fontWeight: 700,
              fontFamily: "var(--font-lora),serif",
            }}
          >
            V
          </span>
        </div>
      )}
      <div
        style={{
          maxWidth: "82%",
          minWidth: 0,
          display: "flex",
          flexDirection: "column",
        }}
      >
        {!isUser && msg.timeline && msg.timeline.length > 0 && (
          <Timeline entries={msg.timeline} />
        )}
        {/*
          Render the legacy bottom content bubble ONLY when:
          - the message is from the user (their bubble is always the
            content slot), OR
          - the assistant message has no timeline at all (very old
            messages from before timeline existed).
          When the timeline contains text entries the prose is already
          rendered there, and showing it again here would duplicate
          every paragraph below the timeline.
        */}
        {((msg.content && isUser) ||
          (msg.content &&
            !isUser &&
            (!msg.timeline || msg.timeline.length === 0))) && (
          <div
            style={{
              padding: isUser ? "9px 14px" : "10px 14px",
              borderRadius: isUser
                ? "14px 14px 4px 14px"
                : "4px 14px 14px 14px",
              background: isUser ? "#1a1a1a" : "#f7f4ef",
              color: isUser ? "#fff" : "#1a1a1a",
              fontSize: "0.84rem",
              lineHeight: 1.6,
              fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
              ...proseWrap,
            }}
          >
            {isUser ? (
              <span style={{ whiteSpace: "pre-wrap", ...proseWrap }}>
                {msg.content}
              </span>
            ) : (
              <span
                style={proseWrap}
                dangerouslySetInnerHTML={{
                  __html: renderMarkdown(msg.content),
                }}
              />
            )}
          </div>
        )}
      </div>
    </div>
  );
}

/**
 * Renders the chronological turn timeline: thoughts as their own
 * collapsed pills, tool calls grouped by adjacent runs of the same
 * name with a ×N counter. The flow visually mirrors what actually
 * happened: thought → tools → thought → tools → ... → final summary.
 */
function Timeline({ entries }: { entries: TimelineEntry[] }) {
  // Walk the entries and emit a renderable list. Adjacent same-category
  // tool entries get bundled into a TimelineToolGroup; thought and
  // text entries pass through as-is.
  type Item =
    | { kind: "thought"; text: string }
    | { kind: "text"; text: string }
    | {
        kind: "toolGroup";
        category: string;
        entries: Array<Extract<TimelineEntry, { kind: "tool" }>>;
      };
  const items: Item[] = [];
  for (const e of entries) {
    if (e.kind === "thought") {
      items.push({ kind: "thought", text: e.text });
    } else if (e.kind === "text") {
      items.push({ kind: "text", text: e.text });
    } else {
      const last = items[items.length - 1];
      const category = getFriendlyCategory(e.name);
      if (last && last.kind === "toolGroup" && last.category === category) {
        last.entries.push(e);
      } else {
        items.push({ kind: "toolGroup", category, entries: [e] });
      }
    }
  }
  return (
    <div style={{ marginBottom: 6 }}>
      {items.map((item, i) => {
        if (item.kind === "thought") {
          return <ThinkingBubble key={i} thoughts={item.text} />;
        }
        if (item.kind === "text") {
          return <TimelineText key={i} text={item.text} />;
        }
        return (
          <TimelineToolGroup
            key={i}
            category={item.category}
            entries={item.entries}
          />
        );
      })}
    </div>
  );
}

/**
 * One text segment in the assistant's timeline. Rendered as its own
 * bubble so each round of multi-tool-loop output reads as a discrete
 * step instead of concatenating into a wall of text.
 */
function TimelineText({ text }: { text: string }) {
  const proseWrap: React.CSSProperties = {
    overflowWrap: "anywhere",
    wordBreak: "break-word",
    minWidth: 0,
  };
  return (
    <div
      style={{
        padding: "10px 14px",
        borderRadius: "4px 14px 14px 14px",
        background: "#f7f4ef",
        color: "#1a1a1a",
        fontSize: "0.84rem",
        lineHeight: 1.6,
        fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
        marginBottom: 6,
        ...proseWrap,
      }}
    >
      <span
        style={proseWrap}
        dangerouslySetInnerHTML={{ __html: renderMarkdown(text) }}
      />
    </div>
  );
}

function TimelineToolGroup({
  category,
  entries,
}: {
  category: string;
  entries: Array<Extract<TimelineEntry, { kind: "tool" }>>;
}) {
  const [expanded, setExpanded] = useState(false);
  const count = entries.length;
  const allDone = entries.every((e) => e.status === "done");

  return (
    <div
      style={{
        margin: "4px 0",
        background: "#f0ede8",
        borderRadius: 8,
        fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
      }}
    >
      <button
        onClick={() => setExpanded(!expanded)}
        style={{
          display: "flex",
          alignItems: "center",
          width: "100%",
          gap: 8,
          padding: "6px 12px",
          background: "none",
          border: "none",
          fontSize: "0.75rem",
          color: "#6b6560",
          cursor: "pointer",
          textAlign: "left",
        }}
      >
        <span style={{ width: 14, display: "flex", justifyContent: "center" }}>
          {!allDone ? (
            <Loader2
              style={{ width: 12, height: 12 }}
              className="animate-spin"
            />
          ) : (
            <Wrench style={{ width: 12, height: 12, color: "#2e7d32" }} />
          )}
        </span>
        <span style={{ flex: 1 }}>
          {category} {count > 1 ? `(x${count})` : ""} {!allDone ? "..." : " ✓"}
        </span>
        <span
          style={{
            transform: expanded ? "rotate(180deg)" : "none",
            transition: "transform 0.15s ease",
          }}
        >
          <ChevronDown style={{ width: 12, height: 12, opacity: 0.5 }} />
        </span>
      </button>

      {expanded && (
        <div
          style={{
            padding: "0 12px 8px 34px",
            display: "flex",
            flexDirection: "column",
            gap: 4,
          }}
        >
          {entries.map((e, i) => (
            <div
              key={i}
              style={{
                fontSize: "0.7rem",
                color: "#8c8580",
                display: "flex",
                alignItems: "center",
                gap: 6,
              }}
            >
              <div
                style={{
                  width: 4,
                  height: 4,
                  borderRadius: "50%",
                  background: "#ccc",
                }}
              />
              <span style={{ fontFamily: "var(--font-mono), monospace" }}>
                {friendlyToolName(e.name)}
              </span>
              {!e.result && e.status === "running" && (
                <span className="animate-pulse">...</span>
              )}
              {e.result && (
                <span
                  style={{
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                    whiteSpace: "nowrap",
                    opacity: 0.7,
                    maxWidth: 150,
                  }}
                  title={e.result}
                >
                  — {e.result}
                </span>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Main panel ────────────────────────────────────────────────────────────────

interface ChatPanelProps {
  /**
   * When true, the panel renders inline as a flex child of its parent
   * (a structural left column on project pages). Skips the fixed-position
   * slide-out treatment, the collapsed-tab affordance, and the
   * --chat-panel-width side-effect. Always "open" — there's no close
   * button because the panel IS the column.
   *
   * When false / omitted: legacy behavior — fixed slide-out on the
   * right, collapsible, sets --chat-panel-width so the workspace
   * content shifts left to make room.
   */
  structural?: boolean;
  /**
   * When set with `structural` on a project route, renders a unified shell:
   * full-width top bar (chat controls | section icons) and a split row
   * below (chat column | artifact slot). Omit on slide-out chat.
   */
  artifactSlot?: ReactNode;
}

/** Shared dimensions for preview-select + send icon buttons in the composer. */
const COMPOSER_ACTION_BTN_BASE: CSSProperties = {
  flexShrink: 0,
  width: 32,
  height: 32,
  boxSizing: "border-box",
  borderRadius: 8,
  display: "flex",
  alignItems: "center",
  justifyContent: "center",
  padding: 0,
};

/** Preview pick chip + select-mode toggle for unified project shell chat composer. */
function ProjectPreviewChatInputWrap({
  unifiedShell,
  children,
}: {
  unifiedShell: boolean;
  children: (selectToggle: React.ReactNode) => React.ReactNode;
}) {
  const bridge = usePreviewBridge();

  if (!unifiedShell || !bridge) {
    return <>{children(null)}</>;
  }

  const { selectMode, setSelectMode, picked, clearPick } = bridge;

  const chip = picked ? (
    <div
      style={{
        display: "flex",
        alignItems: "flex-start",
        gap: 8,
        marginBottom: 8,
        padding: "8px 10px",
        borderRadius: 8,
        background: "#eef2ff",
        border: "1px solid #c7d2fe",
        fontSize: "0.72rem",
        lineHeight: 1.45,
        color: "#312e81",
      }}
    >
      <MousePointerClick
        style={{
          width: 14,
          height: 14,
          flexShrink: 0,
          marginTop: 2,
          opacity: 0.85,
        }}
        aria-hidden
      />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontWeight: 600, marginBottom: 2 }}>
          Preview selection
        </div>
        <div style={{ opacity: 0.92 }}>
          <span
            style={{
              fontFamily: "var(--font-ibm-plex-mono),monospace",
              fontSize: "0.68rem",
            }}
          >
            {picked.tagName}
          </span>
          {" · "}
          <span style={{ wordBreak: "break-word" }}>{picked.selector}</span>
        </div>
        {picked.textSnippet ? (
          <div style={{ marginTop: 4, opacity: 0.88, wordBreak: "break-word" }}>
            {`"${picked.textSnippet.slice(0, 140)}${picked.textSnippet.length > 140 ? "..." : ""}"`}
          </div>
        ) : null}
      </div>
      <button
        type="button"
        onClick={clearPick}
        aria-label="Clear preview selection"
        style={{
          flexShrink: 0,
          background: "none",
          border: "none",
          padding: 2,
          cursor: "pointer",
          borderRadius: 4,
          color: "#4338ca",
          display: "flex",
          alignItems: "center",
        }}
      >
        <X style={{ width: 13, height: 13 }} />
      </button>
    </div>
  ) : null;

  const selectToggle = (
    <button
      type="button"
      aria-pressed={selectMode}
      aria-label={
        selectMode ? "Exit preview select mode" : "Select element from preview"
      }
      title={
        selectMode
          ? "Click preview to choose an element"
          : "Choose element from preview"
      }
      onClick={() => setSelectMode(!selectMode)}
      style={{
        ...COMPOSER_ACTION_BTN_BASE,
        border: selectMode ? "1px solid #6366f1" : "1px solid #e8e4dc",
        background: selectMode ? "#eef2ff" : "#faf8f5",
        color: selectMode ? "#4338ca" : "#6b6560",
        cursor: "pointer",
      }}
    >
      <MousePointerClick style={{ width: 15, height: 15 }} strokeWidth={2} />
    </button>
  );

  return (
    <>
      {chip}
      {children(selectToggle)}
    </>
  );
}

export function ChatPanel({
  structural = false,
  artifactSlot,
}: ChatPanelProps = {}) {
  const { status } = useSession();
  const params = useParams();
  const pathname = usePathname() ?? "";
  const workspace = (params?.workspace as string) || "";
  // When the user is on a /project/<id>/* route, scope the chat to
  // that project. The threads list, the new-thread create call, and
  // the system prompt all branch on this; the chat header surfaces it
  // so the user knows the AI is "talking about" the right thing.
  const projectId = (params?.projectId as string) || "";
  /** Full project shell (chat | artifact); must render even while auth is loading or signed out. */
  const unifiedProjectShell =
    structural && Boolean(projectId) && artifactSlot !== undefined;
  const [activeProjectName, setActiveProjectName] = useState<string | null>(
    null,
  );

  const [open, setOpen] = useState(() => {
    // Structural mode is always-open by definition — the panel IS the
    // column, there's no "closed" state to persist.
    if (structural) return true;
    if (typeof window === "undefined") return false;
    return localStorage.getItem("vibn-chat-open") !== "false";
  });
  const [threads, setThreads] = useState<Thread[]>([]);
  // threadsLoaded flips to true after the FIRST loadThreads() resolves.
  // Used to gate the auto-create effect — without it we race the fetch
  // and spawn an empty thread before history loads.
  const [threadsLoaded, setThreadsLoaded] = useState(false);
  const [activeThread, setActiveThread] = useState<string | null>(null);
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [chatMode, setChatMode] = useState<"collaborate" | "vibe" | "delegate">(
    "vibe",
  );
  const [projectFiles, setProjectFiles] = useState<string[]>([]);
  const [attachedFiles, setAttachedFiles] = useState<string[]>([]);
  const [showSuggestions, setShowSuggestions] = useState(false);
  const [suggestionsFilter, setSuggestionsFilter] = useState("");
  const [suggestionIndex, setSuggestionIndex] = useState(0);

  // Fetch codebase files list inside the scoped project
  useEffect(() => {
    if (!projectId || !workspace || status !== "authenticated") return;
    fetch(`/api/projects/${projectId}/files?workspace=${workspace}`)
      .then((res) => res.json())
      .then((data) => {
        if (Array.isArray(data.files)) {
          setProjectFiles(data.files);
        }
      })
      .catch(() => {});
  }, [projectId, workspace, status]);
  const [sending, setSending] = useState(false);
  const [showThreads, setShowThreads] = useState(false);
  const [mcpToken, setMcpToken] = useState<string | null>(null);
  const messagesEndRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLTextAreaElement>(null);
  // AbortController for the in-flight /api/chat fetch. Lives in a ref
  // so the Stop button can reach it without re-rendering on every
  // streaming chunk.
  const abortRef = useRef<AbortController | null>(null);

  const scrollToBottom = useCallback(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, []);

  // Persist open state + adjust main content margin
  useEffect(() => {
    if (structural) return;
    localStorage.setItem("vibn-chat-open", String(open));
    document.documentElement.style.setProperty(
      "--chat-panel-width",
      open ? "380px" : "0px",
    );
  }, [open, structural]);

  // Load MCP token — prefer localStorage cache, fetch from API if missing.
  // We use /api/workspaces (not the URL param) because the URL slug
  // (e.g. "mark-account") differs from the actual workspace slug ("mark").
  useEffect(() => {
    if (!workspace || status !== "authenticated") return;
    const cached = localStorage.getItem(`vibn-mcp-token-${workspace}`);
    if (cached) {
      setMcpToken(cached);
      return;
    }
    fetch("/api/workspaces?include_default_token=true")
      .then((r) => (r.ok ? r.json() : null))
      .then((d) => {
        if (d?.defaultToken) {
          localStorage.setItem(`vibn-mcp-token-${workspace}`, d.defaultToken);
          setMcpToken(d.defaultToken);
        }
      })
      .catch(() => {});
  }, [workspace, status]);

  // Load threads (scoped to the current project when one is in the URL).
  // Reset the loaded flag when projectId changes so the resume effect
  // re-runs against the correct list and doesn't restore a thread from
  // the previous project.
  const loadThreads = useCallback(async () => {
    if (!workspace || status !== "authenticated") return;
    try {
      const qs = new URLSearchParams({ workspace });
      if (projectId) qs.set("projectId", projectId);
      const res = await fetch(`/api/chat/threads?${qs.toString()}`);
      const data = await res.json();
      setThreads(data.threads || []);
    } catch {
      /* silent */
    } finally {
      setThreadsLoaded(true);
    }
  }, [workspace, projectId, status]);

  useEffect(() => {
    setThreadsLoaded(false);
    setActiveThread(null);
    setMessages([]);
    // Clear the threads array immediately so the resume effect doesn't
    // race the loadThreads() fetch and resume a stale project-scoped
    // thread when the user navigates from /project/X back to /projects.
    setThreads([]);
    loadThreads();
  }, [loadThreads, projectId]);

  // Look up the active project's display name once we have a projectId,
  // so the chat header can show "Talking about: <name>".
  useEffect(() => {
    if (!projectId) {
      setActiveProjectName(null);
      return;
    }
    let cancelled = false;
    fetch(`/api/projects/${projectId}/anatomy`, { credentials: "include" })
      .then((r) => (r.ok ? r.json() : null))
      .then((d) => {
        if (cancelled) return;
        const name = d?.project?.name;
        if (name) setActiveProjectName(name);
      })
      .catch(() => {});
    return () => {
      cancelled = true;
    };
  }, [projectId]);

  // Create and activate a new thread (tagged to the active project, if any).
  const newThread = useCallback(async () => {
    try {
      const res = await fetch("/api/chat/threads", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ workspace, projectId: projectId || undefined }),
      });
      const data = await res.json();
      if (data.thread) {
        setThreads((prev) => [data.thread, ...prev]);
        setActiveThread(data.thread.id);
        setMessages([]);
        setShowThreads(false);
      }
    } catch {
      /* silent */
    }
  }, [workspace, projectId]);

  // Load thread messages
  const loadThread = useCallback(async (id: string) => {
    setActiveThread(id);
    setShowThreads(false);
    setMessages([]);
    try {
      const res = await fetch(`/api/chat/threads/${id}`);
      const data = await res.json();
      // Hydrate the timeline from persisted textSegments + toolCalls
      // so a reloaded thread renders the same per-round bubbles the
      // user saw during streaming. Older messages without
      // textSegments fall back to the legacy single-bubble path.
      const hydrated = (data.messages || []).map(
        (m: {
          role: string;
          textSegments?: string[];
          toolCalls?: Array<{
            name: string;
            args: Record<string, unknown>;
            result?: string;
          }>;
        }) => {
          if (m.role !== "assistant") return m as unknown as Message;
          const segs: string[] = Array.isArray(m.textSegments)
            ? m.textSegments
            : [];
          if (segs.length === 0) return m as unknown as Message;
          const timeline: TimelineEntry[] = segs.map((t) => ({
            kind: "text",
            text: t,
          }));
          // We don't have round-level interleaving for tool calls in
          // the persisted shape (the schema flattens them), so we drop
          // the toolCalls into the timeline at the end. The streamed
          // shape preserves true ordering; this is just a reload
          // approximation. Good enough — what the user really cares
          // about is the text segments not run-on'ing into one blob.
          if (Array.isArray(m.toolCalls)) {
            for (const tc of m.toolCalls) {
              timeline.push({ kind: "tool", name: tc.name, status: "done" });
            }
          }
          return { ...m, timeline, content: "" } as unknown as Message;
        },
      );
      setMessages(hydrated);
    } catch {
      /* silent */
    }
  }, []);

  // Auto-resume previous thread (or create a fresh one if the user has
  // never chatted in this workspace). We MUST wait for threadsLoaded
  // before deciding — otherwise we race the fetch and spawn an empty
  // thread before history arrives. Last-active thread is restored from
  // localStorage so a page reload (deploy, refresh) lands the user back
  // in the conversation they were in.
  useEffect(() => {
    if (!open || status !== "authenticated" || !workspace) return;
    if (!threadsLoaded) return;
    if (activeThread) return;

    if (threads.length === 0) {
      newThread();
      return;
    }

    const scopeKey = projectId ? `${workspace}:${projectId}` : workspace;
    const savedKey = `vibn-chat-active-thread:${scopeKey}`;
    const saved =
      typeof window !== "undefined" ? localStorage.getItem(savedKey) : null;
    const target =
      saved && threads.some((t) => t.id === saved) ? saved : threads[0].id;
    loadThread(target);
  }, [
    open,
    status,
    workspace,
    projectId,
    threadsLoaded,
    threads,
    activeThread,
    newThread,
    loadThread,
  ]);

  // Persist active thread so reload re-opens the same conversation,
  // keyed per-project so each project has its own "last conversation".
  useEffect(() => {
    if (typeof window === "undefined" || !workspace) return;
    const scopeKey = projectId ? `${workspace}:${projectId}` : workspace;
    const savedKey = `vibn-chat-active-thread:${scopeKey}`;
    if (activeThread) localStorage.setItem(savedKey, activeThread);
  }, [activeThread, workspace, projectId]);

  useEffect(() => {
    scrollToBottom();
  }, [messages, scrollToBottom]);

  const deleteThread = useCallback(
    async (id: string, e: React.MouseEvent) => {
      e.stopPropagation();
      await fetch(`/api/chat/threads/${id}`, { method: "DELETE" });
      setThreads((prev) => prev.filter((t) => t.id !== id));
      if (activeThread === id) {
        setActiveThread(null);
        setMessages([]);
      }
      loadThreads();
    },
    [activeThread, loadThreads],
  );

  const sendMessage = useCallback(
    async (override?: string) => {
      let raw = (override ?? input).trim();
      if (!raw || sending || !activeThread) return;
      if (unifiedProjectShell && previewMessagePrepRef.current) {
        raw = previewMessagePrepRef.current(raw);
      }
      const text = raw;
      if (!override) setInput("");
      setSending(true);

      const userMsg: Message = { role: "user", content: text };
      setMessages((prev) => [...prev, userMsg]);

      let assistantContent = "";
      const assistantMsg: Message = { role: "assistant", content: "" };
      let msgIndex = -1;

      const controller = new AbortController();
      abortRef.current = controller;

      try {
        // If Delegate mode is selected, route to the background runner instead of streaming chat!
        if (chatMode === "delegate") {
          const r = await fetch(`/api/projects/${projectId}/agent/sessions`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              appName: "frontend",
              appPath: ".",
              task: text,
            }),
          });
          if (!r.ok) {
            const err = await r.json().catch(() => ({}));
            throw new Error(err.error || `HTTP ${r.status}`);
          }

          setAttachedFiles([]);
          setMessages((prev) => [
            ...prev,
            {
              role: "assistant",
              content:
                "I have started a background runner for this task. You can safely close this browser or work on something else. I will commit and ship the code when I am finished!",
            },
          ]);
          setSending(false);
          return;
        }

        const res = await fetch("/api/chat", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            thread_id: activeThread,
            message: text,
            workspace,
            mcp_token: mcpToken,
            chatMode,
            attachedFiles,
          }),
          signal: controller.signal,
        });

        setAttachedFiles([]);

        if (!res.ok || !res.body) throw new Error("Stream failed");

        const reader = res.body.getReader();
        const decoder = new TextDecoder();
        let buf = "";

        setMessages((prev) => {
          msgIndex = prev.length;
          return [...prev, { ...assistantMsg }];
        });

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          buf += decoder.decode(value, { stream: true });

          const lines = buf.split("\n");
          buf = lines.pop() ?? "";

          for (const line of lines) {
            if (!line.startsWith("data: ")) continue;
            let ev: {
              type: string;
              text?: string;
              name?: string;
              result?: string;
              error?: string;
            };
            try {
              ev = JSON.parse(line.slice(6));
            } catch {
              continue;
            }

            if (ev.type === "text" && ev.text) {
              // Each text SSE event = one round of the model's text
              // output. Push a new "text" timeline entry so the
              // renderer can show multi-round turns as separate
              // bubbles instead of one run-on paragraph. We still
              // maintain `assistantContent` (joined with blank lines)
              // so the legacy single-bubble fallback path and any
              // post-stream consumers still work.
              assistantContent += (assistantContent ? "\n\n" : "") + ev.text;
              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = next[msgIndex].timeline ?? [];
                  next[msgIndex] = {
                    ...next[msgIndex],
                    // Don't write to msg.content during streaming —
                    // the timeline is the source of truth. Setting
                    // content on every text event re-renders one
                    // giant bubble in the bottom slot AND the
                    // segmented timeline above it, duplicating the
                    // same prose. Persisted messages pick up
                    // content via the final flush below.
                    timeline: [...tl, { kind: "text", text: ev.text }],
                  };
                }
                return next;
              });
            } else if (ev.type === "thinking" && ev.text) {
              // Each thinking event from the server is one round of the
              // model's reasoning. Push as a separate timeline entry so
              // the renderer can show it as its own collapsed pill —
              // 12 rounds become 12 small pills the user can each
              // expand independently, not one giant blob.
              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = next[msgIndex].timeline ?? [];
                  next[msgIndex] = {
                    ...next[msgIndex],
                    timeline: [...tl, { kind: "thought", text: ev.text }],
                  };
                }
                return next;
              });
            } else if (ev.type === "tool_start") {
              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = next[msgIndex].timeline ?? [];
                  next[msgIndex] = {
                    ...next[msgIndex],
                    timeline: [
                      ...tl,
                      { kind: "tool", name: ev.name, status: "running" },
                    ],
                  };
                }
                return next;
              });
            } else if (ev.type === "tool_result") {
              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = next[msgIndex].timeline ?? [];
                  // Walk backward to the most recent matching running
                  // tool entry and mark it done. Avoids cross-matching
                  // earlier same-named entries.
                  let updated = false;
                  const newTl: TimelineEntry[] = [];
                  for (let i = tl.length - 1; i >= 0; i--) {
                    const e = tl[i];
                    if (
                      !updated &&
                      e.kind === "tool" &&
                      e.name === ev.name &&
                      e.status === "running"
                    ) {
                      newTl.unshift({
                        ...e,
                        status: "done",
                        result: ev.result,
                      });
                      updated = true;
                    } else {
                      newTl.unshift(e);
                    }
                  }
                  next[msgIndex] = { ...next[msgIndex], timeline: newTl };
                }
                return next;
              });
            } else if (ev.type === "error") {
              const errText = ev.error || "Unknown error";
              const isToolErr = /tool|mcp|coolify|gitea/i.test(errText);
              const errBubble = isToolErr
                ? `⚠️ **Tool error:** ${errText}`
                : `⚠️ ${errText}`;
              assistantContent += (assistantContent ? "\n\n" : "") + errBubble;
              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = next[msgIndex].timeline ?? [];
                  next[msgIndex] = {
                    ...next[msgIndex],
                    timeline: [...tl, { kind: "text", text: errBubble }],
                  };
                }
                return next;
              });
            }
          }
        }

        // Auto-title thread from first message
        const thisThread = threads.find((t) => t.id === activeThread);
        if (thisThread?.title === "New conversation") {
          const title = text.slice(0, 50);
          await fetch(`/api/chat/threads/${activeThread}`, {
            method: "PATCH",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ title }),
          });
          setThreads((prev) =>
            prev.map((t) => (t.id === activeThread ? { ...t, title } : t)),
          );
        }

        loadThreads();
      } catch (e) {
        const isAbort = e instanceof DOMException && e.name === "AbortError";
        if (isAbort) {
          // Server-side will have appended "(stopped by user)" to the
          // partial response and persisted it. We just need to make
          // sure the local UI reflects whatever streamed in before the
          // user clicked Stop — which it already does, because we've
          // been mutating `messages[msgIndex]` chunk-by-chunk above.
          setMessages((prev) => {
            const next = [...prev];
            if (
              msgIndex >= 0 &&
              next[msgIndex] &&
              !next[msgIndex].content.includes("(stopped by user)")
            ) {
              next[msgIndex] = {
                ...next[msgIndex],
                content:
                  (next[msgIndex].content || "") + "\n\n_(stopped by user)_",
              };
            }
            return next;
          });
        } else {
          const errMsg = e instanceof Error ? e.message : String(e);
          const isNetwork = /fetch|network|failed to fetch/i.test(errMsg);
          const friendlyError = isNetwork
            ? "⚠️ Network error — check your connection and try again."
            : `⚠️ Something went wrong: ${errMsg.slice(0, 200)}\n\nYou can try again or start a new message.`;
          setMessages((prev) => {
            const next = [...prev];
            if (msgIndex >= 0 && next[msgIndex]) {
              next[msgIndex] = { ...next[msgIndex], content: friendlyError };
            }
            return next;
          });
        }
      } finally {
        abortRef.current = null;
        setSending(false);
      }
    },
    [
      input,
      sending,
      activeThread,
      workspace,
      mcpToken,
      threads,
      loadThreads,
      unifiedProjectShell,
      chatMode,
      projectId,
      attachedFiles,
    ],
  );

  const cancelMessage = useCallback(() => {
    abortRef.current?.abort();
  }, []);

  // External components (e.g. ProjectHeaderUrls' "Start preview" button)
  // can ask the chat to send a canned prompt without prop-drilling. Open
  // the panel if collapsed, then fire the prompt as if the user typed it.
  useEffect(() => {
    function onPrompt(e: Event) {
      const ce = e as CustomEvent<{ prompt?: string; scopeProjectId?: string }>;
      const prompt = ce.detail?.prompt;
      if (!prompt) return;
      // If the dispatcher scopes the prompt to a specific project, only
      // accept it when the chat panel is currently bound to that project.
      // Prevents a "Start preview on Manifest" click from accidentally
      // landing in a chat that's scoped to a different project.
      if (ce.detail?.scopeProjectId && ce.detail.scopeProjectId !== projectId) {
        return;
      }
      setOpen(true);
      void sendMessage(prompt);
    }
    window.addEventListener("vibn:chat-prompt", onPrompt as EventListener);
    return () =>
      window.removeEventListener("vibn:chat-prompt", onPrompt as EventListener);
  }, [sendMessage, projectId]);

  const handleInputChange = (val: string) => {
    setInput(val);
    const match = val.match(/\/(\S*)$/);
    if (match) {
      setShowSuggestions(true);
      setSuggestionsFilter(match[1] || "");
      setSuggestionIndex(0);
    } else {
      setShowSuggestions(false);
    }
  };

  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (showSuggestions) {
      const filtered = projectFiles
        .filter((f) =>
          f.toLowerCase().includes(suggestionsFilter.toLowerCase()),
        )
        .slice(0, 8);

      if (e.key === "ArrowDown") {
        e.preventDefault();
        setSuggestionIndex((prev) => (prev + 1) % Math.max(1, filtered.length));
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        setSuggestionIndex(
          (prev) => (prev - 1 + filtered.length) % Math.max(1, filtered.length),
        );
      } else if (e.key === "Enter" || e.key === "Tab") {
        e.preventDefault();
        if (filtered[suggestionIndex]) {
          const selectedFile = filtered[suggestionIndex];
          const before = input.slice(0, input.lastIndexOf("/"));
          setInput(before + " ");
          if (!attachedFiles.includes(selectedFile)) {
            setAttachedFiles((prev) => [...prev, selectedFile]);
          }
          setShowSuggestions(false);
        }
      } else if (e.key === "Escape") {
        e.preventDefault();
        setShowSuggestions(false);
      }
    } else {
      if (e.key === "Enter" && !e.shiftKey) {
        e.preventDefault();
        sendMessage();
      } else if (e.key === "Escape" && sending) {
        e.preventDefault();
        cancelMessage();
      }
    }
  };

  // Slide-out chat hidden until signed in. Structural project shell always
  // mounts so Preview / Product / Plan pages render; chat column shows loading
  // or sign-in instead of wiping the whole viewport (blank page).
  if (!unifiedProjectShell && status !== "authenticated") return null;

  // ── Collapsed tab ──────────────────────────────────────────────────────────
  // Structural mode is always-open; skip the collapsed-tab branch entirely.
  if (!open && !structural) {
    return (
      <button
        onClick={() => setOpen(true)}
        style={{
          position: "fixed",
          right: 0,
          top: "50%",
          transform: "translateY(-50%)",
          zIndex: 1000,
          background: "#1a1a1a",
          color: "#fff",
          border: "none",
          borderRadius: "8px 0 0 8px",
          padding: "14px 10px",
          cursor: "pointer",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 6,
          boxShadow: "-2px 0 12px #1a1a1a14",
        }}
        title="Open Vibn AI"
      >
        <MessageSquare style={{ width: 16, height: 16 }} />
        <span
          style={{
            writingMode: "vertical-rl",
            textOrientation: "mixed",
            fontSize: "0.65rem",
            fontWeight: 600,
            letterSpacing: "0.08em",
            fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
            transform: "rotate(180deg)",
          }}
        >
          VIBN AI
        </span>
      </button>
    );
  }

  const conversationColumn = (
    <>
      {/* Thread list dropdown */}
      {showThreads && (
        <div
          style={{
            borderBottom: "1px solid #e8e4dc",
            background: "#faf8f5",
            maxHeight: 200,
            overflowY: "auto",
            flexShrink: 0,
          }}
        >
          {threads.length === 0 && (
            <div
              style={{
                padding: "12px 16px",
                fontSize: "0.78rem",
                color: "#a09a90",
              }}
            >
              No conversations yet
            </div>
          )}
          {threads.map((t) => (
            <div
              key={t.id}
              onClick={() => loadThread(t.id)}
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                padding: "9px 16px",
                background: activeThread === t.id ? "#f0ede8" : "transparent",
                cursor: "pointer",
                borderBottom: "1px solid #f0ede8",
              }}
              onMouseEnter={(e) => {
                if (activeThread !== t.id)
                  e.currentTarget.style.background = "#f7f4ef";
              }}
              onMouseLeave={(e) => {
                if (activeThread !== t.id)
                  e.currentTarget.style.background = "transparent";
              }}
            >
              <div style={{ minWidth: 0 }}>
                <div
                  style={{
                    fontSize: "0.8rem",
                    fontWeight: 500,
                    color: "#1a1a1a",
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                    whiteSpace: "nowrap",
                  }}
                >
                  {t.title}
                </div>
                <div style={{ fontSize: "0.7rem", color: "#a09a90" }}>
                  {timeAgo(t.updatedAt)}
                </div>
              </div>
              <button
                onClick={(e) => deleteThread(t.id, e)}
                style={{
                  background: "none",
                  border: "none",
                  cursor: "pointer",
                  padding: "2px 4px",
                  color: "#c0bab2",
                  flexShrink: 0,
                }}
              >
                <Trash2 style={{ width: 12, height: 12 }} />
              </button>
            </div>
          ))}
        </div>
      )}

      {/* Messages */}
      <div
        style={{
          flex: 1,
          minWidth: 0,
          overflowY: "auto",
          overflowX: "hidden",
          padding: "16px 14px",
        }}
      >
        {messages.length === 0 && !sending && (
          <div
            style={{ padding: "24px 14px", maxWidth: 640, margin: "0 auto" }}
          >
            <div style={{ display: "flex", gap: 12 }}>
              <div
                style={{
                  width: 28,
                  height: 28,
                  borderRadius: 6,
                  background: "#1a1a1a",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  flexShrink: 0,
                  color: "#fff",
                  fontSize: "0.85rem",
                  fontFamily: "var(--font-lora),serif",
                }}
              >
                V
              </div>
              <div
                style={{
                  background: "#fff",
                  border: "1px solid #e8e4dc",
                  padding: "14px 18px",
                  borderRadius: "0 12px 12px 12px",
                  fontSize: "0.9rem",
                  color: "#1a1a1a",
                  lineHeight: 1.6,
                  boxShadow: "0 1px 2px #1a1a1a05",
                  maxWidth: "85%",
                }}
              >
                Welcome to {activeProjectName ? activeProjectName : "Vibn"}!
                Tell me what you want to build and I&apos;ll scaffold it, run it
                in a preview, and ship it when you say so.
              </div>
            </div>
          </div>
        )}

        {messages.map((msg, i) => (
          <MessageBubble key={msg.id || i} msg={msg} />
        ))}

        {sending && messages[messages.length - 1]?.role !== "assistant" && (
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 8,
              padding: "6px 0",
            }}
          >
            <div
              style={{
                width: 24,
                height: 24,
                borderRadius: "50%",
                background: "#1a1a1a",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                flexShrink: 0,
              }}
            >
              <span
                style={{
                  color: "#fff",
                  fontSize: "0.6rem",
                  fontWeight: 700,
                  fontFamily: "var(--font-lora),serif",
                }}
              >
                V
              </span>
            </div>
            <div style={{ display: "flex", gap: 4 }}>
              {[0, 1, 2].map((i) => (
                <span
                  key={i}
                  style={{
                    width: 6,
                    height: 6,
                    borderRadius: "50%",
                    background: "#c0bab2",
                    animation: `vibn-bounce 1.2s ease infinite ${i * 0.2}s`,
                    display: "inline-block",
                  }}
                />
              ))}
            </div>
          </div>
        )}

        <div ref={messagesEndRef} />
      </div>

      {/* Input */}
      <div
        style={{
          padding: "12px 14px",
          borderTop: "1px solid #e8e4dc",
          background: "#faf8f5",
          flexShrink: 0,
          position: "relative",
        }}
      >
        {/* Chat Mode Toggle */}
        <div
          style={{
            display: "flex",
            gap: 4,
            marginBottom: 8,
            padding: "2px",
            background: "#f0ede8",
            borderRadius: 8,
            border: "1px solid #e8e4dc",
          }}
        >
          <button
            type="button"
            onClick={() => setChatMode("vibe")}
            style={{
              flex: 1,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              gap: 6,
              padding: "6px 0",
              fontSize: "0.72rem",
              fontWeight: chatMode === "vibe" ? 600 : 500,
              borderRadius: 6,
              border: "none",
              background: chatMode === "vibe" ? "#fff" : "transparent",
              color: chatMode === "vibe" ? "#3d5afe" : "#6b6560",
              cursor: "pointer",
              boxShadow:
                chatMode === "vibe" ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
              transition: "all 0.1s ease",
            }}
            title="Vibe Code: Fast, iterative coding with immediate live previews."
          >
            <Sparkles
              style={{
                width: 12,
                height: 12,
                color: chatMode === "vibe" ? "#3d5afe" : "#8c8580",
              }}
            />
            <span>Vibe Code</span>
          </button>
          <button
            type="button"
            onClick={() => setChatMode("collaborate")}
            style={{
              flex: 1,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              gap: 6,
              padding: "6px 0",
              fontSize: "0.72rem",
              fontWeight: chatMode === "collaborate" ? 600 : 500,
              borderRadius: 6,
              border: "none",
              background: chatMode === "collaborate" ? "#fff" : "transparent",
              color: chatMode === "collaborate" ? "#d4a04a" : "#6b6560",
              cursor: "pointer",
              boxShadow:
                chatMode === "collaborate"
                  ? "0 1px 2px rgba(0,0,0,0.05)"
                  : "none",
              transition: "all 0.1s ease",
            }}
            title="Architect: Brainstorm, spec out features, and plan architecture without writing code."
          >
            <Compass
              style={{
                width: 12,
                height: 12,
                color: chatMode === "collaborate" ? "#d4a04a" : "#8c8580",
              }}
            />
            <span>Architect</span>
          </button>
          <button
            type="button"
            onClick={() => setChatMode("delegate")}
            style={{
              flex: 1,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              gap: 6,
              padding: "6px 0",
              fontSize: "0.72rem",
              fontWeight: chatMode === "delegate" ? 600 : 500,
              borderRadius: 6,
              border: "none",
              background: chatMode === "delegate" ? "#fff" : "transparent",
              color: chatMode === "delegate" ? "#2e7d32" : "#6b6560",
              cursor: "pointer",
              boxShadow:
                chatMode === "delegate" ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
              transition: "all 0.1s ease",
            }}
            title="Delegate Offline: Send this task to the background runner to build automatically while you step away."
          >
            <Cpu
              style={{
                width: 12,
                height: 12,
                color: chatMode === "delegate" ? "#2e7d32" : "#8c8580",
              }}
            />
            <span>Delegate</span>
          </button>
        </div>

        {!mcpToken && (
          <div
            style={{
              fontSize: "0.7rem",
              color: "#9a7b3a",
              background: "#d4a04a12",
              border: "1px solid #d4a04a30",
              borderRadius: 6,
              padding: "5px 10px",
              marginBottom: 8,
              lineHeight: 1.4,
            }}
          >
            Read-only mode — add your MCP token in Settings to enable actions.
          </div>
        )}
        {/* File Autocomplete Suggestions */}
        {showSuggestions &&
          (() => {
            const filtered = projectFiles
              .filter((f) =>
                f.toLowerCase().includes(suggestionsFilter.toLowerCase()),
              )
              .slice(0, 8);
            if (filtered.length === 0) return null;
            return (
              <div
                style={{
                  position: "absolute",
                  bottom: "calc(100% - 10px)",
                  left: 14,
                  right: 14,
                  background: "#fff",
                  border: "1px solid #e8e4dc",
                  borderRadius: 8,
                  boxShadow:
                    "0 -4px 12px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.02)",
                  zIndex: 1000,
                  maxHeight: 200,
                  overflowY: "auto",
                  padding: "4px 0",
                }}
              >
                <div
                  style={{
                    fontSize: "0.65rem",
                    fontWeight: 600,
                    color: "#a09a90",
                    padding: "6px 12px 4px",
                    borderBottom: "1px solid #f0ede8",
                    marginBottom: 4,
                    textTransform: "uppercase",
                    letterSpacing: "0.05em",
                  }}
                >
                  Codebase Files
                </div>
                {filtered.map((file, idx) => (
                  <div
                    key={file}
                    onClick={() => {
                      const before = input.slice(0, input.lastIndexOf("/"));
                      setInput(before + " ");
                      if (!attachedFiles.includes(file)) {
                        setAttachedFiles((prev) => [...prev, file]);
                      }
                      setShowSuggestions(false);
                    }}
                    onMouseEnter={() => setSuggestionIndex(idx)}
                    style={{
                      padding: "6px 12px",
                      fontSize: "0.76rem",
                      fontFamily: "var(--font-mono), monospace",
                      color: idx === suggestionIndex ? "#3d5afe" : "#1a1a1a",
                      background:
                        idx === suggestionIndex ? "#3d5afe0c" : "transparent",
                      cursor: "pointer",
                      display: "flex",
                      justifyContent: "space-between",
                      alignItems: "center",
                    }}
                  >
                    <span
                      style={{
                        overflow: "hidden",
                        textOverflow: "ellipsis",
                        whiteSpace: "nowrap",
                      }}
                    >
                      📄 {file}
                    </span>
                    {idx === suggestionIndex && (
                      <span
                        style={{
                          fontSize: "0.6rem",
                          color: "#3d5afe",
                          opacity: 0.7,
                          fontFamily: "var(--font-inter), sans-serif",
                        }}
                      >
                        press Enter
                      </span>
                    )}
                  </div>
                ))}
              </div>
            );
          })()}

        {/* Attached Files Chips */}
        {attachedFiles.length > 0 && (
          <div
            style={{
              display: "flex",
              flexWrap: "wrap",
              gap: 6,
              marginBottom: 8,
            }}
          >
            {attachedFiles.map((file) => (
              <div
                key={file}
                style={{
                  display: "flex",
                  alignItems: "center",
                  gap: 6,
                  background: "#eef2ff",
                  border: "1px solid #c7d2fe",
                  borderRadius: 6,
                  padding: "4px 8px",
                  fontSize: "0.7rem",
                  color: "#312e81",
                  fontFamily: "var(--font-mono), monospace",
                }}
              >
                <span>📄 {file.split("/").pop()}</span>
                <button
                  type="button"
                  onClick={() =>
                    setAttachedFiles((prev) => prev.filter((f) => f !== file))
                  }
                  style={{
                    background: "none",
                    border: "none",
                    padding: 0,
                    cursor: "pointer",
                    color: "#4338ca",
                    display: "flex",
                    alignItems: "center",
                  }}
                  title="Remove file"
                >
                  <X style={{ width: 12, height: 12 }} />
                </button>
              </div>
            ))}
          </div>
        )}

        <ProjectPreviewChatInputWrap unifiedShell={unifiedProjectShell}>
          {(selectToggle) => (
            <div
              style={{
                display: "flex",
                gap: 8,
                alignItems: "center",
                background: "#fff",
                borderRadius: 10,
                border: "1px solid #e8e4dc",
                padding: "8px 10px",
                boxShadow: "0 1px 3px #1a1a1a05",
              }}
            >
              <textarea
                ref={inputRef}
                value={input}
                onChange={(e) => handleInputChange(e.target.value)}
                onKeyDown={handleKeyDown}
                placeholder={
                  sending ? "Esc to stop generating…" : "Ask Vibn AI anything…"
                }
                rows={1}
                disabled={!activeThread}
                style={{
                  flex: 1,
                  border: "none",
                  outline: "none",
                  background: "transparent",
                  fontSize: "0.84rem",
                  lineHeight: 1.5,
                  resize: "none",
                  fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
                  color: "#1a1a1a",
                  maxHeight: 120,
                  overflowY: "auto",
                }}
                onInput={(e) => {
                  const el = e.currentTarget;
                  el.style.height = "auto";
                  el.style.height = Math.min(el.scrollHeight, 120) + "px";
                }}
              />
              {selectToggle}
              {(() => {
                // While the AI is streaming or running tools, the button
                // turns into a Stop control. Click → AbortController fires,
                // server bails between rounds, partial text gets persisted.
                const isActive = sending;
                const canSend = !sending && input.trim() && activeThread;
                return (
                  <button
                    type="button"
                    onClick={isActive ? cancelMessage : () => sendMessage()}
                    disabled={!isActive && !canSend}
                    aria-label={isActive ? "Stop generating" : "Send message"}
                    title={isActive ? "Stop generating (Esc)" : "Send"}
                    style={{
                      ...COMPOSER_ACTION_BTN_BASE,
                      background: isActive
                        ? "#1a1a1a"
                        : canSend
                          ? "#1a1a1a"
                          : "#e8e4dc",
                      color: isActive || canSend ? "#fff" : "#a09a90",
                      border: "none",
                      cursor: isActive || canSend ? "pointer" : "default",
                      transition: "all 0.15s",
                      position: "relative",
                    }}
                  >
                    {isActive ? (
                      <>
                        <Loader2
                          style={{
                            width: 15,
                            height: 15,
                            position: "absolute",
                            opacity: 0.35,
                          }}
                          className="animate-spin"
                        />
                        <Square
                          style={{
                            width: 9,
                            height: 9,
                            fill: "#fff",
                            strokeWidth: 0,
                          }}
                        />
                      </>
                    ) : (
                      <Send style={{ width: 15, height: 15 }} />
                    )}
                  </button>
                );
              })()}
            </div>
          )}
        </ProjectPreviewChatInputWrap>
      </div>
    </>
  );

  const structuralChatLoadingColumn = (
    <div
      style={{
        flex: 1,
        minHeight: 0,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        padding: 24,
        gap: 14,
      }}
    >
      <Loader2
        className="animate-spin"
        style={{ width: 22, height: 22, color: "#9c9590" }}
      />
      <p style={{ fontSize: "0.84rem", color: "#6b6560", margin: 0 }}>
        Signing you in…
      </p>
    </div>
  );

  const authHref = pathname
    ? `/auth?callbackUrl=${encodeURIComponent(pathname)}`
    : "/auth";

  const structuralChatSignedOutColumn = (
    <div
      style={{
        flex: 1,
        minHeight: 0,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        padding: 24,
        gap: 12,
      }}
    >
      <p
        style={{
          fontSize: "0.88rem",
          fontWeight: 500,
          color: "#1a1a1a",
          margin: 0,
          textAlign: "center",
        }}
      >
        Sign in to use Vibn AI
      </p>
      <p
        style={{
          fontSize: "0.76rem",
          color: "#a09a90",
          margin: 0,
          textAlign: "center",
          maxWidth: 260,
          lineHeight: 1.5,
        }}
      >
        Preview and tabs still work here. Chat needs an account.
      </p>
      <Link
        href={authHref}
        style={{
          marginTop: 4,
          padding: "10px 18px",
          borderRadius: 10,
          background: "#1a1a1a",
          color: "#fff",
          fontSize: "0.82rem",
          fontWeight: 500,
          textDecoration: "none",
        }}
      >
        Sign in
      </Link>
    </div>
  );

  const structuralChatBody =
    status === "loading"
      ? structuralChatLoadingColumn
      : status !== "authenticated"
        ? structuralChatSignedOutColumn
        : conversationColumn;

  if (unifiedProjectShell) {
    return (
      <>
        <PreviewBridgeProvider>
          <div
            style={{
              flex: 1,
              minHeight: 0,
              minWidth: 0,
              display: "flex",
              flexDirection: "column",
              fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
              background: "#fff",
            }}
          >
            <div
              style={{
                display: "flex",
                flexDirection: "row",
                alignItems: "stretch",
                flexShrink: 0,
                height: 48,
                borderBottom: "1px solid #e8e4dc",
                background: "#faf8f5",
                boxSizing: "border-box",
              }}
            >
              <div
                style={{
                  width: 380,
                  flexShrink: 0,
                  height: "100%",
                  display: "flex",
                  alignItems: "center",
                  padding: "0 12px",
                  gap: 6,
                  boxSizing: "border-box",
                  borderRight: "1px solid #e8e4dc",
                }}
              >
                <div
                  style={{
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "space-between",
                    width: "100%",
                    gap: 6,
                  }}
                >
                  <div
                    style={{
                      display: "flex",
                      alignItems: "center",
                      gap: 8,
                      minWidth: 0,
                    }}
                  >
                    {workspace ? (
                      <Link
                        href={`/${workspace}/projects`}
                        title="All projects"
                        style={{ flexShrink: 0, display: "flex" }}
                      >
                        <img
                          src="/vibn-black-circle-logo.png"
                          alt="VIBN"
                          width={26}
                          height={26}
                          style={{
                            borderRadius: 7,
                            objectFit: "cover",
                            display: "block",
                          }}
                        />
                      </Link>
                    ) : null}
                    <button
                      type="button"
                      onClick={() => setShowThreads((v) => !v)}
                      aria-label="Conversations"
                      style={{
                        display: "flex",
                        alignItems: "center",
                        gap: 4,
                        background: "none",
                        border: "none",
                        cursor: "pointer",
                        padding: "2px 4px",
                        borderRadius: 6,
                        minWidth: 0,
                      }}
                    >
                      <ChevronDown
                        style={{
                          width: 12,
                          height: 12,
                          color: "#a09a90",
                          transition: "transform 0.15s",
                          flexShrink: 0,
                          transform: showThreads ? "rotate(180deg)" : "none",
                        }}
                      />
                      {projectId && (
                        <span
                          title={`Chat is scoped to ${activeProjectName ?? "this project"}. Tool calls assume projectId=${projectId}.`}
                          style={{
                            marginLeft: 4,
                            padding: "1px 6px",
                            fontSize: "0.6rem",
                            fontWeight: 600,
                            letterSpacing: "0.02em",
                            color: "#3d5afe",
                            background: "#3d5afe14",
                            borderRadius: 999,
                            maxWidth: 140,
                            whiteSpace: "nowrap",
                            overflow: "hidden",
                            textOverflow: "ellipsis",
                          }}
                        >
                          ▸ {activeProjectName ?? "this project"}
                        </span>
                      )}
                    </button>
                  </div>
                  <button
                    type="button"
                    onClick={newThread}
                    style={{
                      background: "none",
                      border: "none",
                      cursor: "pointer",
                      padding: "4px 5px",
                      borderRadius: 6,
                      color: "#6b6560",
                      display: "flex",
                      alignItems: "center",
                    }}
                    title="New conversation"
                  >
                    <Plus style={{ width: 14, height: 14 }} />
                  </button>
                </div>
              </div>
              <div
                style={{
                  flex: 1,
                  minWidth: 0,
                  minHeight: 0,
                  display: "flex",
                  alignItems: "stretch",
                }}
              >
                <ProjectIconRail workspace={workspace} projectId={projectId} />
              </div>
            </div>

            <div
              style={{
                flex: 1,
                display: "flex",
                flexDirection: "row",
                minHeight: 0,
                minWidth: 0,
              }}
            >
              <div
                style={{
                  width: 380,
                  flexShrink: 0,
                  borderRight: "1px solid #e8e4dc",
                  background: "#fff",
                  display: "flex",
                  flexDirection: "column",
                  minHeight: 0,
                  minWidth: 0,
                }}
              >
                {structuralChatBody}
              </div>
              <div
                key={pathname}
                style={{
                  flex: 1,
                  minWidth: 0,
                  minHeight: 0,
                  overflow: "auto",
                  background: "#faf8f5",
                  display: "flex",
                  flexDirection: "column",
                }}
              >
                {artifactSlot}
              </div>
            </div>
          </div>
        </PreviewBridgeProvider>

        <style>{`
        @keyframes vibn-bounce {
          0%, 60%, 100% { transform: translateY(0); }
          30% { transform: translateY(-4px); }
        }
      `}</style>
      </>
    );
  }

  // ── Open panel ─────────────────────────────────────────────────────────────
  // Structural mode: fill the parent column. Default mode: fixed slide-out
  // anchored to the right edge of the viewport (legacy behavior).
  return (
    <div
      style={
        structural
          ? {
              flex: 1,
              minHeight: 0,
              background: "#fff",
              display: "flex",
              flexDirection: "column",
              fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
            }
          : {
              position: "fixed",
              right: 0,
              top: 0,
              bottom: 0,
              zIndex: 999,
              width: 380,
              background: "#fff",
              borderLeft: "1px solid #e8e4dc",
              display: "flex",
              flexDirection: "column",
              boxShadow: "-4px 0 24px #1a1a1a08",
              fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
            }
      }
    >
      {/* Header — structural: circle logo + threads; slide-out: wordmark */}
      <div
        style={
          structural
            ? {
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                height: 48,
                padding: "0 12px",
                boxSizing: "border-box",
                borderBottom: "1px solid #e8e4dc",
                background: "#faf8f5",
                flexShrink: 0,
                gap: 6,
              }
            : {
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                padding: "14px 16px",
                borderBottom: "1px solid #e8e4dc",
                background: "#faf8f5",
                flexShrink: 0,
              }
        }
      >
        {structural ? (
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 8,
              minWidth: 0,
            }}
          >
            {workspace ? (
              <Link
                href={`/${workspace}/projects`}
                title="All projects"
                style={{ flexShrink: 0, display: "flex" }}
              >
                <img
                  src="/vibn-black-circle-logo.png"
                  alt="VIBN"
                  width={26}
                  height={26}
                  style={{
                    borderRadius: 7,
                    objectFit: "cover",
                    display: "block",
                  }}
                />
              </Link>
            ) : null}
            <button
              type="button"
              onClick={() => setShowThreads((v) => !v)}
              aria-label="Conversations"
              style={{
                display: "flex",
                alignItems: "center",
                gap: 4,
                background: "none",
                border: "none",
                cursor: "pointer",
                padding: "2px 4px",
                borderRadius: 6,
                minWidth: 0,
              }}
            >
              <ChevronDown
                style={{
                  width: 12,
                  height: 12,
                  color: "#a09a90",
                  transition: "transform 0.15s",
                  flexShrink: 0,
                  transform: showThreads ? "rotate(180deg)" : "none",
                }}
              />
              {projectId && (
                <span
                  title={`Chat is scoped to ${activeProjectName ?? "this project"}. Tool calls assume projectId=${projectId}.`}
                  style={{
                    marginLeft: 4,
                    padding: "1px 6px",
                    fontSize: "0.6rem",
                    fontWeight: 600,
                    letterSpacing: "0.02em",
                    color: "#3d5afe",
                    background: "#3d5afe14",
                    borderRadius: 999,
                    maxWidth: 140,
                    whiteSpace: "nowrap",
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                  }}
                >
                  ▸ {activeProjectName ?? "this project"}
                </span>
              )}
            </button>
          </div>
        ) : (
          <button
            type="button"
            onClick={() => setShowThreads((v) => !v)}
            style={{
              display: "flex",
              alignItems: "center",
              gap: 6,
              background: "none",
              border: "none",
              cursor: "pointer",
              padding: "4px 6px",
              borderRadius: 6,
              minWidth: 0,
            }}
          >
            <span
              style={{
                fontFamily: "var(--font-lora),serif",
                fontSize: "0.95rem",
                fontWeight: 400,
                color: "#1a1a1a",
                letterSpacing: "-0.01em",
                whiteSpace: "nowrap",
              }}
            >
              Vibn AI
            </span>
            <ChevronDown
              style={{
                width: 13,
                height: 13,
                color: "#a09a90",
                transition: "transform 0.15s",
                flexShrink: 0,
                transform: showThreads ? "rotate(180deg)" : "none",
              }}
            />
            {projectId && (
              <span
                title={`Chat is scoped to ${activeProjectName ?? "this project"}. Tool calls assume projectId=${projectId}.`}
                style={{
                  marginLeft: 6,
                  padding: "2px 8px",
                  fontSize: "0.65rem",
                  fontWeight: 600,
                  letterSpacing: "0.02em",
                  color: "#3d5afe",
                  background: "#3d5afe14",
                  borderRadius: 999,
                  maxWidth: 160,
                  whiteSpace: "nowrap",
                  overflow: "hidden",
                  textOverflow: "ellipsis",
                }}
              >
                ▸ {activeProjectName ?? "this project"}
              </span>
            )}
          </button>
        )}
        <div style={{ display: "flex", gap: 4 }}>
          <button
            type="button"
            onClick={newThread}
            style={{
              background: "none",
              border: "none",
              cursor: "pointer",
              padding: structural ? "4px 5px" : "5px 6px",
              borderRadius: 6,
              color: "#6b6560",
              display: "flex",
              alignItems: "center",
            }}
            title="New conversation"
          >
            <Plus
              style={{
                width: structural ? 14 : 15,
                height: structural ? 14 : 15,
              }}
            />
          </button>
          {!structural && (
            <button
              type="button"
              onClick={() => setOpen(false)}
              style={{
                background: "none",
                border: "none",
                cursor: "pointer",
                padding: "5px 6px",
                borderRadius: 6,
                color: "#6b6560",
                display: "flex",
                alignItems: "center",
              }}
              title="Close"
            >
              <ChevronRight style={{ width: 15, height: 15 }} />
            </button>
          )}
        </div>
      </div>

      {conversationColumn}

      <style>{`
        @keyframes vibn-bounce {
          0%, 60%, 100% { transform: translateY(0); }
          30% { transform: translateY(-4px); }
        }
      `}</style>
    </div>
  );
}
