import type { UIMessage } from "@ai-sdk/react";
import { memo } from "react";
import { AssistantResponse, ChatBubble, ToolUseRow } from "~/components/runs/v3/ai/AIChatMessages";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover";

// ---------------------------------------------------------------------------
// AgentMessageView — renders an AI SDK UIMessage[] conversation.
//
// Extracted from the playground route so it can be reused on the run details
// page when the user picks the Agent view.
//
// UIMessage part types (AI SDK):
//   text            — markdown text content
//   reasoning       — model reasoning/thinking
//   tool-{name}     — tool call with input/output/state
//   source-url      — citation link
//   source-document — citation document reference
//   file            — file attachment (image, etc.)
//   step-start      — visual separator between steps
//   data-{name}     — custom data parts (rendered as a small popover)
// ---------------------------------------------------------------------------

export function AgentMessageView({ messages }: { messages: UIMessage[] }) {
  return (
    <div className="mx-auto flex w-full min-w-0 max-w-[800px] flex-col gap-2">
      {messages.map((msg) => (
        <MessageBubble key={msg.id} message={msg} />
      ))}
    </div>
  );
}

// Memoized so stable messages (anything older than the one currently
// streaming) don't re-render on every chunk. This matters a lot during
// `resumeStream()` history replay, where each re-render would otherwise
// re-run Prism highlighting on every tool-call CodeBlock in the list.
//
// Default shallow prop comparison is fine: AI SDK's useChat keeps stable
// references for messages that haven't changed, so only the last message
// (the one receiving new chunks) re-renders.
export const MessageBubble = memo(function MessageBubble({ message }: { message: UIMessage }) {
  if (message.role === "user") {
    const text =
      message.parts
        ?.filter((p) => p.type === "text")
        .map((p) => (p as { type: "text"; text: string }).text)
        .join("") ?? "";

    return (
      <div className="flex min-w-0 justify-end">
        <div className="max-w-[80%] rounded-lg bg-indigo-600 px-4 py-2.5 text-sm text-white">
          <div className="whitespace-pre-wrap wrap-anywhere">{text}</div>
        </div>
      </div>
    );
  }

  if (message.role === "assistant") {
    const hasContent = message.parts && message.parts.length > 0;
    if (!hasContent) return null;

    return <div className="space-y-2">{renderAssistantParts(message.parts ?? [])}</div>;
  }

  return null;
});

// Group consecutive data-* parts (rendered as inline DataPartPopover pills)
// under a single "Tool calls:" label with a flex-wrap row so they have a
// proper gap between them. Non-data parts pass through to renderPart
// unchanged.
function renderAssistantParts(parts: UIMessage["parts"]) {
  const nodes: React.ReactNode[] = [];
  let i = 0;
  while (i < parts.length) {
    const type = parts[i].type as string;
    if (type?.startsWith?.("data-") && type !== "data-subagent-run") {
      const groupStart = i;
      const group: UIMessage["parts"] = [];
      while (
        i < parts.length &&
        (parts[i].type as string)?.startsWith?.("data-") &&
        (parts[i].type as string) !== "data-subagent-run"
      ) {
        group.push(parts[i]);
        i++;
      }
      nodes.push(
        <div key={`data-${groupStart}`} className="flex items-center gap-1.5">
          <span className="text-xs font-medium text-text-dimmed">AI SDK data parts:</span>
          <div className="flex flex-wrap gap-1.5">
            {group.map((g, k) => renderPart(g, groupStart + k))}
          </div>
        </div>
      );
    } else {
      nodes.push(renderPart(parts[i], i));
      i++;
    }
  }
  return nodes;
}

// URLs in `source-url`/`file` parts come from streamed agent/tool data, so an
// unsafe scheme like `javascript:` would become a clickable XSS payload once it
// reaches an href/src. Allow only http(s)/blob (and data: for inline images),
// and return null for anything else so the caller can skip the link/image.
export function toSafeUrl(value: unknown, allowDataImage = false): string | null {
  if (typeof value !== "string") return null;
  let parsed: URL;
  try {
    parsed = new URL(value);
  } catch {
    return null;
  }
  if (parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.protocol === "blob:") {
    return value;
  }
  if (allowDataImage && parsed.protocol === "data:" && /^data:image\//i.test(value)) {
    return value;
  }
  return null;
}

export function renderPart(part: UIMessage["parts"][number], i: number) {
  const p = part as any;
  const type = part.type as string;

  // Text — markdown rendered via AssistantResponse
  if (type === "text") {
    return p.text ? <AssistantResponse key={i} text={p.text} headerLabel="" /> : null;
  }

  // Reasoning — amber-bordered italic block
  if (type === "reasoning") {
    return (
      <div key={i} className="border-l-2 border-amber-500/40 pl-2">
        <ChatBubble>
          <div className="whitespace-pre-wrap text-xs italic text-amber-200/70">{p.text ?? ""}</div>
        </ChatBubble>
      </div>
    );
  }

  // Tool call — type: "tool-{name}" with toolCallId, input, output, state
  if (type.startsWith("tool-")) {
    const toolName = type.slice(5);

    // Sub-agent tool: output is a UIMessage with parts
    const isSubAgent =
      p.output != null && typeof p.output === "object" && Array.isArray(p.output.parts);

    // For sub-agent tools, show the last text part as the "output" tab
    // (mirrors what toModelOutput typically sends to the parent LLM)
    // instead of dumping the full UIMessage JSON.
    let resultOutput: string | undefined;
    if (isSubAgent) {
      const lastText = (p.output.parts as any[])
        .filter((part: any) => part.type === "text" && part.text)
        .pop();
      resultOutput = lastText?.text ?? undefined;
    } else if (p.output != null) {
      resultOutput = typeof p.output === "string" ? p.output : JSON.stringify(p.output, null, 2);
    }

    // Status label for the tool row. AI SDK 7 HITL adds the
    // approval-requested / approval-responded states between input-available
    // and output-available, so surface those alongside the existing states.
    let resultSummary: string | undefined;
    if (p.state === "input-streaming" || p.state === "input-available") {
      resultSummary = "calling...";
    } else if (p.state === "approval-requested") {
      resultSummary = "awaiting approval";
    } else if (p.state === "approval-responded" || p.state === "output-denied") {
      resultSummary = p.approval?.approved
        ? "approved"
        : `denied${p.approval?.reason ? `: ${p.approval.reason}` : ""}`;
    } else if (p.state === "output-error") {
      resultSummary = `error: ${p.errorText ?? "unknown"}`;
    }

    return (
      <ToolUseRow
        key={i}
        tool={{
          toolCallId: p.toolCallId ?? `tool-${i}`,
          toolName,
          inputJson: JSON.stringify(p.input ?? {}, null, 2),
          resultOutput,
          resultSummary,
          subAgent: isSubAgent
            ? {
                parts: p.output.parts,
                isStreaming: p.state === "output-available" && p.preliminary === true,
              }
            : undefined,
        }}
      />
    );
  }

  // Source URL — clickable citation link
  if (type === "source-url") {
    const safeUrl = toSafeUrl(p.url);
    const label = p.title || p.url;
    // Unsafe scheme: render the citation text without a clickable link.
    if (!safeUrl) {
      return label ? (
        <div key={i} className="text-xs text-text-dimmed">
          {label}
        </div>
      ) : null;
    }
    return (
      <div key={i} className="text-xs">
        <a
          href={safeUrl}
          target="_blank"
          rel="noopener noreferrer"
          className="text-indigo-400 underline hover:text-indigo-300"
        >
          {label}
        </a>
      </div>
    );
  }

  // Source document — citation label
  if (type === "source-document") {
    return (
      <div key={i} className="text-xs text-text-dimmed">
        {p.title}
        {p.mediaType ? ` (${p.mediaType})` : ""}
      </div>
    );
  }

  // File — render as image if image type, otherwise as download link
  if (type === "file") {
    const isImage = typeof p.mediaType === "string" && p.mediaType.startsWith("image/");
    if (isImage) {
      const safeSrc = toSafeUrl(p.url, true); // allow data: URIs for inline images
      // Unsafe scheme: fall back to the filename, matching the non-image branch.
      if (!safeSrc) {
        return p.filename ? (
          <div key={i} className="text-xs text-text-dimmed">
            {p.filename}
          </div>
        ) : null;
      }
      return (
        <img
          key={i}
          src={safeSrc}
          alt={p.filename ?? "file"}
          className="max-h-64 rounded border border-border-bright"
        />
      );
    }
    const safeUrl = toSafeUrl(p.url);
    // Unsafe scheme: show the filename without a clickable download link.
    if (!safeUrl) {
      return p.filename ? (
        <div key={i} className="text-xs text-text-dimmed">
          {p.filename}
        </div>
      ) : null;
    }
    return (
      <div key={i} className="text-xs">
        <a
          href={safeUrl}
          target="_blank"
          rel="noopener noreferrer"
          className="text-indigo-400 underline hover:text-indigo-300"
        >
          {p.filename ?? "Download file"}
        </a>
      </div>
    );
  }

  // Step start — subtle dashed separator with centered label
  if (type === "step-start") {
    return (
      <div key={i} className="flex items-center gap-2 py-0.5">
        <div className="flex-1 border-t border-dashed border-border-bright" />
        <span className="text-[10px] text-text-faint">step</span>
        <div className="flex-1 border-t border-dashed border-border-bright" />
      </div>
    );
  }

  // Data parts — type: "data-{name}", show as labeled JSON popover
  if (type.startsWith("data-")) {
    const dataName = type.slice(5);
    return <DataPartPopover key={i} name={dataName} data={p.data} />;
  }

  return null;
}

function DataPartPopover({ name, data }: { name: string; data: unknown }) {
  const formatted = JSON.stringify(data, null, 2);

  return (
    <Popover>
      <PopoverTrigger asChild>
        <button
          type="button"
          className="inline-flex items-center gap-1 rounded border border-border-bright bg-background-bright px-1.5 py-0.5 font-mono text-[10px] text-text-dimmed transition-colors hover:border-border-brightest hover:text-text-bright"
        >
          <span className="text-purple-400">{name}</span>
          <span className="text-text-faint">{"{}"}</span>
        </button>
      </PopoverTrigger>
      <PopoverContent className="w-auto max-w-md p-0" align="start" sideOffset={4}>
        <div className="flex items-center justify-between border-b border-border-bright px-2.5 py-1.5">
          <span className="text-[10px] font-medium text-text-dimmed">data-{name}</span>
        </div>
        <div className="max-h-60 overflow-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
          <pre className="p-2.5 text-[11px] leading-relaxed text-text-bright">{formatted}</pre>
        </div>
      </PopoverContent>
    </Popover>
  );
}
