import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import type { ComponentChildren, JSX } from "preact";
import { cn } from "@/lib/utils";
import { ChatAttachmentList } from "@/components/chat/chat-attachment-list";
import { getMessageAttachments, type Message } from "@/lib/api";
import { MarkdownContent } from "@/components/markdown/markdown-content";
import { ChevronDown, ChevronUp, Globe2, Link2, Wrench } from "lucide-preact";

type Props = {
  messages: Message[];
  isLoading: boolean;
  isSending: boolean;
  onMessageContextMenu?: (event: MouseEvent, messageId: string) => void;
};

type ToolLogMetadata = {
  kind: "tool_call";
  toolCallId?: string;
  toolName?: string;
  status?: "initiated" | "completed" | "failed";
  summary?: string;
  args?: Record<string, unknown>;
  startedAt?: string;
  completedAt?: string;
  durationMs?: number;
  error?: string | null;
  resultPreview?: string | null;
};

function asToolLogMetadata(value: unknown): ToolLogMetadata | null {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  const record = value as Record<string, unknown>;
  if (record.kind !== "tool_call") return null;
  return record as ToolLogMetadata;
}

function getToolSummary(message: Message, metadata: ToolLogMetadata) {
  if (typeof metadata.summary === "string" && metadata.summary.trim()) return metadata.summary.trim();
  if (metadata.status === "failed" && typeof metadata.error === "string" && metadata.error.trim()) {
    return `Tool failed: ${metadata.error.trim()}`;
  }
  if (typeof metadata.resultPreview === "string" && metadata.resultPreview.trim()) return metadata.resultPreview.trim();
  if (message.content.trim()) return message.content.trim();
  const toolName = metadata.toolName?.trim() || message.name?.trim() || "unknown_tool";
  return `Ran tool '${toolName}'.`;
}

function getToolLabel(message: Message, metadata: ToolLogMetadata) {
  const raw = metadata.toolName?.trim() || message.name?.trim();
  if (!raw) return "Tool call";
  return raw
    .replace(/_/g, " ")
    .split(/\s+/)
    .filter(Boolean)
    .map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`)
    .join(" ");
}

function getToolIconName(toolName: string | null | undefined) {
  const lowered = toolName?.toLowerCase() ?? "";
  if (lowered.includes("search")) return "search";
  if (lowered.includes("url") || lowered.includes("fetch") || lowered.includes("http")) return "fetch";
  return "generic";
}

function formatDuration(durationMs: unknown) {
  if (typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs <= 0) return null;
  return `${Math.round(durationMs)} ms`;
}

function formatToolTimestamp(...values: Array<string | null | undefined>) {
  const value = values.find((candidate) => candidate && !Number.isNaN(new Date(candidate).getTime()));
  if (!value) return null;
  return new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }).format(new Date(value));
}

type ToolCallVisualState = "initiated" | "completed" | "failed";
type MessageRenderItem = { kind: "message"; message: Message } | { kind: "tool_group"; key: string; messages: Message[] };
type ToolStackStyle = JSX.CSSProperties & {
  "--tool-stack-x"?: string;
  "--tool-stack-y"?: string;
  "--tool-stack-z"?: string;
  "--tool-stack-scale"?: string;
  "--tool-stack-opacity"?: string;
  "--tool-stack-delay"?: string;
  "--tool-stack-from-transform"?: string;
  "--tool-stack-to-transform"?: string;
  "--tool-stack-from-opacity"?: string;
  "--tool-stack-to-opacity"?: string;
};
type ToolStackContainerStyle = JSX.CSSProperties & {
  "--tool-stack-from-height"?: string;
  "--tool-stack-to-height"?: string;
};
type ToolStackMotionDirection = "expand" | "collapse" | null;

const COLLAPSED_TOOL_STACK_LIMIT = 4;
const TOOL_STACK_CARD_HEIGHT = 62;
const TOOL_STACK_CARD_GAP = 10;
const TOOL_STACK_LAYOUT_ANIMATION_MS = 340;

function getToolVisualState(metadata: ToolLogMetadata): ToolCallVisualState {
  if (metadata.status === "failed") return "failed";
  if (metadata.status === "initiated") return "initiated";
  return "completed";
}

function getToolDetailLabel(message: Message, metadata: ToolLogMetadata, state: ToolCallVisualState) {
  return [
    state === "failed" ? "Failed" : state === "initiated" ? "Running" : "Completed",
    formatDuration(metadata.durationMs),
    formatToolTimestamp(message.createdAt, metadata.completedAt, metadata.startedAt),
  ]
    .filter(Boolean)
    .join(" • ");
}

function buildMessageRenderItems(messages: Message[]) {
  const items: MessageRenderItem[] = [];
  let toolRun: Message[] = [];

  const flushToolRun = () => {
    if (!toolRun.length) return;
    if (toolRun.length === 1) {
      items.push({ kind: "message", message: toolRun[0] });
    } else {
      items.push({ kind: "tool_group", key: toolRun[0].id, messages: toolRun });
    }
    toolRun = [];
  };

  for (const message of messages) {
    if (message.role === "tool" && asToolLogMetadata(message.metadata)) {
      toolRun.push(message);
      continue;
    }

    flushToolRun();
    items.push({ kind: "message", message });
  }

  flushToolRun();
  return items;
}

function getToolCallMessageIDs(messages: Message[]) {
  const ids = new Set<string>();
  for (const message of messages) {
    if (message.role === "tool" && asToolLogMetadata(message.metadata)) ids.add(message.id);
  }
  return ids;
}

function getToolStackHeight(messageCount: number, expanded: boolean) {
  const visibleCount = Math.min(messageCount, COLLAPSED_TOOL_STACK_LIMIT);
  return expanded
    ? `${TOOL_STACK_CARD_HEIGHT + Math.max(0, messageCount - 1) * (TOOL_STACK_CARD_HEIGHT + TOOL_STACK_CARD_GAP)}px`
    : `${TOOL_STACK_CARD_HEIGHT + Math.max(0, visibleCount - 1) * TOOL_STACK_CARD_GAP}px`;
}

function getToolStackContainerStyle(messageCount: number, expanded: boolean, motionDirection: ToolStackMotionDirection): ToolStackContainerStyle {
  const collapsedHeight = getToolStackHeight(messageCount, false);
  const expandedHeight = getToolStackHeight(messageCount, true);
  const targetHeight = expanded ? expandedHeight : collapsedHeight;
  const fromHeight = motionDirection === "expand" ? collapsedHeight : motionDirection === "collapse" ? expandedHeight : targetHeight;

  return {
    "--tool-stack-from-height": fromHeight,
    "--tool-stack-to-height": targetHeight,
    height: targetHeight,
  };
}

function getExpandedToolLayout(index: number, messageCount: number) {
  const y = `${index * (TOOL_STACK_CARD_HEIGHT + TOOL_STACK_CARD_GAP)}px`;
  return {
    opacity: "1",
    transform: `translate3d(0px, ${y}, 0px) scale(1)`,
    x: "0px",
    y,
    z: "0px",
    scale: "1",
    zIndex: messageCount - index,
  };
}

function getCollapsedToolLayout(index: number, messageCount: number) {
  const depth = messageCount - index - 1;
  const visibleDepth = Math.min(depth, COLLAPSED_TOOL_STACK_LIMIT - 1);
  const isHidden = depth >= COLLAPSED_TOOL_STACK_LIMIT;
  const visibleCount = Math.min(messageCount, COLLAPSED_TOOL_STACK_LIMIT);
  const x = `${visibleDepth * 11}px`;
  const y = `${visibleDepth * TOOL_STACK_CARD_GAP}px`;
  const z = `${visibleDepth * -36}px`;
  const scale = `${Math.max(0.88, 1 - visibleDepth * 0.035)}`;
  const opacity = isHidden ? "0" : `${Math.max(0.34, 1 - visibleDepth * 0.22)}`;

  return {
    opacity,
    transform: `translate3d(${x}, ${y}, ${z}) scale(${scale})`,
    x,
    y,
    z,
    scale,
    zIndex: isHidden ? 0 : visibleCount - visibleDepth,
  };
}

function getToolStackStyle(index: number, messageCount: number, expanded: boolean, motionDirection: ToolStackMotionDirection): ToolStackStyle {
  const expandedLayout = getExpandedToolLayout(index, messageCount);
  const collapsedLayout = getCollapsedToolLayout(index, messageCount);
  const targetLayout = expanded ? expandedLayout : collapsedLayout;
  const fromLayout = motionDirection === "expand" ? collapsedLayout : motionDirection === "collapse" ? expandedLayout : targetLayout;

  return {
    "--tool-stack-x": targetLayout.x,
    "--tool-stack-y": targetLayout.y,
    "--tool-stack-z": targetLayout.z,
    "--tool-stack-scale": targetLayout.scale,
    "--tool-stack-opacity": targetLayout.opacity,
    "--tool-stack-delay": `${Math.min(messageCount - index - 1, COLLAPSED_TOOL_STACK_LIMIT - 1) * 34}ms`,
    "--tool-stack-from-transform": fromLayout.transform,
    "--tool-stack-to-transform": targetLayout.transform,
    "--tool-stack-from-opacity": fromLayout.opacity,
    "--tool-stack-to-opacity": targetLayout.opacity,
    opacity: targetLayout.opacity,
    transform: targetLayout.transform,
    zIndex: targetLayout.zIndex,
  };
}

function ToolCallCard({
  message,
  className,
  style,
}: {
  message: Message;
  className?: string;
  style?: JSX.CSSProperties;
}) {
  const toolLogMetadata = asToolLogMetadata(message.metadata);
  if (!toolLogMetadata) return null;

  const iconKind = getToolIconName(toolLogMetadata.toolName ?? message.name);
  const Icon = iconKind === "search" ? Globe2 : iconKind === "fetch" ? Link2 : Wrench;
  const toolState = getToolVisualState(toolLogMetadata);
  const isFailed = toolState === "failed";
  const isInitiated = toolState === "initiated";
  const toolSummary = getToolSummary(message, toolLogMetadata);
  const toolLabel = getToolLabel(message, toolLogMetadata);
  const toolDetailLabel = getToolDetailLabel(message, toolLogMetadata, toolState);

  return (
    <div
      className={cn(
        "inline-flex min-w-0 items-start gap-3 overflow-hidden rounded-xl border px-3 py-2.5 shadow-[inset_0_1px_0_hsl(180_100%_88%_/_0.06)]",
        isFailed
          ? "border-rose-400/44 bg-[linear-gradient(90deg,hsl(350_64%_20%),hsl(342_58%_9%))]"
          : isInitiated
            ? "border-amber-300/44 bg-[linear-gradient(90deg,hsl(43_72%_20%),hsl(260_48%_13%))]"
            : "border-cyan-400/44 bg-[linear-gradient(90deg,hsl(184_82%_14%),hsl(208_66%_10%))]",
        className
      )}
      style={style}
      title={`${toolSummary}\n${toolLabel} • ${toolDetailLabel}`}
    >
      <span
        className={cn(
          "mt-0.5 flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-lg border",
          isFailed
            ? "border-rose-400/34 bg-rose-400/13 text-rose-300"
            : isInitiated
              ? "border-amber-300/34 bg-amber-300/13 text-amber-200"
              : "border-cyan-300/34 bg-cyan-300/13 text-cyan-300"
        )}
      >
        <Icon className="h-4 w-4" />
      </span>
      <span className="min-w-0 flex-1 space-y-1">
        <span className={cn("block truncate text-sm leading-5", isFailed ? "text-rose-200" : "text-violet-50/95")}>{toolSummary}</span>
        <span className="flex min-w-0 items-center gap-1.5 text-[11px] leading-4">
          <span className={cn("min-w-0 truncate font-semibold", isFailed ? "text-rose-300/85" : isInitiated ? "text-amber-200/90" : "text-cyan-200/90")}>
            {toolLabel}
          </span>
          <span className="min-w-0 truncate text-violet-200/64">{toolDetailLabel}</span>
        </span>
      </span>
    </div>
  );
}

function ToolCallStackCardSurface({
  messageID,
  animateEntry,
  isHidden,
  children,
}: {
  messageID: string;
  animateEntry: boolean;
  isHidden: boolean;
  children: ComponentChildren;
}) {
  const [shouldAnimateEntry] = useState(() => animateEntry);

  return (
    <div
      className={cn("tool-call-stack-card-surface", shouldAnimateEntry && !isHidden && "tool-call-stack-card-enter")}
      data-tool-stack-card-id={messageID}
    >
      {children}
    </div>
  );
}

function ToolCallStack({
  groupKey,
  messages,
  expanded,
  entryMessageIDs,
  onToggle,
}: {
  groupKey: string;
  messages: Message[];
  expanded: boolean;
  entryMessageIDs: Set<string>;
  onToggle: (groupKey: string) => void;
}) {
  const hiddenCount = Math.max(0, messages.length - COLLAPSED_TOOL_STACK_LIMIT);
  const countLabel = `${messages.length} tool ${messages.length === 1 ? "call" : "calls"}`;
  const [motionDirection, setMotionDirection] = useState<ToolStackMotionDirection>(null);
  const [motionRevision, setMotionRevision] = useState(0);
  const motionResetTimerRef = useRef<number | null>(null);

  const handleToggle = () => {
    setMotionDirection(expanded ? "collapse" : "expand");
    setMotionRevision((current) => current + 1);
    if (typeof window !== "undefined") {
      if (motionResetTimerRef.current !== null) window.clearTimeout(motionResetTimerRef.current);
      motionResetTimerRef.current = window.setTimeout(() => {
        setMotionDirection(null);
        motionResetTimerRef.current = null;
      }, TOOL_STACK_LAYOUT_ANIMATION_MS + 60);
    }
    onToggle(groupKey);
  };

  return (
    <div className="flex justify-start">
      <div
        className={cn(
          "tool-call-stack-shell relative w-full max-w-[85%] min-w-0 pr-10",
          motionDirection && (motionRevision % 2 === 0 ? "tool-call-stack-shell-layout-a" : "tool-call-stack-shell-layout-b")
        )}
        data-tool-stack-group={groupKey}
        data-expanded={expanded ? "true" : "false"}
        style={getToolStackContainerStyle(messages.length, expanded, motionDirection)}
      >
        {messages.map((message, index) => {
          const depth = messages.length - index - 1;
          const isHidden = !expanded && depth >= COLLAPSED_TOOL_STACK_LIMIT;
          const shouldAnimateEntry = entryMessageIDs.has(message.id) && !isHidden;
          return (
            <div
              key={message.id}
              className={cn(
                "tool-call-stack-card absolute left-0 right-10 top-0 w-auto max-w-none",
                motionDirection && (motionRevision % 2 === 0 ? "tool-call-stack-card-layout-a" : "tool-call-stack-card-layout-b"),
                isHidden && "pointer-events-none"
              )}
              style={getToolStackStyle(index, messages.length, expanded, motionDirection)}
              aria-hidden={isHidden ? "true" : undefined}
            >
              <ToolCallStackCardSurface messageID={message.id} animateEntry={shouldAnimateEntry} isHidden={isHidden}>
                <ToolCallCard message={message} className="tool-call-stack-card-glass w-full max-w-full" />
              </ToolCallStackCardSurface>
            </div>
          );
        })}
        {!expanded && hiddenCount ? (
          <span className="absolute bottom-1 right-10 z-20 rounded-full border border-cyan-300/30 bg-slate-950/86 px-2 py-0.5 text-[10px] font-semibold leading-none text-cyan-100 shadow-sm">
            +{hiddenCount}
          </span>
        ) : null}
        <button
          type="button"
          className="tool-call-stack-toggle absolute right-0 top-2 z-20 flex h-8 w-8 items-center justify-center rounded-full"
          aria-expanded={expanded ? "true" : "false"}
          aria-label={`${expanded ? "Collapse" : "Expand"} ${countLabel}`}
          title={`${expanded ? "Collapse" : "Expand"} ${countLabel}`}
          onClick={handleToggle}
        >
          {expanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
        </button>
      </div>
    </div>
  );
}

export function ChatMessagesPanel({ messages, isLoading, isSending, onMessageContextMenu }: Props) {
  const hasPendingAssistant = messages.some((message) => message.id.startsWith("temp-assistant-") && message.content.trim().length === 0);
  const renderItems = useMemo(() => buildMessageRenderItems(messages), [messages]);
  const toolCallMessageIDs = useMemo(() => getToolCallMessageIDs(messages), [messages]);
  const seenToolCallMessageIDsRef = useRef<Set<string> | null>(null);
  const entryToolCallMessageIDs = useMemo(() => {
    const seenIDs = seenToolCallMessageIDsRef.current;
    if (!seenIDs) return new Set<string>();
    const entryIDs = new Set<string>();
    for (const id of toolCallMessageIDs) {
      if (!seenIDs.has(id)) entryIDs.add(id);
    }
    return entryIDs;
  }, [toolCallMessageIDs]);
  const [expandedToolGroups, setExpandedToolGroups] = useState<Set<string>>(() => new Set());

  useEffect(() => {
    if (!toolCallMessageIDs.size) return;
    const seenIDs = seenToolCallMessageIDsRef.current ?? new Set<string>();
    for (const id of toolCallMessageIDs) seenIDs.add(id);
    seenToolCallMessageIDsRef.current = seenIDs;
  }, [toolCallMessageIDs]);

  const toggleToolGroup = (groupKey: string) => {
    setExpandedToolGroups((current) => {
      const next = new Set(current);
      if (next.has(groupKey)) next.delete(groupKey);
      else next.add(groupKey);
      return next;
    });
  };

  return (
    <>
      {isLoading && messages.length === 0 ? <p className="text-sm text-muted-foreground">Loading messages...</p> : null}
      <div className="mx-auto max-w-4xl space-y-6">
        {renderItems.map((item) => {
          if (item.kind === "tool_group") {
            return (
              <ToolCallStack
                key={`tool-group-${item.key}`}
                groupKey={item.key}
                messages={item.messages}
                expanded={expandedToolGroups.has(item.key)}
                entryMessageIDs={entryToolCallMessageIDs}
                onToggle={toggleToolGroup}
              />
            );
          }

          const { message } = item;
          const toolLogMetadata = asToolLogMetadata(message.metadata);
          if (message.role === "tool" && toolLogMetadata) {
            return (
              <div key={message.id} className="flex justify-start">
                <ToolCallCard message={message} className="max-w-[85%]" />
              </div>
            );
          }

          const isUser = message.role === "user";
          const isPendingAssistant = message.id.startsWith("temp-assistant-") && isSending && message.content.trim().length === 0;
          const attachments = getMessageAttachments(message.metadata);
          return (
            <div key={message.id} className={cn("flex", isUser ? "justify-end" : "justify-start")}>
              <div
                className={cn(
                  "max-w-[85%] space-y-3",
                  isUser
                    ? "rounded-xl border border-violet-300/24 bg-[linear-gradient(135deg,hsl(258_86%_48%_/_0.86),hsl(278_72%_29%_/_0.86))] px-4 py-3 text-sm leading-6 text-fuchsia-50 shadow-sm"
                    : "text-base leading-7 text-violet-50"
                )}
                onContextMenu={
                  message.role === "assistant" && !message.id.startsWith("temp-") && onMessageContextMenu
                    ? (event) => onMessageContextMenu(event, message.id)
                    : undefined
                }
              >
                {attachments.length ? <ChatAttachmentList attachments={attachments} tone={isUser ? "user" : "assistant"} /> : null}
                {isPendingAssistant ? (
                  <span className="inline-flex items-center gap-1" aria-label="Assistant is typing" role="status">
                    <span className="inline-block h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:0ms]" />
                    <span className="inline-block h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:140ms]" />
                    <span className="inline-block h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:280ms]" />
                  </span>
                ) : message.content.trim() ? (
                  <MarkdownContent
                    markdown={message.content}
                    openLinksInNewTab
                    className={cn("[&_a]:text-inherit [&_a]:underline", isUser ? "leading-[1.78] text-fuchsia-50" : "leading-[1.82] text-violet-50")}
                  />
                ) : null}
              </div>
            </div>
          );
        })}
        {isSending && !hasPendingAssistant ? (
          <div className="flex justify-start">
            <div className="max-w-[85%] text-base leading-7 text-violet-50">
              <span className="inline-flex items-center gap-1" aria-label="Assistant is typing" role="status">
                <span className="inline-block h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:0ms]" />
                <span className="inline-block h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:140ms]" />
                <span className="inline-block h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:280ms]" />
              </span>
            </div>
          </div>
        ) : null}
      </div>
    </>
  );
}
