/**
 * Format-parser-driven preview extractor for the MCP `inspector_list_logs`
 * "thick summary" shape. See `openspec/changes/add-mcp-server/design.md`
 * decision D5.
 *
 * Why we re-parse with the format schemas (not slice `rawRequestBody`):
 * a 500-char prefix is almost always JSON envelope + system prompt boilerplate
 * with zero signal about what the user actually asked. The format parsers
 * give us the first text block of the last user message, which is what an
 * agent needs to make a "is this the log I care about?" judgment from
 * `list_logs` alone.
 *
 * Null returns are an expected and frequent outcome — they cover:
 *   - `apiFormat === "unknown"`
 *   - `rawRequestBody` / `responseText` is null
 *   - JSON.parse failure
 *   - schema validation failure
 *   - no qualifying user / assistant message
 *   - the relevant content is non-text (e.g., image-only multimodal block)
 */

import type { CapturedLog } from "../proxy/schemas";
import {
  AnthropicRequestSchema,
  AnthropicResponseSchema,
} from "../proxy/formats/anthropic/schemas";
import { OpenAIRequestSchema, OpenAIResponseSchema } from "../proxy/formats/openai/schemas";

/** Preview length cap, per design D5. */
const PREVIEW_MAX_CHARS = 500;

function truncate(text: string): string {
  return text.length <= PREVIEW_MAX_CHARS ? text : text.slice(0, PREVIEW_MAX_CHARS);
}

/**
 * Safely extract the first text from an Anthropic `Message.content` (string
 * or array of blocks). Returns `null` for non-text-only or empty content.
 */
function firstAnthropicText(
  content: string | ReadonlyArray<{ type: string; text?: string }>,
): string | null {
  if (typeof content === "string") {
    return content.length > 0 ? content : null;
  }
  for (const block of content) {
    if (block.type === "text" && typeof block.text === "string" && block.text.length > 0) {
      return block.text;
    }
  }
  return null;
}

/**
 * Safely extract the first text from an OpenAI message content (string or
 * array of `{type:"text"|"image_url",...}`). Returns `null` for non-text /
 * empty content.
 */
function firstOpenAIText(
  content: string | ReadonlyArray<{ type: string; text?: string }> | null | undefined,
): string | null {
  if (content === null || content === undefined) return null;
  if (typeof content === "string") {
    return content.length > 0 ? content : null;
  }
  for (const part of content) {
    if (part.type === "text" && typeof part.text === "string" && part.text.length > 0) {
      return part.text;
    }
  }
  return null;
}

/**
 * Extract a 500-char preview of the last `role: "user"` message's first text
 * block. Returns `null` for unknown format, parse failures, or non-text
 * content. See module-level docstring for the full null-case list.
 */
export function extractLastUserMessagePreview(log: CapturedLog): string | null {
  if (log.apiFormat === "unknown") return null;
  if (log.rawRequestBody === null) return null;

  let json: unknown;
  try {
    json = JSON.parse(log.rawRequestBody);
  } catch {
    return null;
  }

  if (log.apiFormat === "anthropic") {
    const parsed = AnthropicRequestSchema.safeParse(json);
    if (!parsed.success) return null;
    // Iterate from the end to find the most recent user turn.
    for (let i = parsed.data.messages.length - 1; i >= 0; i--) {
      const msg = parsed.data.messages[i];
      if (msg && msg.role === "user") {
        const text = firstAnthropicText(msg.content);
        return text === null ? null : truncate(text);
      }
    }
    return null;
  }

  if (log.apiFormat === "openai") {
    const parsed = OpenAIRequestSchema.safeParse(json);
    if (!parsed.success) return null;
    for (let i = parsed.data.messages.length - 1; i >= 0; i--) {
      const msg = parsed.data.messages[i];
      if (msg && msg.role === "user") {
        const text = firstOpenAIText(msg.content);
        return text === null ? null : truncate(text);
      }
    }
    return null;
  }

  return null;
}

/**
 * Extract a 500-char preview of the assistant's response text. Returns
 * `null` for unknown format, parse failures, missing response, or non-text
 * content.
 */
export function extractResponsePreview(log: CapturedLog): string | null {
  if (log.apiFormat === "unknown") return null;
  if (log.responseText === null) return null;

  let json: unknown;
  try {
    json = JSON.parse(log.responseText);
  } catch {
    return null;
  }

  if (log.apiFormat === "anthropic") {
    const parsed = AnthropicResponseSchema.safeParse(json);
    if (!parsed.success) return null;
    for (const block of parsed.data.content) {
      if (block.type === "text" && typeof block.text === "string" && block.text.length > 0) {
        return truncate(block.text);
      }
    }
    return null;
  }

  if (log.apiFormat === "openai") {
    const parsed = OpenAIResponseSchema.safeParse(json);
    if (!parsed.success) return null;
    // Pick the first choice that actually carries a text-like content.
    for (const choice of parsed.data.choices) {
      const msg = choice.message;
      if (!msg) continue;
      const direct = firstOpenAIText(msg.content);
      if (direct !== null) return truncate(direct);
      // Some MiniMax / Moonshot-style providers stuff the text into
      // reasoning_content / thinking / think instead of content. We surface
      // whichever is the first non-empty one in a stable priority order.
      const fallback = msg.reasoning_content ?? msg.thinking ?? msg.think;
      if (typeof fallback === "string" && fallback.length > 0) return truncate(fallback);
    }
    return null;
  }

  return null;
}
