import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { motion } from 'framer-motion';
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
import { Pin } from 'lucide-react';
import type { Chat, Message } from '@/api/types';
import { MessageBubble } from './MessageBubble';
import { ToolCallGroup } from './ToolCallGroup';
import { ToolCallLine, type ToolRun } from './ToolCallLine';
import { AskUserInputCard } from './AskUserInputCard';
import { RequestReadAccessCard } from './RequestReadAccessCard';
import { MessageListErrorBoundary } from './MessageListErrorBoundary';

interface Props {
  messages: Message[];
  sessionChats?: Chat[];
}

// v1.8.2: pre-render units. The single linear `messages` array gets walked
// into a render-time list where each tool_call is a first-class item and
// tool_result messages are folded onto their matching tool_run by id.
// Batch 9.7: tool_run carries chat_id so AskUserInputCard can post the
// answer without threading the chat id through MessageList's parent.
type RenderItem =
  | { kind: 'message'; message: Message; capHitInfo?: { position: number; isLatest: boolean } }
  | { kind: 'tool_run'; run: ToolRun; key: string; chatId: string }
  | { kind: 'tool_group'; runs: ToolRun[]; key: string };

const GROUP_THRESHOLD = 3;

function isCapHitSentinel(m: Message): boolean {
  return m.role === 'system' && m.metadata?.kind === 'cap_hit';
}

// First pass: walk messages chronologically, expanding assistant tool_calls
// into per-call run items and folding tool_result messages onto their
// matching runs. Tool messages themselves never produce a render item.
// Assistant messages produce a text render item only when they have text;
// pure tool-call messages are "transparent" so consecutive tool runs can
// still group across them.
function flatten(messages: Message[]): RenderItem[] {
  const items: RenderItem[] = [];
  const runsByCallId = new Map<string, ToolRun>();

  for (const m of messages) {
    if (m.role === 'tool') {
      if (m.tool_results) {
        const run = runsByCallId.get(m.tool_results.tool_call_id);
        if (run) run.result = m.tool_results;
      }
      continue;
    }
    const hasToolCalls = m.tool_calls != null && m.tool_calls.length > 0;
    // v1.13.7: trim before checking. AI SDK v6 streaming occasionally emits a
    // leading "\n" text-delta on tool-call-only turns, which used to flow into
    // messages.content with length=1 and render an empty bubble + ActionRow
    // between each tool call. Whitespace-only content has no visible payload,
    // so treat it as no-content.
    const hasText = m.content.trim().length > 0;
    if (m.role === 'assistant' && hasToolCalls) {
      if (hasText || m.status === 'streaming') {
        items.push({ kind: 'message', message: m });
      }
      for (const tc of m.tool_calls!) {
        const run: ToolRun = { call: tc, result: null };
        runsByCallId.set(tc.id, run);
        items.push({ kind: 'tool_run', run, key: tc.id, chatId: m.chat_id });
      }
      continue;
    }
    items.push({ kind: 'message', message: m });
  }

  return items;
}

// Second pass: collapse runs of >=GROUP_THRESHOLD consecutive tool_run items
// of the same tool name into a single tool_group. Any other render item
// (text bubble, sentinel, user message) breaks the chain.
// Batch 9.7: ask_user_input never groups — each pause has its own card so
// grouping would render them as collapsed ToolCallLines which can't surface
// the interactive form.
function group(items: RenderItem[]): RenderItem[] {
  const out: RenderItem[] = [];
  let i = 0;
  while (i < items.length) {
    const item = items[i]!;
    if (item.kind !== 'tool_run') {
      out.push(item);
      i += 1;
      continue;
    }
    const name = item.run.call.name;
    if (name === 'ask_user_input' || name === 'request_read_access') {
      // v1.13.17: same rationale as ask_user_input — grouping would collapse
      // the interactive pause card into a non-actionable ToolCallLine.
      out.push(item);
      i += 1;
      continue;
    }
    let j = i + 1;
    while (
      j < items.length &&
      items[j]!.kind === 'tool_run' &&
      (items[j] as { kind: 'tool_run'; run: ToolRun }).run.call.name === name
    ) {
      j += 1;
    }
    const run = items.slice(i, j) as Array<{
      kind: 'tool_run';
      run: ToolRun;
      key: string;
      chatId: string;
    }>;
    if (run.length >= GROUP_THRESHOLD) {
      out.push({
        kind: 'tool_group',
        runs: run.map((r) => r.run),
        key: `group-${run[0]!.key}`,
      });
    } else {
      for (const r of run) out.push(r);
    }
    i = j;
  }
  return out;
}

// Third pass: number cap-hit sentinels (1-indexed) and mark the latest.
// CapHitSentinel uses position to compute the "N continues remaining"
// tooltip, and isLatest to gate the Continue button (only the most recent
// sentinel is actionable).
function stampCapHits(items: RenderItem[]): RenderItem[] {
  const totalCapHits = items.reduce(
    (n, it) => n + (it.kind === 'message' && isCapHitSentinel(it.message) ? 1 : 0),
    0,
  );
  if (totalCapHits === 0) return items;
  let index = 0;
  return items.map((it) => {
    if (it.kind !== 'message' || !isCapHitSentinel(it.message)) return it;
    index += 1;
    return {
      ...it,
      capHitInfo: { position: index, isLatest: index === totalCapHits },
    };
  });
}

export function MessageList({ messages, sessionChats }: Props) {
  const virtuosoRef = useRef<VirtuosoHandle>(null);
  const isNearBottomRef = useRef(true);
  const renderedKeysRef = useRef(new Set<string>());
  const prefersReducedMotionRef = useRef(false);
  const [animateEnabled, setAnimateEnabled] = useState(true);

  const [pinMessageId, setPinMessageId] = useState<string | null>(() => {
    if (typeof window !== 'undefined') {
      const hash = window.location.hash;
      if (hash.startsWith('#pin=')) return hash.slice(5);
    }
    return null;
  });

  const renderItems = useMemo(() => stampCapHits(group(flatten(messages))), [messages]);

  const pinIndex = useMemo(() => {
    if (!pinMessageId) return -1;
    return renderItems.findIndex(
      (item) => item.kind === 'message' && item.message.id === pinMessageId,
    );
  }, [pinMessageId, renderItems]);

  useEffect(() => {
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
    prefersReducedMotionRef.current = mq.matches;
    const handler = (e: MediaQueryListEvent) => {
      prefersReducedMotionRef.current = e.matches;
    };
    mq.addEventListener('change', handler);
    return () => mq.removeEventListener('change', handler);
  }, []);

  useEffect(() => {
    const handler = () => {
      const hash = window.location.hash;
      if (hash.startsWith('#pin=')) {
        setPinMessageId(hash.slice(5));
      } else {
        setPinMessageId(null);
      }
    };
    window.addEventListener('hashchange', handler);
    return () => window.removeEventListener('hashchange', handler);
  }, []);

  const atBottomStateChange = useCallback((atBottom: boolean) => {
    isNearBottomRef.current = atBottom;
    setAnimateEnabled(atBottom);
  }, []);

  const scrollToPin = useCallback(() => {
    if (pinIndex >= 0 && virtuosoRef.current) {
      virtuosoRef.current.scrollToIndex({ index: pinIndex, align: 'center' });
    }
  }, [pinIndex]);

  if (messages.length === 0) {
    return (
      <div className="flex-1 flex items-center justify-center text-sm text-muted-foreground">
        Send a message to start.
      </div>
    );
  }

  return (
    <MessageListErrorBoundary>
    <div className="flex-1 flex flex-col">
      {pinMessageId && pinIndex >= 0 && (
        <div className="shrink-0 flex items-center gap-2 px-4 py-1.5 bg-primary/10 border-b border-primary/20 text-xs text-primary">
          <Pin className="size-3" />
          <span>Pinned message</span>
          <button
            type="button"
            onClick={scrollToPin}
            className="ml-auto underline hover:no-underline"
          >
            Jump to pinned
          </button>
        </div>
      )}
      <Virtuoso
        ref={virtuosoRef}
        className="flex-1"
        data={renderItems}
        followOutput="auto"
        overscan={5}
        atBottomStateChange={atBottomStateChange}
        itemContent={(index, item) => {
          const key = item.kind === 'message' ? `msg-${item.message.id}` : item.key;
          const isNew = !renderedKeysRef.current.has(key);
          if (isNew) renderedKeysRef.current.add(key);

          const reducedMotion = prefersReducedMotionRef.current;
          const delay = isNew && !reducedMotion ? Math.min(index * 0.04, 0.5) : 0;
          const shouldAnimate = isNew && animateEnabled;

          return (
            <div
              className="max-w-[1000px] mx-auto w-full px-6 py-2"
              id={item.kind === 'message' ? `msg-${item.message.id}` : undefined}
            >
              <motion.div
                initial={shouldAnimate ? { opacity: 0, y: 8 } : false}
                animate={{ opacity: 1, y: 0 }}
                transition={delay > 0 ? { duration: 0.2, delay } : { duration: 0 }}
              >
                {item.kind === 'message' ? (
                  <MessageBubble
                    message={item.message}
                    sessionChats={sessionChats}
                    capHitInfo={item.capHitInfo}
                  />
                ) : item.kind === 'tool_run' ? (
                  item.run.call.name === 'ask_user_input' ? (
                    <AskUserInputCard
                      toolCall={item.run.call}
                      toolResult={item.run.result}
                      chatId={item.chatId}
                    />
                  ) : item.run.call.name === 'request_read_access' ? (
                    <RequestReadAccessCard
                      toolCall={item.run.call}
                      toolResult={item.run.result}
                      chatId={item.chatId}
                    />
                  ) : (
                    <ToolCallLine run={item.run} chatId={item.chatId} />
                  )
                ) : (
                  <ToolCallGroup runs={item.runs} />
                )}
              </motion.div>
            </div>
          );
        }}
      />
    </div>
    </MessageListErrorBoundary>
  );
}
