"use client";

import { ArrowUp, Square } from "lucide-react";
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { parseAgentReply } from "@/lib/agent-reply";
import type { ChatMessage, ChatMessageView } from "@/lib/chat-message-view";
import {
  createRectificationActivityReceiptState,
  receiptFromRectificationActivityState,
  reduceRectificationActivityReceipt,
  type CompletedActivityReceiptView,
} from "@/lib/rectification-activity-receipt";
import {
  isRecommendedRectificationCandidate,
  natalRecastMeaning,
  parseRectificationCandidateResult,
  workingRectificationHouseTable,
  type RectificationCandidateResult,
} from "@/lib/rectification-candidate-result";
import {
  diffRectificationBoard,
  RECTIFICATION_BOARD_SPLIT_MIN_PX,
} from "@/lib/rectification-board-model";
import { membershipHref } from "@/lib/membership";
import { vargaSentenceFromMethods } from "@/lib/rectification-varga-sentence";
import {
  isPublicRectificationMethod,
  isPublicRectificationTool,
} from "@/lib/rectification-agentic/v9/public-receipt";
import type { PublicLanguageModel } from "@/lib/public-models";
import { ChatMessageRow } from "./chat-message-row";
import {
  ChatMessageActions,
  toggleChatMessageFeedback,
} from "./chat-message-actions";
import { ModelSelector } from "./model-selector";
import { RectificationBoard, RectificationBoardPeek } from "./rectification-board";
import { Button } from "./ui/button";
import { Textarea } from "./ui/textarea";

type PersistedTurn = Readonly<{
  id: string;
  role: "user" | "assistant";
  text: string | null;
  status: string;
  receipt?: Readonly<{
    status: string;
    phases: readonly string[];
    tool_activities?: readonly Readonly<{
      tool: string;
      status: string;
      methods?: readonly string[];
    }>[];
    tools: readonly string[];
    methods?: readonly string[];
    skill_name?: string;
    skill_version?: string;
  }> | null;
}>;

type CandidateResult = RectificationCandidateResult | null;

function RectificationCandidateCards({
  result,
  acceptingCandidateId,
  readonly,
  onAccept,
}: Readonly<{
  result: RectificationCandidateResult;
  acceptingCandidateId: string | null;
  readonly: boolean;
  onAccept: (candidateId: string) => void;
}>) {
  return (
    <section className="rectification-candidates" aria-label="生时校正候选时间">
      <div className="rectification-candidates-heading">
        <strong>当前可能的出生时间</strong>
        <span>这次校正的结果是采用一个代表性时间作当前排盘。相邻分钟目前分不开，还不能确认唯一分钟。可以先采用，也可以继续补充事件或改选；新增证据后，候选和相对支持度会重新计算。</span>
        <span>相对支持度不是统计概率；采用不等于确认出生时间，也不会覆盖原始填报时间。</span>
      </div>
      <div className="rectification-candidate-list">
        {result.candidates.map((candidate) => {
          const selected = result.selectedTime === candidate.time;
          const recommended = isRecommendedRectificationCandidate(result, candidate);
          return (
            <button
              type="button"
              className={`rectification-candidate${selected ? " is-selected" : ""}`}
              key={candidate.candidateId}
              disabled={selected || Boolean(acceptingCandidateId) || readonly}
              onClick={() => onAccept(candidate.candidateId)}
            >
              <span className="rectification-candidate-time">
                <strong>{candidate.time}</strong>
                {selected && <span className="rectification-candidate-badge">已采用</span>}
                {recommended && <span className="rectification-candidate-badge">当前推荐</span>}
              </span>
              <span className="rectification-candidate-support">相对支持度 {candidate.relativeSupport}</span>
              <span className="rectification-candidate-action">
                {selected ? "已采用" : acceptingCandidateId === candidate.candidateId ? "正在采用…" : result.selectedTime ? "改选为此时间" : "采用此时间"}
              </span>
            </button>
          );
        })}
      </div>
    </section>
  );
}

function RectificationOosPanel({
  prompts,
}: Readonly<{ prompts: RectificationCandidateResult["oosBlindPrompts"] }>) {
  if (prompts.length === 0) return null;
  return (
    <section className="rectification-refinement" aria-label="盘外对照">
      <strong>盘外对照</strong>
      <p>这些经历没有进入刚才的评分。若记得大概时间，可以补一条用来核对；不补也可以先看盘。</p>
      <ul>
        {prompts.map((item) => (
          <li key={item.domain}>{item.user_meaning}</li>
        ))}
      </ul>
    </section>
  );
}

type RectificationAgenticChatProps = Readonly<{
  caseId: string;
  sessionId: string;
  readonly: boolean;
  shouldStartOpening: boolean;
  initialTurns: readonly PersistedTurn[];
  models: readonly PublicLanguageModel[];
  selectedModelId: string;
  onSelectModel: (modelId: string) => void;
  onMessagesChange?: (messages: ChatMessage[]) => void;
  onCompleted?: () => void;
  onPendingChange?: (pending: boolean) => void;
  onProfileIncomplete?: () => void;
  onSaved?: (time: string, status: "accepted" | "confirmed") => void;
  onStartConsultation?: () => void;
  pendingConsultationQuestion?: string | null;
  onRestart?: () => void;
  headerSlot: HTMLElement | null;
}>;

type RenderMessage = ChatMessageView & {
  renderKey: string;
  completedReceipt?: CompletedActivityReceiptView;
  failed?: boolean;
  turnId?: string;
};

function turnOfferedSelection(message: RenderMessage): boolean {
  return Boolean(message.completedReceipt?.steps.includes("rectification-offer-candidates"));
}

function completedReceiptFromPersisted(receipt: PersistedTurn["receipt"]): CompletedActivityReceiptView {
  if (!receipt) return { steps: [], methods: [] };
  if (Array.isArray(receipt.tool_activities)) {
    let state = createRectificationActivityReceiptState();
    for (const activity of receipt.tool_activities) {
      if (!isPublicRectificationTool(activity.tool)
        || (activity.status !== "completed" && activity.status !== "failed")) continue;
      state = reduceRectificationActivityReceipt(state, {
        tool: activity.tool,
        status: activity.status,
        methods: activity.status === "completed" && Array.isArray(activity.methods)
          ? activity.methods.filter(isPublicRectificationMethod)
          : [],
      });
    }
    return receiptFromRectificationActivityState(state);
  }
  return {
    steps: [...new Set((receipt.tools ?? []).filter(isPublicRectificationTool))],
    methods: [...new Set((receipt.methods ?? []).filter(isPublicRectificationMethod))],
  };
}


export function toggleRectificationFeedback(
  current: "up" | "down" | undefined,
  requested: "up" | "down",
): "up" | "down" | undefined {
  return toggleChatMessageFeedback(current, requested);
}

function hasActivityReceipt(receipt: CompletedActivityReceiptView): boolean {
  return receipt.steps.length > 0 || receipt.methods.length > 0 || Boolean(receipt.failedTool);
}

function persistedTurnFailed(turn: PersistedTurn): boolean {
  return turn.status === "failed"
    || turn.status === "retryable"
    || turn.receipt?.status === "failed"
    || turn.receipt?.status === "degraded"
    || turn.receipt?.status === "blocked";
}

function messagesFromTurns(initialTurns: readonly PersistedTurn[]): RenderMessage[] {
  return initialTurns.flatMap((turn, index): RenderMessage[] => {
    const key = `persisted-${turn.id}-${index}`;
    if (turn.role === "assistant") {
      const failed = persistedTurnFailed(turn);
      return [{
        role: "assistant",
        text: failed ? "" : turn.text ?? "",
        renderKey: key,
        state: turn.status === "completed" || failed ? "settled" : "thinking",
        completedReceipt: completedReceiptFromPersisted(turn.receipt),
        failed,
        turnId: turn.id,
      }];
    }
    return [{
      role: "user",
      text: turn.text ?? "",
      renderKey: key,
      state: "settled",
    }];
  });
}

export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
  const {
    caseId,
    sessionId,
    readonly,
    shouldStartOpening,
    initialTurns,
    models,
    selectedModelId,
    onSelectModel,
    onMessagesChange,
    onCompleted,
    onPendingChange,
    onProfileIncomplete,
    onSaved,
    onStartConsultation,
    pendingConsultationQuestion,
    onRestart,
    headerSlot,
  } = props;

  const [messages, setMessages] = useState<RenderMessage[]>(() => messagesFromTurns(initialTurns));
  const [draft, setDraft] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");
  const [savedTime, setSavedTime] = useState<string | null>(null);
  const [savedStatus, setSavedStatus] = useState<"accepted" | "confirmed" | null>(null);
  const [candidateResult, setCandidateResult] = useState<CandidateResult>(null);
  const [acceptingCandidateId, setAcceptingCandidateId] = useState<string | null>(null);
  const [feedback, setFeedback] = useState<Record<string, "up" | "down" | undefined>>({});
  const [copiedMessageKey, setCopiedMessageKey] = useState<string | null>(null);
  const [regeneratingMessageKey, setRegeneratingMessageKey] = useState<string | null>(null);
  const conversation = useRef<HTMLElement>(null);
  const workspace = useRef<HTMLDivElement>(null);
  const composer = useRef<HTMLTextAreaElement>(null);
  const keyCounter = useRef(0);
  const openingStarted = useRef(false);
  const previousBoardResult = useRef<CandidateResult>(null);
  const runAbort = useRef<AbortController | null>(null);
  const [compactBoard, setCompactBoard] = useState(false);
  const [boardOpen, setBoardOpen] = useState(false);
  const [boardDiff, setBoardDiff] = useState(() => diffRectificationBoard(null, null));
  const boardId = useId();
  const boardTitleId = useId();

  useLayoutEffect(() => {
    const query = window.matchMedia(`(max-width: ${RECTIFICATION_BOARD_SPLIT_MIN_PX - 1}px)`);
    const update = () => {
      const nextCompact = query.matches;
      setCompactBoard(nextCompact);
      if (!nextCompact) setBoardOpen(false);
    };
    update();
    query.addEventListener("change", update);
    return () => query.removeEventListener("change", update);
  }, []);

  useEffect(() => {
    setBoardDiff(diffRectificationBoard(previousBoardResult.current, candidateResult));
    previousBoardResult.current = candidateResult;
  }, [candidateResult]);

  const closeBoard = useCallback(() => setBoardOpen(false), []);
  const toggleBoard = useCallback(() => setBoardOpen((current) => !current), []);

  const stopRun = useCallback(() => {
    runAbort.current?.abort();
  }, []);

  const setPending = useCallback((value: boolean) => {
    setBusy(value);
    onPendingChange?.(value);
  }, [onPendingChange]);

  useEffect(() => {
    const container = conversation.current;
    if (!container) return;
    const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    container.scrollTo({
      top: container.scrollHeight,
      behavior: busy || reduceMotion ? "auto" : "smooth",
    });
  }, [busy, candidateResult, error, messages, savedTime]);

  // Candidate snapshot comes from the persisted Candidate Snapshot API, never
  // from parsing agent text or hidden sentinels.
  const loadCandidate = useCallback(async (): Promise<CandidateResult> => {
    try {
      const response = await fetch(
        `/api/rectification/cases/${encodeURIComponent(caseId)}?sessionId=${encodeURIComponent(sessionId)}`,
        { cache: "no-store" },
      );
      if (!response.ok) return null;
      const payload = await response.json().catch(() => null);
      return parseRectificationCandidateResult(payload?.latest_result);
    } catch {
      return null;
    }
  }, [caseId, sessionId]);

  useEffect(() => {
    let active = true;
    void loadCandidate().then((result) => {
      if (active) setCandidateResult(result);
    });
    return () => { active = false; };
  }, [loadCandidate]);

  const send = useCallback(async (action: "opening" | "message", messageText: string) => {
    const trimmed = action === "message" ? messageText.trim() : "";
    if ((action === "message" && !trimmed) || busy || readonly) return;
    setError("");
    setPending(true);

    keyCounter.current += 1;
    const requestId = globalThis.crypto.randomUUID();
    const turnKey = keyCounter.current;
    const userRenderKey = `v9-user-${turnKey}`;
    const assistantRenderKey = `v9-assistant-${turnKey}`;

    setMessages((current) => [
      ...current,
      ...(action === "message"
        ? [{ role: "user", text: trimmed, renderKey: userRenderKey, state: "settled" } satisfies RenderMessage]
        : []),
      { role: "assistant", text: "", renderKey: assistantRenderKey, state: "thinking" },
    ]);
    setDraft("");

    let raw = "";
    let activityReceiptState = createRectificationActivityReceiptState();
    let completedReceipt = receiptFromRectificationActivityState(activityReceiptState);
    let completedTurnId: string | undefined;
    const abortController = new AbortController();
    runAbort.current = abortController;
    try {
      const response = await fetch("/api/rectification/agent", {
        method: "POST",
        headers: { "content-type": "application/json" },
        signal: abortController.signal,
        body: JSON.stringify({
          caseId,
          sessionId,
          requestId,
          action,
          modelId: selectedModelId,
          ...(action === "message" ? { message: trimmed } : {}),
        }),
      });
      if (!response.ok) {
        const payload = await response.json().catch(() => null);
        const message = payload?.message || payload?.error || `请求失败（${response.status}）`;
        setMessages((current) => current.filter((message) => message.renderKey !== assistantRenderKey));
        if (payload?.code === "profile_incomplete") {
          onProfileIncomplete?.();
          return;
        }
        if (response.status === 402) {
          window.location.assign(membershipHref("rectification"));
          return;
        }
        if (response.status === 401) setError("请先登录。");
        else setError(message);
        return;
      }
      if (!response.body) {
        setMessages((current) => current.filter((message) => message.renderKey !== assistantRenderKey));
        setError("服务暂时不可用，请稍后再试。");
        return;
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = "";
      let completed = false;
      let streamFailed = false;
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split("\n");
        buffer = lines.pop() ?? "";
        for (const line of lines) {
          if (!line.trim()) continue;
          let event: {
            type?: unknown;
            status?: unknown;
            text?: unknown;
            message?: unknown;
            tool?: unknown;
            methods?: unknown;
            turnId?: unknown;
          };
          try {
            event = JSON.parse(line) as typeof event;
          } catch {
            continue;
          }
          if (typeof event.type !== "string") continue;
          if (event.type === "answer.delta" && typeof event.text === "string") {
            raw += event.text;
            const parsed = parseAgentReply(raw);
            setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
              ? { ...message, text: parsed.text, state: "streaming" }
              : message));
          } else if (event.type === "attempt.reset") {
            raw = "";
            activityReceiptState = createRectificationActivityReceiptState();
            completedReceipt = receiptFromRectificationActivityState(activityReceiptState);
            completedTurnId = undefined;
            setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
              ? { ...message, text: "", state: "thinking", completedReceipt: undefined, failed: false, turnId: undefined }
              : message));
          } else if (event.type === "run.failed") {
            streamFailed = true;
          } else if (event.type === "error") {
            streamFailed = true;
            setError(typeof event.message === "string" ? event.message : "生时校正暂时不可用，请稍后再试。");
          } else if (event.type === "run.completed") {
            completed = true;
            if (typeof event.turnId === "string") completedTurnId = event.turnId;
          } else if (event.type === "tool.activity") {
            const tool = isPublicRectificationTool(event.tool) ? event.tool : null;
            if (!tool) continue;
            if (event.status === "started") continue;
            if (event.status !== "completed" && event.status !== "failed") continue;
            activityReceiptState = reduceRectificationActivityReceipt(activityReceiptState, {
              tool,
              status: event.status,
              methods: event.status === "completed" && Array.isArray(event.methods)
                ? event.methods.filter(isPublicRectificationMethod)
                : [],
            });
            completedReceipt = receiptFromRectificationActivityState(activityReceiptState);
          }
        }
      }

      const parsed = parseAgentReply(raw);
      const succeeded = completed && !streamFailed && Boolean(parsed.text);
      setMessages((current) => current.flatMap((message): RenderMessage[] => {
        if (message.renderKey !== assistantRenderKey) return [message];
        if (succeeded) {
          return [{
            ...message,
            text: parsed.text,
            state: "settled",
            completedReceipt,
            failed: false,
            turnId: completedTurnId,
          }];
        }
        if (streamFailed || hasActivityReceipt(completedReceipt)) {
          return [{
            ...message,
            text: "",
            state: "settled",
            completedReceipt,
            failed: true,
          }];
        }
        return [];
      }));
      if (!succeeded && completedReceipt.failedTool) {
        setError(completedReceipt.failedTool === "rectification-compare-candidates"
          ? "候选比较未完成，当前进度已保留。"
          : "本轮处理未完成，当前进度已保留。请稍后再试。");
      }
      if (succeeded) {
        onMessagesChange?.([
          ...(action === "message" ? [{ role: "user" as const, text: trimmed }] : []),
          { role: "assistant", text: parsed.text },
        ]);
        onCompleted?.();
        await loadCandidate().then((result) => {
          if (result) setCandidateResult(result);
        });
      }
    } catch (caught) {
      const aborted = caught instanceof DOMException
        ? caught.name === "AbortError"
        : caught instanceof Error && caught.name === "AbortError";
      if (aborted) {
        const parsed = parseAgentReply(raw);
        setMessages((current) => current.flatMap((message): RenderMessage[] => {
          if (message.renderKey !== assistantRenderKey) return [message];
          if (parsed.text) {
            return [{
              ...message,
              text: parsed.text,
              state: "settled",
              completedReceipt,
              failed: false,
              turnId: completedTurnId,
            }];
          }
          return [];
        }));
        return;
      }
      setError("生时校正暂时不可用，请稍后再试。");
      setMessages((current) => current.flatMap((message): RenderMessage[] => {
        if (message.renderKey !== assistantRenderKey) return [message];
        return hasActivityReceipt(completedReceipt)
          ? [{
            ...message,
            text: "",
            state: "settled",
            completedReceipt,
            failed: true,
          }]
          : [];
      }));
    } finally {
      if (runAbort.current === abortController) runAbort.current = null;
      setPending(false);
    }
  }, [busy, caseId, loadCandidate, onCompleted, onMessagesChange, onProfileIncomplete, readonly, selectedModelId, sessionId, setPending]);

  useEffect(() => {
    if (readonly || openingStarted.current || !shouldStartOpening) return;
    openingStarted.current = true;
    void send("opening", "");
  }, [readonly, send, shouldStartOpening]);

  const acceptCandidate = useCallback(async (candidateId: string) => {
    if (!candidateResult || acceptingCandidateId || readonly) return;
    setError("");
    setAcceptingCandidateId(candidateId);
    try {
      const response = await fetch(
        `/api/rectification/cases/${encodeURIComponent(caseId)}/candidates/accept`,
        {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({
            sessionId,
            resultId: candidateResult.resultId,
            candidateId,
            requestId: globalThis.crypto.randomUUID(),
          }),
        },
      );
      const payload = await response.json().catch(() => null);
      if (!response.ok || payload?.ok !== true || payload?.status !== "accepted") {
        throw new Error(payload?.error || payload?.message || "暂时无法采用该候选时间");
      }
      setCandidateResult((current) => {
        if (!current) return current;
        const selectedTime = typeof payload.saved_time === "string" ? payload.saved_time.slice(0, 5) : current.selectedTime;
        const next = {
          ...current,
          selectedTime,
          selectionKind: "user_accepted" as const,
        };
        const houseTable = workingRectificationHouseTable(next);
        const recastMeaning = natalRecastMeaning(houseTable);
        return {
          ...next,
          houseTable: houseTable ?? current.houseTable,
          natalRecast: houseTable && recastMeaning
            ? {
                time: houseTable.time,
                lagna: houseTable.lagna,
                user_meaning: recastMeaning,
                unique_minute_claim: false as const,
                confirmation_allowed: false as const,
              }
            : current.natalRecast,
        };
      });
      setSavedTime(payload.saved_time);
      setSavedStatus("accepted");
      onSaved?.(payload.saved_time, "accepted");
      onCompleted?.();
    } catch (caught) {
      setError(caught instanceof Error ? caught.message : "暂时无法采用该候选时间");
    } finally {
      setAcceptingCandidateId(null);
    }
  }, [acceptingCandidateId, candidateResult, caseId, onCompleted, onSaved, readonly, sessionId]);

  async function copyMessage(message: RenderMessage) {
    try {
      await navigator.clipboard.writeText(message.text);
      setCopiedMessageKey(message.renderKey);
      window.setTimeout(() => setCopiedMessageKey((current) => (
        current === message.renderKey ? null : current
      )), 1_500);
    } catch {
      // Clipboard permission failures must not interrupt the conversation.
    }
  }

  async function regenerateMessage(message: RenderMessage) {
    if (!message.turnId || regeneratingMessageKey || busy || readonly) return;
    setError("");
    setRegeneratingMessageKey(message.renderKey);
    const previousText = message.text;
    try {
      const response = await fetch(
        `/api/rectification/cases/${encodeURIComponent(caseId)}/turns/${encodeURIComponent(message.turnId)}/regenerate`,
        {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({
            sessionId,
            requestId: globalThis.crypto.randomUUID(),
          }),
        },
      );
      const payload = await response.json().catch(() => null);
      if (!response.ok || payload?.ok !== true || typeof payload.assistantMessage !== "string") {
        throw new Error(payload?.message || payload?.error || "暂时无法重新生成回答");
      }
      setMessages((current) => current.map((item) => item.renderKey === message.renderKey
        ? { ...item, text: payload.assistantMessage, state: "settled" }
        : item));
    } catch (caught) {
      setMessages((current) => current.map((item) => item.renderKey === message.renderKey
        ? { ...item, text: previousText, state: "settled" }
        : item));
      setError(caught instanceof Error ? caught.message : "暂时无法重新生成回答");
    } finally {
      setRegeneratingMessageKey((current) => current === message.renderKey ? null : current);
    }
  }

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    await send("message", draft);
  }

  const latestRegeneratableKey = [...messages]
    .reverse()
    .find((message) => (
      message.role === "assistant"
      && message.state === "settled"
      && !message.failed
      && Boolean(message.turnId)
      && Boolean(message.text)
    ))?.renderKey;
  const latestOfferMessage = [...messages]
    .reverse()
    .find((message) => (
      message.role === "assistant"
      && message.state === "settled"
      && !message.failed
      && Boolean(message.text)
    ));
  const showSelectionCards = Boolean(
    candidateResult?.selectionAllowed
    && latestOfferMessage
    && turnOfferedSelection(latestOfferMessage)
    && !busy
    && regeneratingMessageKey === null,
  );
  const selectionCardMessageKey = showSelectionCards && latestOfferMessage
    ? latestOfferMessage.renderKey
    : undefined;
  const canSend = !busy && !readonly && !regeneratingMessageKey;

  const boardPeek = compactBoard && !boardOpen ? (
    <RectificationBoardPeek
      result={candidateResult}
      expanded={boardOpen}
      boardId={boardId}
      onOpen={toggleBoard}
    />
  ) : null;

  return (
    <div
      ref={workspace}
      className={`rectification-workspace${compactBoard ? " is-compact" : ""}${boardOpen ? " is-board-open" : ""}`}
    >
      {headerSlot && boardPeek ? createPortal(boardPeek, headerSlot) : null}
      <div className="rectification-workspace__chat" inert={compactBoard && boardOpen ? true : undefined}>
        <section ref={conversation} className="conversation is-rectification" aria-label="生时校正对话" aria-busy={busy || regeneratingMessageKey !== null}>
          <div className="message-list">
          {pendingConsultationQuestion?.trim() && (
            <p className="rectification-pending-note">
              先陪你核对出生时间范围，之后会回到你原来的问题：“{pendingConsultationQuestion.trim()}”
            </p>
          )}
          {messages.map((message) => {
            const showActions = message.role === "assistant"
              && message.state === "settled"
              && !message.failed
              && Boolean(message.text);
            const regenerating = regeneratingMessageKey === message.renderKey;
            const canRegenerate = message.renderKey === latestRegeneratableKey
              && !busy
              && !readonly
              && regeneratingMessageKey === null;
            const displayedMessage = regenerating
              ? { ...message, text: "", state: "thinking" as const }
              : message;
            const vargaSentence = message.state === "settled" && !message.failed
              ? vargaSentenceFromMethods(message.completedReceipt?.methods)
              : null;
            return (
              <div key={message.renderKey} className="rectification-message-wrap rectification-message-entry">
                {message.state === "settled" && message.failed && (
                  <p className="rectification-activity-failure" role="status">
                    本轮处理未完成，已保留服务端记录的执行进度。
                  </p>
                )}
                {(!message.failed || Boolean(displayedMessage.text) || regenerating) && (
                  <ChatMessageRow
                    message={displayedMessage}
                    showActivity={displayedMessage.state === "thinking"}
                    vargaSentence={vargaSentence}
                  />
                )}
                {showActions && !regenerating && (
                  <ChatMessageActions
                    feedback={feedback[message.renderKey]}
                    copied={copiedMessageKey === message.renderKey}
                    canRegenerate={canRegenerate}
                    onFeedback={(requested) => setFeedback((current) => ({
                      ...current,
                      [message.renderKey]: toggleRectificationFeedback(current[message.renderKey], requested),
                    }))}
                    onCopy={() => void copyMessage(message)}
                    onRegenerate={() => void regenerateMessage(message)}
                  />
                )}
                {showSelectionCards && message.renderKey === selectionCardMessageKey && candidateResult && (
                  <RectificationCandidateCards
                    result={candidateResult}
                    acceptingCandidateId={acceptingCandidateId}
                    readonly={readonly}
                    onAccept={(candidateId) => void acceptCandidate(candidateId)}
                  />
                )}
              </div>
            );
          })}
          {savedTime && (
            <div className="rectification-saved-wrap">
              <p className="rectification-saved" role="status">
                {savedStatus === "confirmed" ? "已确认校正时间" : "当前排盘时间（代表性候选，还不能确认唯一分钟）"}：{savedTime}。后续排盘将使用该时间；你仍可继续补充事件或改选其他候选。
              </p>
              {savedStatus === "accepted" && candidateResult && (
                <RectificationOosPanel prompts={candidateResult.oosBlindPrompts} />
              )}
              {savedStatus === "accepted" && onStartConsultation && (
                <div className="rectification-consult-handoff">
                  <Button type="button" onClick={onStartConsultation}>
                    用这个时间看盘
                  </Button>
                </div>
              )}
            </div>
          )}
          {error && <p className="error-message" role="alert">{error}</p>}
          {readonly && (
            <div className="rectification-terminal-actions">
              <p className="rectification-terminal-note">该校正已结束，只能查看历史。</p>
              <Button type="button" onClick={onRestart}>再次校正</Button>
            </div>
          )}
        </div>
      </section>

        <div className="composer-wrap">
          <form className="composer" onSubmit={submit}>
          <Textarea
            ref={composer}
            aria-label={readonly ? "该校正已结束，只能查看历史" : "继续描述你的经历或回答"}
            value={draft}
            disabled={!canSend}
            placeholder={readonly
              ? "该校正已结束，只能查看历史；需要再次校正请新建。"
              : "继续说你记得的人生经历，或回答刚才的问题…"}
            onChange={(event) => setDraft(event.target.value)}
            onKeyDown={(event) => {
              if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
                event.preventDefault();
                event.currentTarget.form?.requestSubmit();
              }
            }}
          />
          {busy ? (
            <Button
              className="composer-stop"
              aria-label="停止回答"
              title="停止当前推理，已生成内容会保留"
              size="icon"
              type="button"
              onClick={stopRun}
            >
              <Square aria-hidden="true" />
            </Button>
          ) : (
            <Button aria-label="发送" disabled={!draft.trim() || !canSend} size="icon" type="submit">
              <ArrowUp aria-hidden="true" />
            </Button>
          )}
        </form>

        <div className="composer-footer">
          <ModelSelector
            models={models}
            selectedModelId={selectedModelId}
            disabled={busy || readonly}
            onSelect={onSelectModel}
          />
        </div>
        </div>
      </div>
      <RectificationBoard
        result={candidateResult}
        savedStatus={savedStatus}
        diff={boardDiff}
        compact={compactBoard}
        open={boardOpen}
        boardId={boardId}
        titleId={boardTitleId}
        onClose={closeBoard}
      />
    </div>
  );
}
