import type {
  CanonicalContentBlock,
  CanonicalFinishReason,
  CanonicalMessage,
  CanonicalModelEvent,
  CanonicalTextBlock,
  CanonicalThinkingBlock,
  CanonicalToolCall,
  CanonicalUsage,
} from "../protocol/canonical.js";
import type { CanonicalModelError } from "../protocol/errors.js";
import { extractTextToolCalls } from "./parseTextToolCalls.js";

export type ModelMessageAssemblerState = {
  content: CanonicalContentBlock[];
  textBuffer: string;
  thinkingBuffer: string;
  thinkingSignature?: string;
  usage: CanonicalUsage;
  finishReason?: CanonicalFinishReason;
  error?: CanonicalModelError;
  toolCalls: CanonicalToolCall[];
  hasRepairedToolCalls?: boolean;
};

export type AssembledAssistantMessage = {
  message: CanonicalMessage;
  finishReason: CanonicalFinishReason;
  usage?: CanonicalUsage;
  toolCalls: CanonicalToolCall[];
  error?: CanonicalModelError;
  hasRepairedToolCalls?: boolean;
};

export function createModelMessageAssemblerState(): ModelMessageAssemblerState {
  return {
    content: [],
    textBuffer: "",
    thinkingBuffer: "",
    usage: {},
    toolCalls: [],
  };
}

export function applyModelEventToAssembler(
  state: ModelMessageAssemblerState,
  event: CanonicalModelEvent,
): void {
  switch (event.type) {
    case "request_started":
    case "message_start":
    case "tool_call_start":
    case "tool_call_delta":
      return;
    case "text_delta":
      state.textBuffer += event.text;
      return;
    case "thinking_delta":
      state.thinkingBuffer += event.text;
      if (event.signature !== undefined && event.signature.length > 0) {
        state.thinkingSignature = event.signature;
      }
      return;
    case "tool_call_end":
      flushTextBuffers(state);
      state.toolCalls.push(event.toolCall);
      state.content.push({
        type: "tool_call",
        ...event.toolCall,
      });
      if (event.wasRepaired) {
        state.hasRepairedToolCalls = true;
      }
      return;
    case "message_end":
      flushTextBuffers(state);
      state.finishReason = event.finishReason;
      return;
    case "usage":
      state.usage = mergeUsage(state.usage, event.usage);
      return;
    case "error":
      flushTextBuffers(state);
      state.error = event.error;
      state.finishReason = "error";
      return;
  }
}

export function assembleAssistantMessage(state: ModelMessageAssemblerState): AssembledAssistantMessage {
  flushTextBuffers(state);

  if (state.toolCalls.length === 0) {
    const textIdx = state.content.findIndex(
      (b): b is CanonicalTextBlock => b.type === "text" && hasTextToolCallMarker(b.text),
    );
    if (textIdx >= 0) {
      const textBlock = state.content[textIdx] as CanonicalTextBlock;
      const { toolCalls, remainingText } = extractTextToolCalls(textBlock.text);
      if (toolCalls.length > 0) {
        console.log(`[text-tool-call-fallback] Extracted ${toolCalls.length} tool call(s) from assistant text`);
        if (remainingText.length > 0) {
          (state.content[textIdx] as CanonicalTextBlock).text = remainingText;
        } else {
          state.content.splice(textIdx, 1);
        }
        for (const tc of toolCalls) {
          state.content.push({ type: "tool_call", ...tc });
          state.toolCalls.push(tc);
        }
      }
    }
  }

  return {
    message: {
      role: "assistant",
      content: [...state.content],
    },
    finishReason: state.finishReason ?? (state.error ? "error" : "unknown"),
    usage: hasUsage(state.usage) ? state.usage : undefined,
    toolCalls: [...state.toolCalls],
    error: state.error,
    hasRepairedToolCalls: state.hasRepairedToolCalls,
  };
}

const TEXT_TOOL_CALL_MARKERS = [
  "<function=",
  "<tool_call>",
  "\uff5cDSML\uff5c",
  "[TOOL_CALLS]",
  "<|python_tag|>",
];

function hasTextToolCallMarker(text: string): boolean {
  return TEXT_TOOL_CALL_MARKERS.some((m) => text.includes(m));
}

function flushTextBuffers(state: ModelMessageAssemblerState): void {
  if (state.thinkingBuffer.length > 0 || state.thinkingSignature !== undefined) {
    const block: CanonicalThinkingBlock = {
      type: "thinking",
      text: state.thinkingBuffer,
    };
    if (state.thinkingSignature !== undefined) {
      block.signature = state.thinkingSignature;
    }
    state.content.push(block);
    state.thinkingBuffer = "";
    state.thinkingSignature = undefined;
  }

  if (state.textBuffer.length > 0) {
    state.content.push({
      type: "text",
      text: state.textBuffer,
    } satisfies CanonicalTextBlock);
    state.textBuffer = "";
  }
}

function mergeUsage(first: CanonicalUsage, second: CanonicalUsage): CanonicalUsage {
  return {
    inputTokens: add(first.inputTokens, second.inputTokens),
    outputTokens: add(first.outputTokens, second.outputTokens),
    cacheReadTokens: add(first.cacheReadTokens, second.cacheReadTokens),
    cacheWriteTokens: add(first.cacheWriteTokens, second.cacheWriteTokens),
    totalTokens: add(first.totalTokens, second.totalTokens),
  };
}

function add(first: number | undefined, second: number | undefined): number | undefined {
  if (first === undefined && second === undefined) {
    return undefined;
  }
  return (first ?? 0) + (second ?? 0);
}

function hasUsage(usage: CanonicalUsage): boolean {
  return Object.values(usage).some((value) => value !== undefined);
}
