import type { Message, ToolResult } from "#cli/core/model";
import type { StoreState } from "./state";
import { formatAgentErrorForUi, formatAgentErrorHintForUi } from "#cli/core/store/errors";
import { isTerminalStatusText } from "#cli/i18n";
import { createFileRevertHandlers } from "#cli/core/store/file-reverts";
import { scheduleToolResultHighlight as scheduleToolResultHighlightImpl } from "./highlight";
import { scheduleAssistantMarkdownCodeHighlight as scheduleAssistantMarkdownCodeHighlightImpl } from "./markdown-code-highlight";
import { createMessageMutations } from "./message-mutations";
import { scheduleToolCallReadHighlightByCallId as scheduleToolCallReadHighlightByCallIdImpl } from "./read-highlight";

type DebugLog = (eventName: string, payload?: unknown) => void;

const ASSISTANT_MARKDOWN_COALESCE_MS = 16;

type AssistantMessage = Extract<Message, { role: "assistant" }>;
type StreamingMarkdownPart = Extract<AssistantMessage["parts"][number], { type: "markdown" }> &
  Readonly<{
    __appendSeq?: number;
    __lastAppendDelta?: string;
  }>;

function unrefTimer(timer: ReturnType<typeof setTimeout> | null): void {
  if (timer && typeof timer === "object" && "unref" in timer && typeof timer.unref === "function") {
    timer.unref();
  }
}

export function createMessagePipeline(
  opts: Readonly<{
    state: StoreState;
    debugLog: DebugLog;
  }>,
) {
  const { state, debugLog } = opts;

  const messageMutations = createMessageMutations(state);
  const {
    pushMessage,
    setSessionHistoryDirty,
    replaceMessage,
    updateAssistant,
    setAssistantStatus,
    setAssistantThinking,
    startNewThinkingBlock,
    endThinkingBlock,
    appendAssistantMarkdown: appendAssistantMarkdownImpl,
    appendInfoPart,
    upsertSubagentInfo,
    tickSubagentFrame,
    appendAssistantLlmMeta,
    updateAssistantLlmMeta,
    upsertToolCallDelta,
    upsertToolCallEnd,
    upsertToolOutputStart,
    appendToolOutputDelta,
    attachToolResultPart,
    insertApprovalPart,
    toggleChecklist,
    toggleBlockCollapsed,
    revertFromMessage: revertFromMessageBase,
    restoreRevertedMessages: restoreRevertedMessagesBase,
    clearRevertedMessages: clearRevertedMessagesBase,
    hasRevertedMessages,
    revertedMessageCount,
    upsertChecklistPartFromDimSdk,
    rebuildUserHistory,
  } = messageMutations;

  const fileRevertHandlers = createFileRevertHandlers({
    state,
    debugLog,
    revertFromMessageBase,
    restoreRevertedMessagesBase,
    clearRevertedMessagesBase,
  });
  const {
    revertFromMessage,
    revertFilesFromMessage,
    restoreRevertedMessages,
    clearRevertedMessages,
  } = fileRevertHandlers;

  type PendingAssistantThinking = {
    content: string;
    partId: string;
  };

  type PendingAssistantFrame = {
    markdownDelta: string;
    statusText: string | null;
    thinkingUpdates: Map<string, PendingAssistantThinking>;
    timer: ReturnType<typeof setTimeout> | null;
  };

  const pendingAssistantFrameById = new Map<string, PendingAssistantFrame>();
  const lastAssistantMarkdownFlushAt = new Map<string, number>();

  function commitAssistantMarkdown(assistantId: string, delta: string): void {
    appendAssistantMarkdownImpl(assistantId, delta);
    lastAssistantMarkdownFlushAt.set(assistantId, Date.now());
    scheduleAssistantMarkdownCodeHighlightImpl({
      state,
      assistantId,
      updateAssistant,
    });
  }

  function pushAssistantMarkdownPartImpl(assistantId: string, markdown: string): void {
    const normalizedAssistantId = String(assistantId ?? "").trim();
    const nextMarkdown = String(markdown ?? "");
    if (!normalizedAssistantId || !nextMarkdown) return;

    updateAssistant(normalizedAssistantId, (m) => {
      const parts = m.parts.slice();
      parts.push({
        type: "markdown",
        markdown: nextMarkdown,
        __appendSeq: 1,
        __lastAppendDelta: nextMarkdown,
      } satisfies StreamingMarkdownPart);
      return { ...m, parts };
    });
    lastAssistantMarkdownFlushAt.set(normalizedAssistantId, Date.now());
    scheduleAssistantMarkdownCodeHighlightImpl({
      state,
      assistantId: normalizedAssistantId,
      updateAssistant,
    });
  }

  function commitAssistantStatus(assistantId: string, text: string): void {
    setAssistantStatus(assistantId, text);
    lastAssistantMarkdownFlushAt.set(assistantId, Date.now());
  }

  function commitAssistantThinking(assistantId: string, content: string, partId: string): void {
    setAssistantThinking(assistantId, content, partId);
    lastAssistantMarkdownFlushAt.set(assistantId, Date.now());
  }

  function flushPendingAssistantFrame(assistantId: string): void {
    const pending = pendingAssistantFrameById.get(assistantId);
    if (!pending) return;
    pendingAssistantFrameById.delete(assistantId);
    if (pending.timer) clearTimeout(pending.timer);
    if (pending.markdownDelta) commitAssistantMarkdown(assistantId, pending.markdownDelta);
    for (const update of pending.thinkingUpdates.values())
      commitAssistantThinking(assistantId, update.content, update.partId);
    if (pending.statusText) commitAssistantStatus(assistantId, pending.statusText);
  }

  function flushAllPendingAssistantFrames(): void {
    for (const assistantId of pendingAssistantFrameById.keys())
      flushPendingAssistantFrame(assistantId);
  }

  function schedulePendingAssistantFrameFlush(assistantId: string): void {
    const pending = pendingAssistantFrameById.get(assistantId);
    if (!pending) return;
    if (pending.timer) return;

    const lastFlushAt = lastAssistantMarkdownFlushAt.get(assistantId) ?? 0;
    const delayMs = Math.max(0, ASSISTANT_MARKDOWN_COALESCE_MS - (Date.now() - lastFlushAt));
    pending.timer = setTimeout(() => {
      flushPendingAssistantFrame(assistantId);
    }, delayMs);
    unrefTimer(pending.timer);
  }

  function getOrCreatePendingAssistantFrame(assistantId: string): PendingAssistantFrame {
    const existing = pendingAssistantFrameById.get(assistantId);
    if (existing) return existing;
    const next: PendingAssistantFrame = {
      markdownDelta: "",
      statusText: null,
      thinkingUpdates: new Map(),
      timer: null,
    };
    pendingAssistantFrameById.set(assistantId, next);
    return next;
  }

  function withAssistantFlush<T extends unknown[], R>(
    assistantIdIndex: number,
    fn: (...args: T) => R,
  ): (...args: T) => R {
    return (...args: T) => {
      const assistantId = String(args[assistantIdIndex] ?? "").trim();
      if (assistantId) flushPendingAssistantFrame(assistantId);
      return fn(...args);
    };
  }

  function withGlobalAssistantFlush<T extends unknown[], R>(
    fn: (...args: T) => R,
  ): (...args: T) => R {
    return (...args: T) => {
      flushAllPendingAssistantFrames();
      return fn(...args);
    };
  }

  function replaceMessages(next: readonly Message[]): void {
    flushAllPendingAssistantFrames();
    state.messages.splice(0, state.messages.length);
    for (const message of next) pushMessage(message);
    state.sessionHistoryDirty = false;
    rebuildUserHistory();
  }

  function appendAssistantMarkdown(assistantId: string, delta: string): void {
    const nextDelta = String(delta ?? "");
    if (!nextDelta) return;

    const normalizedAssistantId = String(assistantId ?? "").trim();
    if (!normalizedAssistantId) return;

    const pending = pendingAssistantFrameById.get(normalizedAssistantId);
    const lastFlushAt = lastAssistantMarkdownFlushAt.get(normalizedAssistantId) ?? 0;
    if (!pending && Date.now() - lastFlushAt >= ASSISTANT_MARKDOWN_COALESCE_MS) {
      commitAssistantMarkdown(normalizedAssistantId, nextDelta);
      return;
    }

    if (pending) {
      pending.markdownDelta += nextDelta;
      schedulePendingAssistantFrameFlush(normalizedAssistantId);
      return;
    }

    const nextPending = getOrCreatePendingAssistantFrame(normalizedAssistantId);
    nextPending.markdownDelta = nextDelta;
    schedulePendingAssistantFrameFlush(normalizedAssistantId);
  }

  function setAssistantStatusBuffered(messageId: string, text: string): void {
    const normalizedMessageId = String(messageId ?? "").trim();
    if (!normalizedMessageId) return;
    if (isTerminalStatusText(text)) {
      flushPendingAssistantFrame(normalizedMessageId);
      commitAssistantStatus(normalizedMessageId, text);
      return;
    }
    const pending = getOrCreatePendingAssistantFrame(normalizedMessageId);
    pending.statusText = text;
    schedulePendingAssistantFrameFlush(normalizedMessageId);
  }

  function setAssistantThinkingBuffered(
    assistantId: string,
    content: string,
    partId: string,
  ): void {
    const normalizedAssistantId = String(assistantId ?? "").trim();
    const normalizedPartId = String(partId ?? "").trim();
    if (!normalizedAssistantId || !normalizedPartId) return;
    const pending = getOrCreatePendingAssistantFrame(normalizedAssistantId);
    pending.thinkingUpdates.set(normalizedPartId, {
      content,
      partId: normalizedPartId,
    });
    schedulePendingAssistantFrameFlush(normalizedAssistantId);
  }

  function flushPendingAssistantOutput(assistantId?: string): void {
    const normalizedAssistantId = String(assistantId ?? "").trim();
    if (normalizedAssistantId) {
      flushPendingAssistantFrame(normalizedAssistantId);
      return;
    }
    flushAllPendingAssistantFrames();
  }

  function ensureErrorVisibleInTranscript(assistantId: string, errorMeta: unknown): void {
    flushPendingAssistantOutput(assistantId);
    const msg = state.messages.find((m) => m.id === assistantId);
    if (!msg || msg.role !== "assistant") return;

    const markdown = msg.parts.flatMap((p) =>
      p.type === "markdown" ? [String(p.markdown ?? "")] : [],
    );
    const joined = markdown.join("\n");

    // Check if there's already a hint appended
    const alreadyHasHint = /\nHint:/.test(joined);
    if (alreadyHasHint) return;

    // Get the environment for hint generation
    const env = state.context?.env;

    // If errorMeta is a boolean (from done event metadata), use the message content for hint generation
    let errText = "";
    let hint: string | null = null;

    if (typeof errorMeta === "boolean" && errorMeta) {
      // Error is indicated by boolean true, generate hint based on message content
      hint = formatAgentErrorHintForUi(joined, env);
    } else if (errorMeta) {
      errText = formatAgentErrorForUi(errorMeta);
      // Avoid duplicate error blocks if the agent already emitted one (or included the exact error text).
      const alreadyHasErrorHeading = /(?:^|\n)#+\s*error\b/i.test(joined);
      const alreadyHasErrorText = errText.trim() ? joined.includes(errText) : false;

      if (!alreadyHasErrorHeading && !alreadyHasErrorText) {
        hint = formatAgentErrorHintForUi(errText, env);
        // Render synthetic error blocks as their own markdown part so an
        // interrupted stream cannot corrupt the already-rendered markdown.
        pushAssistantMarkdownPartImpl(
          assistantId,
          ["# Error", "", "```text", errText, "```", ...(hint ? ["", hint] : [])].join("\n"),
        );
        return;
      }
      hint = formatAgentErrorHintForUi(errText, env);
    }

    // If there's already an error block, just append the hint
    if (hint) {
      appendAssistantMarkdown(assistantId, `\n\n${hint}`);
    }
  }

  function scheduleToolResultHighlight(assistantId: string, result: ToolResult): void {
    scheduleToolResultHighlightImpl({
      state,
      assistantId,
      result,
      updateAssistant,
    });
    const callId = String(result?.toolCallId ?? "").trim();
    if (!callId) return;
    if (state.selectedToolCallId !== callId) return;
    const mode = state.expandedBashOutputs.has(callId) ? "expanded" : "collapsed";
    scheduleToolCallReadHighlightByCallIdImpl({
      state,
      callId,
      mode,
      updateAssistant,
    });
  }

  function upsertToolCallEndWithHighlights(
    assistantId: string,
    callId: string,
    toolName: string,
    args: Record<string, unknown>,
    thoughtSignature?: string,
    meta?: Readonly<{ subagentId?: string }>,
  ): void {
    flushPendingAssistantOutput(assistantId);
    upsertToolCallEnd(assistantId, callId, toolName, args, thoughtSignature, meta);
    // Defer expensive diff highlighting (shiki) until the user actually selects the tool call.
    // This keeps the UI responsive during long runs with many tool calls.
  }

  const pushMessageWithFlush = withGlobalAssistantFlush(pushMessage);
  const replaceMessageWithFlush = withGlobalAssistantFlush(replaceMessage);
  const updateAssistantWithFlush = withAssistantFlush(0, updateAssistant);
  const startNewThinkingBlockWithFlush = withAssistantFlush(0, startNewThinkingBlock);
  const endThinkingBlockWithFlush = withAssistantFlush(0, endThinkingBlock);
  const pushAssistantMarkdownPartWithFlush = withAssistantFlush(0, pushAssistantMarkdownPartImpl);
  const appendInfoPartWithFlush = withAssistantFlush(0, appendInfoPart);
  const upsertSubagentInfoWithFlush = withAssistantFlush(0, upsertSubagentInfo);
  const tickSubagentFrameWithFlush = withAssistantFlush(0, tickSubagentFrame);
  const appendAssistantLlmMetaWithFlush = withAssistantFlush(0, appendAssistantLlmMeta);
  const updateAssistantLlmMetaWithFlush = withAssistantFlush(0, updateAssistantLlmMeta);
  const upsertToolCallDeltaWithFlush = withAssistantFlush(0, upsertToolCallDelta);
  const upsertToolCallEndWithFlush = withAssistantFlush(0, upsertToolCallEnd);
  const upsertToolOutputStartWithFlush = withAssistantFlush(0, upsertToolOutputStart);
  const appendToolOutputDeltaWithFlush = withAssistantFlush(0, appendToolOutputDelta);
  const attachToolResultPartWithFlush = withAssistantFlush(0, attachToolResultPart);
  const insertApprovalPartWithFlush = withAssistantFlush(0, insertApprovalPart);
  const upsertChecklistPartFromDimSdkWithFlush = withAssistantFlush(
    0,
    upsertChecklistPartFromDimSdk,
  );
  const revertFromMessageWithFlush = withGlobalAssistantFlush(revertFromMessage);
  const revertFilesFromMessageWithFlush = withGlobalAssistantFlush(revertFilesFromMessage);
  const restoreRevertedMessagesWithFlush = withGlobalAssistantFlush(restoreRevertedMessages);
  const clearRevertedMessagesWithFlush = withGlobalAssistantFlush(clearRevertedMessages);

  return {
    pushMessage: pushMessageWithFlush,
    setSessionHistoryDirty,
    replaceMessage: replaceMessageWithFlush,
    updateAssistant: updateAssistantWithFlush,
    setAssistantStatus: setAssistantStatusBuffered,
    setAssistantThinking: setAssistantThinkingBuffered,
    startNewThinkingBlock: startNewThinkingBlockWithFlush,
    endThinkingBlock: endThinkingBlockWithFlush,
    appendAssistantMarkdown,
    pushAssistantMarkdownPart: pushAssistantMarkdownPartWithFlush,
    appendInfoPart: appendInfoPartWithFlush,
    upsertSubagentInfo: upsertSubagentInfoWithFlush,
    tickSubagentFrame: tickSubagentFrameWithFlush,
    appendAssistantLlmMeta: appendAssistantLlmMetaWithFlush,
    updateAssistantLlmMeta: updateAssistantLlmMetaWithFlush,
    upsertToolCallDelta: upsertToolCallDeltaWithFlush,
    upsertToolCallEnd: upsertToolCallEndWithFlush,
    upsertToolOutputStart: upsertToolOutputStartWithFlush,
    appendToolOutputDelta: appendToolOutputDeltaWithFlush,
    attachToolResultPart: attachToolResultPartWithFlush,
    insertApprovalPart: insertApprovalPartWithFlush,
    toggleChecklist,
    toggleBlockCollapsed,
    hasRevertedMessages,
    revertedMessageCount,
    upsertChecklistPartFromDimSdk: upsertChecklistPartFromDimSdkWithFlush,
    rebuildUserHistory,
    replaceMessages,
    ensureErrorVisibleInTranscript,
    flushPendingAssistantOutput,
    scheduleToolResultHighlight,
    upsertToolCallEndWithHighlights,
    revertFromMessage: revertFromMessageWithFlush,
    revertFilesFromMessage: revertFilesFromMessageWithFlush,
    restoreRevertedMessages: restoreRevertedMessagesWithFlush,
    clearRevertedMessages: clearRevertedMessagesWithFlush,
  };
}
