import { useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react';
import { MessageBubble, type MessageActions } from '@/components/MessageBubble';
import { ToolCallGroup } from '@/components/ToolCallGroup';
import { ToolCallLine, type ToolRun } from '@/components/ToolCallLine';
import { AskUserInputCard } from '@/components/AskUserInputCard';
import { wireToolCallToRun, type CoderToolCallWire } from '@/lib/coder-tools';
import type { Message } from '@/api/types';

export interface CoderMessageWire {
  id: string;
  role: 'user' | 'assistant' | 'system';
  content: string;
  // F1: 'cancelled' — a user Stop / stall finalized the turn (renders "Stopped").
  status?: 'streaming' | 'complete' | 'failed' | 'cancelled';
  model?: string | null;
  reasoning_text?: string;
  tool_calls?: CoderToolCallWire[];
}

export interface CoderToolMessageWire {
  id: string;
  role: 'tool';
  tool_results: {
    tool_call_id: string;
    output: unknown;
    truncated?: boolean;
    error?: string;
  };
}

export type CoderTimelineWire = CoderMessageWire | CoderToolMessageWire;

function isToolMessage(m: CoderTimelineWire): m is CoderToolMessageWire {
  return m.role === 'tool';
}

type RenderItem =
  | { kind: 'message'; message: CoderMessageWire }
  | { kind: 'tool_run'; run: ToolRun; key: string }
  | { kind: 'tool_group'; runs: ToolRun[]; key: string };

const GROUP_THRESHOLD = 3;
const SCROLL_THRESHOLD_PX = 150;

function flattenCoderMessages(messages: CoderTimelineWire[]): RenderItem[] {
  const items: RenderItem[] = [];
  const runsByCallId = new Map<string, ToolRun>();

  for (const m of messages) {
    if (isToolMessage(m)) {
      const run = runsByCallId.get(m.tool_results.tool_call_id);
      if (run) {
        run.result = {
          tool_call_id: m.tool_results.tool_call_id,
          output: m.tool_results.output,
          truncated: m.tool_results.truncated ?? false,
          ...(m.tool_results.error ? { error: m.tool_results.error } : {}),
        };
      }
      continue;
    }

    if (m.role === 'user' || m.role === 'system') {
      items.push({ kind: 'message', message: m });
      continue;
    }

    const hasToolCalls = (m.tool_calls?.length ?? 0) > 0;
    const hasText = m.content.trim().length > 0;
    const hasReasoning = (m.reasoning_text?.trim().length ?? 0) > 0;
    // External agents persist tool calls + final answer on one row. Render tools
    // before the answer text so the timeline matches BooChat (tools, then reply).
    const externalCombined = hasToolCalls && (hasText || hasReasoning);

    if (externalCombined) {
      if (hasReasoning) {
        items.push({
          kind: 'message',
          message: { ...m, content: '', reasoning_text: m.reasoning_text },
        });
      }
      for (const tc of m.tool_calls!) {
        const run = wireToolCallToRun(tc);
        runsByCallId.set(tc.id, run);
        items.push({ kind: 'tool_run', run, key: tc.id });
      }
      if (hasText || m.status === 'streaming') {
        items.push({
          kind: 'message',
          message: { ...m, reasoning_text: undefined },
        });
      }
      continue;
    }

    // Native inference: separate assistant rows per step — mirror MessageList.
    if (hasText || hasReasoning || m.status === 'streaming') {
      items.push({ kind: 'message', message: m });
    }
    if (hasToolCalls) {
      for (const tc of m.tool_calls!) {
        const run = wireToolCallToRun(tc);
        runsByCallId.set(tc.id, run);
        items.push({ kind: 'tool_run', run, key: tc.id });
      }
    }
  }

  return items;
}

function groupToolRuns(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') {
      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 }>;
    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;
}

interface Props {
  messages: CoderTimelineWire[];
  chatId?: string;
  footer?: ReactNode;
  actions?: MessageActions;
  // write-edit-robustness #4: assistant message ids that have a worktree
  // checkpoint. The "Restore to here" control renders only on these.
  checkpointMessageIds?: Set<string>;
  // write-edit-robustness #4: suppress restore during an active turn (mirrors
  // composer gating in CoderPane).
  restoreDisabled?: boolean;
}

const CODER_HIDDEN_ACTIONS: ('fork' | 'delete')[] = ['fork'];

export function CoderMessageList({
  messages,
  chatId,
  footer,
  actions,
  checkpointMessageIds,
  restoreDisabled,
}: Props) {
  const endRef = useRef<HTMLDivElement>(null);
  const scrollRef = useRef<HTMLDivElement>(null);
  const isNearBottomRef = useRef(true);

  const renderItems = useMemo(
    () => groupToolRuns(flattenCoderMessages(messages)),
    [messages],
  );

  const handleScroll = useCallback(() => {
    const el = scrollRef.current;
    if (!el) return;
    isNearBottomRef.current =
      el.scrollHeight - el.scrollTop - el.clientHeight < SCROLL_THRESHOLD_PX;
  }, []);

  useEffect(() => {
    if (isNearBottomRef.current) {
      endRef.current?.scrollIntoView({ block: 'end' });
    }
  }, [messages]);

  if (messages.length === 0) {
    return null;
  }

  return (
    <div className="flex-1 overflow-y-auto" ref={scrollRef} onScroll={handleScroll}>
      <div className="max-w-[1000px] mx-auto w-full px-6 py-4 space-y-4">
        {renderItems.map((item) => {
          if (item.kind === 'message') {
            return (
              <MessageBubble
                key={item.message.id}
                message={item.message as unknown as Message}
                actions={actions}
                hideActions={CODER_HIDDEN_ACTIONS}
                hasCheckpoint={checkpointMessageIds?.has(item.message.id) ?? false}
                restoreDisabled={restoreDisabled}
              />
            );
          }
          if (item.kind === 'tool_run') {
            if (item.run.call.name === 'ask_user_input' && chatId) {
              return (
                <AskUserInputCard
                  key={item.key}
                  toolCall={item.run.call}
                  toolResult={item.run.result}
                  chatId={chatId}
                  apiPrefix="/api/coder"
                />
              );
            }
            return <ToolCallLine key={item.key} run={item.run} />;
          }
          return <ToolCallGroup key={item.key} runs={item.runs} />;
        })}
        {footer}
        <div ref={endRef} />
      </div>
    </div>
  );
}
