import { OpenAIRequestSchema, parseOpenAIResponse } from "../../../proxy/formats/openai/schemas";
import type { AnatomySegment } from "../anatomy/types";
import { countCharacters, estimateTokens } from "../anatomy/tokenEstimate";
import type { LogFormatAdapter } from "./types";
import { emptyRequestAnalysis, EMPTY_RESPONSE_ANALYSIS } from "./types";

type OpenAIMessageContent = string | ReadonlyArray<Record<string, unknown>> | null | undefined;

/** Flatten an OpenAI `content` field to its text representation. */
function contentToText(content: unknown): string {
  if (typeof content === "string") return content;
  if (!Array.isArray(content)) return "";
  const parts: string[] = [];
  for (const block of content) {
    if (block === null || typeof block !== "object") continue;
    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    const b = block as Record<string, unknown>;
    const type = b.type;
    if (type === "text" && typeof b.text === "string") {
      parts.push(b.text);
    } else if (type === "image_url") {
      parts.push("[image]");
    }
  }
  return parts.join("\n");
}

/** Flatten an OpenAI message to text — content plus optional tool calls plus optional reasoning. */
function messageToText(message: Record<string, unknown>): string {
  const parts: string[] = [];
  const text = contentToText(message.content);
  if (text.length > 0) parts.push(text);
  const reasoning =
    typeof message.reasoning_content === "string"
      ? message.reasoning_content
      : typeof message.thinking === "string"
        ? message.thinking
        : typeof message.think === "string"
          ? message.think
          : "";
  if (reasoning.length > 0) parts.push(reasoning);
  const toolCalls = message.tool_calls;
  if (Array.isArray(toolCalls)) {
    for (const call of toolCalls) {
      if (call === null || typeof call !== "object") continue;
      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
      const fn = (call as { function?: { name?: unknown; arguments?: unknown } }).function;
      if (fn === undefined) continue;
      const name = typeof fn.name === "string" ? fn.name : "";
      const args = typeof fn.arguments === "string" ? fn.arguments : "";
      parts.push(`${name} ${args}`.trim());
    }
  }
  return parts.join("\n");
}

/** Flatten an OpenAI `tools` array to its schema text. */
function toolsToText(tools: unknown): string {
  if (!Array.isArray(tools)) return "";
  const parts: string[] = [];
  for (const tool of tools) {
    if (tool === null || typeof tool !== "object") continue;
    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    const t = tool as {
      function?: { name?: unknown; description?: unknown; parameters?: unknown };
    };
    const fn = t.function;
    if (fn === undefined) continue;
    if (typeof fn.name === "string") parts.push(fn.name);
    if (typeof fn.description === "string") parts.push(fn.description);
    if (fn.parameters !== undefined) parts.push(JSON.stringify(fn.parameters));
  }
  return parts.join("\n");
}

function segment(
  role: AnatomySegment["role"],
  label: string,
  text: string,
  path: string,
): AnatomySegment {
  return {
    role,
    label,
    text,
    size: estimateTokens(text),
    characters: countCharacters(text),
    path,
  };
}

export const openAILogFormatAdapter: LogFormatAdapter = {
  format: "openai",

  analyzeRequest(rawBody) {
    if (rawBody === null) return emptyRequestAnalysis(rawBody);
    try {
      const result = OpenAIRequestSchema.safeParse(JSON.parse(rawBody));
      if (!result.success) return emptyRequestAnalysis(rawBody);
      return {
        parsed: result.data,
        comparisonValue: result.data,
        messageCount: result.data.messages.length,
        toolCount:
          result.data.tools !== undefined && result.data.tools.length > 0
            ? result.data.tools.length
            : null,
      };
    } catch {
      return emptyRequestAnalysis(rawBody);
    }
  },

  analyzeResponse(responseText) {
    if (responseText === null) return EMPTY_RESPONSE_ANALYSIS;
    const parsed = parseOpenAIResponse(responseText);
    if (parsed === null) return EMPTY_RESPONSE_ANALYSIS;
    const toolNames =
      parsed.choices[0]?.message?.tool_calls
        ?.map((toolCall) => toolCall.function?.name ?? "")
        .filter((name) => name !== "") ?? [];
    return {
      parsed,
      toolNames: toolNames.length > 0 ? toolNames : null,
    };
  },

  anatomySegments(parsed) {
    if (parsed === null || typeof parsed !== "object") return null;
    // We deliberately skip OpenAIRequestSchema validation here for the
    // same reason as the Anthropic adapter: the Anatomy view should
    // render even when the body shape is slightly off-schema. We only
    // need the top-level `messages` and `tools` arrays.
    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    const body = parsed as { messages?: unknown; tools?: unknown };
    const segments: AnatomySegment[] = [];

    // Promote a leading role: "system" message into a "system" segment.
    // Spec: derive system role from a leading role: "system" message, then
    // a segment per remaining message. We still keep the system message in
    // the segment list (not skipped) when there is no separate system field
    // to compare against — the leading role: "system" message IS the system
    // content for OpenAI Chat Completions. If a separate `system` field
    // existed and matched the message, we would dedupe, but Chat
    // Completions has no such field, so we never have to skip.
    if (Array.isArray(body.messages)) {
      body.messages.forEach((message, index) => {
        if (message === null || typeof message !== "object") return;
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
        const m = message as { role?: unknown; content?: unknown };
        const role =
          m.role === "user" || m.role === "assistant" || m.role === "system" || m.role === "tool"
            ? m.role
            : "user";
        const text = messageToText(m);
        segments.push(segment(role, `[${index}] ${role}`, text, `/messages/${index}`));
      });
    }

    if (Array.isArray(body.tools) && body.tools.length > 0) {
      const text = toolsToText(body.tools);
      segments.push(segment("tools", "tools", text, "/tools"));
    }

    return segments.length > 0 ? segments : null;
  },
};
