import { memo, useMemo } from 'react';
import type { Chat, ErrorReason, Message } from '@/api/types';
import { shortenModelName } from '@/lib/modelName';
import { CapHitSentinel } from './CapHitSentinel';
import { DoomLoopSentinel } from './DoomLoopSentinel';
import { MarkdownRenderer } from './MarkdownRenderer';
import {
  StatsLine,
  ActionRow,
  CompactCard,
  SummaryCard,
  ReasoningBlock,
  MistakeRecoverySentinel,
  SendToTerminalMenu,
} from './message-parts';

// v1.8.2: human labels for the machine-readable error reasons that ride on
// failed assistant messages via metadata.kind === 'error'. Kept short so the
// inline render under "message failed" stays a single muted line.
const ERROR_REASON_LABELS: Record<ErrorReason, string> = {
  llm_provider_error: 'LLM provider error',
  tool_execution_failed: 'Tool execution failed',
  summary_after_cap_failed: 'Summary after tool budget hit failed',
};

// v1.14.x-html-artifact-panes: MarkdownBody and its path-linkifier helpers
// moved to apps/web/src/components/MarkdownRenderer.tsx so the new artifact
// panes can render assistant content with the same Shiki + remark-gfm setup.

export interface MessageActions {
  onRegenerate?: (chatId: string, messageId: string) => Promise<void>;
  onResend?: (chatId: string, content: string) => Promise<void>;
  onFork?: (chatId: string, messageId: string) => Promise<void>;
  onDelete?: (chatId: string, messageId: string) => Promise<void>;
  // write-edit-robustness #4 (BooCoder only): reset the worktree to this
  // message's pre-turn checkpoint and trim the transcript past it. BooChat
  // passes no such callback → the "Restore to here" control never renders.
  onRestoreCheckpoint?: (chatId: string, messageId: string) => Promise<void>;
}

interface Props {
  message: Message;
  sessionChats?: Chat[];
  capHitInfo?: { position: number; isLatest: boolean };
  actions?: MessageActions;
  /** Hide actions that don't apply (fork, delete). */
  hideActions?: ('fork' | 'delete')[];
  /**
   * write-edit-robustness #4: this assistant message has a worktree checkpoint
   * → render "Restore to here" (only when `actions.onRestoreCheckpoint` is also
   * provided). CoderMessageList sets this from the checkpoint set.
   */
  hasCheckpoint?: boolean;
  /**
   * write-edit-robustness #4: suppress the restore control during an active
   * turn (mirrors composer gating). Defaults to enabled.
   */
  restoreDisabled?: boolean;
}

export const MessageBubble = memo(function MessageBubble({
  message,
  sessionChats,
  capHitInfo,
  actions,
  hideActions,
  hasCheckpoint,
  restoreDisabled,
}: Props) {
  const hiddenSet = useMemo(() => new Set(hideActions ?? []), [hideActions]);
  // v1.11: anchored rolling summary row. Checked BEFORE the kind==='compact'
  // branch because summary=true never coexists with kind='compact' (new
  // compactions emit role='assistant' rows with kind='message'+summary=true).
  if (message.summary) {
    return <SummaryCard message={message} />;
  }
  if (message.kind === 'compact') {
    return <CompactCard message={message} sessionChats={sessionChats} />;
  }

  // v1.8.2: cap-hit sentinels render as a distinct system bubble with a
  // Continue button. MessageList's pre-render pass tags each sentinel with
  // its position; only the latest gets the actionable button.
  if (
    message.role === 'system' &&
    message.metadata?.kind === 'cap_hit' &&
    capHitInfo
  ) {
    return (
      <CapHitSentinel
        message={message}
        capHitPosition={capHitInfo.position}
        isLatest={capHitInfo.isLatest}
      />
    );
  }

  // v1.11.6: doom-loop sentinel. No Continue affordance — retrying with the
  // same tools would just re-loop. The card explains what tripped and
  // suggests next steps (new message angle / switch agents).
  if (message.role === 'system' && message.metadata?.kind === 'doom_loop') {
    return <DoomLoopSentinel message={message} />;
  }

  // feature #12: mistake-recovery sentinel. Non-escalated rows narrate that
  // recovery guidance was injected mid-turn; escalated rows report the turn
  // was stopped and (when can_continue) offer the cap-hit-style Continue.
  if (message.role === 'system' && message.metadata?.kind === 'mistake_recovery') {
    return <MistakeRecoverySentinel message={message} />;
  }

  // v1.8.2: tool messages and assistant tool_calls are now rendered by
  // MessageList via ToolCallLine / ToolCallGroup. Tool-role messages reach
  // this point only if MessageList didn't consume them (shouldn't happen,
  // but guard against it by rendering nothing rather than a stale card).
  if (message.role === 'tool') return null;

  if (message.role === 'user') {
    return (
      <div className="group flex flex-col items-end gap-1">
        <SendToTerminalMenu>
          <div className="boo-user-bubble max-w-[80%] rounded-lg bg-primary text-primary-foreground px-3 py-2 text-sm whitespace-pre-wrap break-words min-w-0">
            {message.content}
          </div>
        </SendToTerminalMenu>
        <ActionRow message={message} actions={actions} hiddenSet={hiddenSet} />
      </div>
    );
  }

  const isStreaming = message.status === 'streaming';
  const failed = message.status === 'failed';
  // F1 (D-10): a user Stop finalizes the turn as 'cancelled' — surface a muted
  // "Stopped" label (not the red "message failed" — a deliberate Stop is not a
  // failure), keeping whatever streamed before the abort.
  const cancelled = message.status === 'cancelled';
  // v1.13.7: match the MessageList.flatten trim guard so a whitespace-only
  // assistant turn doesn't render an empty bubble + dangling ActionRow.
  const hasContent = message.content.trim().length > 0;
  // model-attribution chip: short label for the model that produced this turn.
  const modelLabel = shortenModelName(message.model);
  // Reasoning arrives as a pre-joined string (coder wire) or as parts (native
  // inference). Read whichever is present; loose ?? chain tolerates the coder
  // shape where reasoning_parts is undefined (see CLAUDE.md null-guard note).
  const reasoningText = (
    message.reasoning_text ??
    message.reasoning_parts?.map((p) => p.text ?? '').join('') ??
    ''
  ).trim();
  const hasReasoning = reasoningText.length > 0;
  // v1.8.2: if metadata stamps an error reason, surface it inline under the
  // generic "message failed" line. Keeps the user's eye where it already is
  // rather than introducing a separate banner.
  const errorMeta =
    message.metadata != null && message.metadata.kind === 'error'
      ? message.metadata
      : null;

  return (
    <div className="group flex flex-col gap-2">
      {hasReasoning && <ReasoningBlock text={reasoningText} streaming={isStreaming} />}
      {(hasContent || isStreaming) && (
        <SendToTerminalMenu>
          <div className="max-w-[90%] text-sm leading-relaxed space-y-2 break-words min-w-0">
            {hasContent ? <MarkdownRenderer content={message.content} /> : null}
            {isStreaming && (
              <span className="inline-block w-1.5 h-3.5 align-baseline bg-muted-foreground/60 animate-pulse" />
            )}
          </div>
        </SendToTerminalMenu>
      )}
      {failed && (
        <div className="text-xs text-destructive">
          message failed
          {errorMeta && (
            <span className="block text-muted-foreground mt-0.5">
              {ERROR_REASON_LABELS[errorMeta.error_reason]}
              {errorMeta.error_text ? ` — ${errorMeta.error_text}` : ''}
            </span>
          )}
        </div>
      )}
      {cancelled && <div className="text-xs text-muted-foreground">Stopped</div>}
      {!isStreaming && (modelLabel || null) && (
        <span
          className="inline-flex w-fit items-center rounded-full border border-primary/25 bg-primary/10 px-2 py-0.5 text-[10px] font-mono text-primary/90"
          title={message.model ?? undefined}
        >
          {modelLabel}
        </span>
      )}
      {!isStreaming && <StatsLine message={message} />}
      {!isStreaming && hasContent && (
        <ActionRow
          message={message}
          actions={actions}
          hiddenSet={hiddenSet}
          hasCheckpoint={hasCheckpoint}
          restoreDisabled={restoreDisabled}
        />
      )}
    </div>
  );
});
