import { z } from "zod";

// Import JsonValueSchema from shared location to avoid circular dependency
import { JsonValueSchema, type JsonValue } from "./formats/jsonSchema";

export { JsonValueSchema };
export type { JsonValue };

export type RequestFormat = "anthropic" | "openai" | "unknown";

import { safeGetOwnProperty } from "../lib/objectUtils";

/**
 * A single SSE event from a streaming response.
 */
export const StreamingChunkSchema = z.object({
  index: z.number(),
  timestamp: z.number(),
  type: z.string(),
  data: JsonValueSchema,
});

export type StreamingChunk = z.infer<typeof StreamingChunkSchema>;

const StreamingChunksArraySchema = z.object({
  chunks: z.array(StreamingChunkSchema),
  truncated: z.boolean().optional().default(false),
});

export const CapturedLogSchema = z.object({
  id: z.number(),
  timestamp: z.string(),
  method: z.string(),
  path: z.string(),
  model: z.string().nullable(),
  sessionId: z.string().nullable(),
  rawRequestBody: z.string().nullable(),
  responseStatus: z.number().nullable(),
  responseText: z.string().nullable(),
  inputTokens: z.number().nullable(),
  outputTokens: z.number().nullable(),
  cacheCreationInputTokens: z.number().nullable(),
  cacheReadInputTokens: z.number().nullable(),
  elapsedMs: z.number().nullable(),
  streaming: z.boolean(),
  userAgent: z.string().nullable(),
  origin: z.string().nullable(),
  rawHeaders: z.record(z.string(), z.string()).optional(),
  /** Headers sent to the upstream LLM */
  headers: z.record(z.string(), z.string()).optional(),
  apiFormat: z.enum(["anthropic", "openai", "unknown"]).default("unknown"),
  isTest: z.boolean().optional().default(false),
  providerName: z.string().nullable().optional(),
  clientPort: z.number().nullable().optional(),
  clientPid: z.number().nullable().optional(),
  clientCwd: z.string().nullable().optional(),
  clientProjectFolder: z.string().nullable().optional(),
  streamingChunks: StreamingChunksArraySchema.optional(),
  streamingChunksPath: z.string().nullable().optional(),
  /** Error message from streaming response (e.g., SSE error event) */
  error: z.string().nullable().optional(),
});

export type CapturedLog = z.infer<typeof CapturedLogSchema>;

export type TokenUsage = {
  inputTokens: number | null;
  outputTokens: number | null;
  cacheCreationInputTokens: number | null;
  cacheReadInputTokens: number | null;
};

// ============================================================
// Import types and schemas from formats
// ============================================================

// Import types and schemas from formats for internal use
import { InspectorRequestSchema, type InspectorRequest } from "./formats/anthropic/schemas";

// Re-export types and schemas from formats for backward compatibility
export {
  InspectorRequestSchema,
  InspectorResponseSchema,
  SseEventSchema,
} from "./formats/anthropic/schemas";
export type {
  InspectorRequest,
  InspectorResponse,
  ResponseContentBlockType,
} from "./formats/anthropic/schemas";

export {
  OpenAIRequestSchema,
  OpenAIResponseSchema,
  OpenAISSERawChunkSchema,
  type OpenAIResponse,
  type OpenAIToolCall,
  parseOpenAIResponse,
} from "./formats/openai/schemas";

// ============================================================
// Utility functions
// ============================================================

function detectFormat(rawBody: string | null): RequestFormat {
  if (rawBody === null) return "unknown";
  try {
    const json: unknown = JSON.parse(rawBody);
    if (typeof json === "object" && json !== null && !Array.isArray(json)) {
      const hasModel = safeGetOwnProperty(json, "model") !== undefined;
      const hasMessages = safeGetOwnProperty(json, "messages") !== undefined;
      if (hasModel && hasMessages) {
        // Anthropic requests put `system` as a top-level key; OpenAI does not.
        // Both formats can have `tools`, so we check `system` as the discriminator.
        if (safeGetOwnProperty(json, "system") !== undefined) {
          return "anthropic";
        }
        const msgVal = safeGetOwnProperty(json, "messages");
        if (Array.isArray(msgVal)) {
          return "openai";
        }
      }
    }
    return "unknown";
  } catch {
    return "unknown";
  }
}

// Schema for model name in request body (used for routing)
const RequestModelSchema = z.object({
  model: z.string(),
});

export function extractModelFromBody(body: string): string | null {
  try {
    const json: unknown = JSON.parse(body);
    const parsed = RequestModelSchema.safeParse(json);
    if (parsed.success) {
      return parsed.data.model;
    }
    return null;
  } catch {
    return null;
  }
}

/**
 * Loose request schema used for routing + log metadata. Tolerant of missing
 * fields so it can extract what it can from a partially-valid or unparseable
 * body without throwing.
 */
export const LooseRequestSchema = z.object({
  model: z.string().optional(),
  metadata: z.object({ user_id: z.string().optional() }).passthrough().optional(),
});

export type RequestMetadata = {
  model: string | null;
  sessionId: string | null;
};

function extractSessionIdFromHeaders(headers: Headers): string | null {
  return headers.get("x-llm-inspector-session-id") ?? headers.get("x-session-affinity");
}

/**
 * Parse a request body exactly once and pull out everything the handler needs
 * for routing + log creation: the model (used for provider selection) and the
 * session id (from `metadata.user_id` or the `x-session-affinity` header).
 *
 * Returning `null` for either field is a valid outcome — callers must handle
 * "unknown" gracefully. Callers that already have the body string should use
 * this instead of re-parsing with `extractModelFromBody` and a second pass
 * for the session id.
 */
export function extractRequestMetadata(body: string | null, headers: Headers): RequestMetadata {
  const headerSessionId = extractSessionIdFromHeaders(headers);
  if (body === null) return { model: null, sessionId: headerSessionId };
  try {
    const json: unknown = JSON.parse(body);
    const loose = LooseRequestSchema.safeParse(json);
    if (loose.success) {
      return {
        model: loose.data.model ?? null,
        sessionId: loose.data.metadata?.user_id ?? headerSessionId,
      };
    }
  } catch {
    // body not valid JSON
  }
  return { model: null, sessionId: headerSessionId };
}

/**
 * Parse a raw request body into an InspectorRequest.
 * @deprecated Use FormatHandler.parseRequest() instead
 */
export function parseRequest(rawBody: string | null): InspectorRequest | null {
  if (rawBody === null) return null;
  try {
    const json: unknown = JSON.parse(rawBody);
    const result = InspectorRequestSchema.safeParse(json);
    if (result.success) return result.data;
    return null;
  } catch {
    return null;
  }
}
