/**
 * Model Configuration
 * Centralized model information, configuration, options, constraints, and validation
 * Single source of truth for all model-related definitions
 * Update manually when pricing changes or new models are added
 */

import { z } from 'zod'

// ============================================================================
// Model Types
// ============================================================================

/**
 * Supported Open AI models (direct SDK access)
 */
export type OpenAIModel = 'gpt-5' | 'gpt-5.4-mini' | 'gpt-5.4-nano'

/**
 * Supported OpenRouter models (explicit union for type safety)
 */
export type OpenRouterModel = 'openrouter/z-ai/glm-5'

/**
 * Supported Google models (direct SDK access)
 */
export type GoogleModel = 'gemini-3-flash-preview' | 'gemini-3.1-flash-lite-preview'

/**
 * Supported Anthropic models (direct SDK access via @anthropic-ai/sdk)
 */
export type AnthropicModel =
  | 'claude-opus-4-8'
  | 'claude-sonnet-4-6'
  | 'claude-haiku-4-5-20251001'
  | 'claude-haiku-4-5'
  | 'claude-sonnet-4-5'

/** Supported LLM models */
export type LLMModel = OpenAIModel | OpenRouterModel | GoogleModel | AnthropicModel | 'mock'

// ============================================================================
// Model Configuration Schemas (Schema-First - Single Source of Truth)
// ============================================================================

/**
 * GPT-5 model options schema
 */
const GPT5OptionsSchema = z.object({
  reasoning_effort: z.enum(['minimal', 'low', 'medium', 'high']).optional(),
  verbosity: z.enum(['low', 'medium', 'high']).optional()
})

/**
 * GPT-5 family complete config schema
 * Validates entire ModelConfig including constraints and options
 * GPT-5, GPT-5.4-mini, and GPT-5.4-nano share same validation rules
 */
export const GPT5ConfigSchema = z.object({
  model: z.enum(['gpt-5', 'gpt-5.4-mini', 'gpt-5.4-nano']),
  provider: z.enum(['openai']),
  apiKey: z.string(),
  temperature: z.literal(1), // Required to be exactly 1
  maxOutputTokens: z.number().min(4000).optional(),
  topP: z.number().min(0).max(1).optional(),
  modelOptions: GPT5OptionsSchema.optional()
})

/**
 * Mock model config schema
 */
export const MockConfigSchema = z.object({
  model: z.enum(['mock']),
  provider: z.enum(['mock']),
  apiKey: z.string(),
  temperature: z.number().min(0).max(2).optional(),
  maxOutputTokens: z.number().min(500).optional(),
  topP: z.number().min(0).max(1).optional(),
  modelOptions: z.object({}).strict().optional() // No options supported
})

/**
 * OpenRouter model options schema
 * OpenRouter-specific options for routing and transforms
 */
const OpenRouterOptionsSchema = z.object({
  /** Optional transforms to apply (e.g., 'middle-out' for long context) */
  transforms: z.array(z.string()).optional(),
  /** Routing strategy (e.g., 'fallback' for automatic provider failover) */
  route: z.enum(['fallback']).optional()
})

/**
 * OpenRouter config schema
 * Validates ModelConfig for OpenRouter provider
 */
export const OpenRouterConfigSchema = z.object({
  model: z.enum(['openrouter/z-ai/glm-5']),
  provider: z.literal('openrouter'),
  apiKey: z.string(),
  temperature: z.number().min(0).max(2).optional(),
  maxOutputTokens: z.number().min(500).optional(),
  topP: z.number().min(0).max(1).optional(),
  modelOptions: OpenRouterOptionsSchema.optional()
})

/**
 * Google model options schema
 * Gemini 3 specific options for thinking depth control
 */
const GoogleOptionsSchema = z.object({
  /** Thinking level for Gemini 3 models (controls reasoning depth) */
  thinkingLevel: z.enum(['minimal', 'low', 'medium', 'high']).optional()
})

/**
 * Google config schema
 * Validates ModelConfig for Google provider (direct SDK access)
 */
export const GoogleConfigSchema = z.object({
  model: z.enum(['gemini-3-flash-preview', 'gemini-3.1-flash-lite-preview']),
  provider: z.literal('google'),
  apiKey: z.string(),
  temperature: z.number().min(0).max(2).optional(),
  maxOutputTokens: z.number().min(500).optional(),
  topP: z.number().min(0).max(1).optional(),
  modelOptions: GoogleOptionsSchema.optional()
})

/**
 * Anthropic model options schema
 * Currently empty - future options must be added per supported model family
 */
const AnthropicOptionsSchema = z.object({}).strict()

/**
 * Anthropic config schema
 * Validates ModelConfig for Anthropic provider (direct SDK access)
 */
const AnthropicStandardConfigSchema = z.object({
  model: z.enum([
    'claude-sonnet-4-6',
    'claude-haiku-4-5-20251001',
    'claude-haiku-4-5',
    'claude-sonnet-4-5'
  ]),
  provider: z.literal('anthropic'),
  apiKey: z.string(),
  temperature: z.number().min(0).max(1).optional(),
  maxOutputTokens: z.number().min(1000).max(64000).optional(), // Anthropic requires max_tokens
  topP: z.number().min(0).max(1).optional(),
  modelOptions: AnthropicOptionsSchema.optional()
})

const AnthropicOpus48ConfigSchema = z.object({
  model: z.literal('claude-opus-4-8'),
  provider: z.literal('anthropic'),
  apiKey: z.string(),
  temperature: z.literal(1).optional(),
  maxOutputTokens: z.number().min(1000).max(128000).optional(), // Anthropic requires max_tokens
  topP: z.literal(1).optional(),
  topK: z.undefined().optional(),
  top_k: z.undefined().optional(),
  modelOptions: AnthropicOptionsSchema.optional()
})

export const AnthropicConfigSchema = z.discriminatedUnion('model', [
  AnthropicOpus48ConfigSchema,
  AnthropicStandardConfigSchema
])

/**
 * Infer TypeScript types from schemas
 */
export type GPT5Options = z.infer<typeof GPT5OptionsSchema>
export type MockOptions = Record<string, never>
export type OpenRouterOptions = z.infer<typeof OpenRouterOptionsSchema>
export type GoogleOptions = z.infer<typeof GoogleOptionsSchema>
export type AnthropicOptions = z.infer<typeof AnthropicOptionsSchema>
export type ModelSpecificOptions = GPT5Options | MockOptions | OpenRouterOptions | GoogleOptions | AnthropicOptions

// ============================================================================
// Model Configuration
// ============================================================================

/**
 * Model configuration for LLM execution
 * Belongs in resource definition (AgentDefinition, WorkflowDefinition, etc.)
 */
export interface ModelConfig {
  model: LLMModel
  provider: 'openai' | 'anthropic' | 'openrouter' | 'google' | 'mock'
  apiKey: string
  temperature?: number
  /** Maximum output tokens per LLM call. NOT the model's context window — see ModelInfo.maxTokens for that. */
  maxOutputTokens?: number
  topP?: number

  /**
   * Model-specific options (flat structure)
   * Options are model-specific, not vendor-specific
   * Available options defined in MODEL_INFO per model
   * Validated at build time via validateModelOptions()
   */
  modelOptions?: ModelSpecificOptions
}

// ============================================================================
// Model Metadata
// ============================================================================

export interface ModelInfo {
  /** Cost in cents per 1M input tokens */
  inputCostPer1M: number
  /** Cost in cents per 1M output tokens */
  outputCostPer1M: number
  /** Minimum tokens required for reliable output generation */
  minTokens: number
  /** Recommended tokens for production use (typically 2x minimum) */
  recommendedTokens: number
  /** Maximum context window size (total tokens: input + output) */
  maxTokens: number
  /** Maximum output tokens supported by the synchronous generation API */
  maxOutputTokens?: number
  /** Model category for grouping and documentation */
  category: 'reasoning' | 'standard' | 'embedding'
  /** Zod schema for validating complete ModelConfig (replaces constraints + optionsSchema) */
  configSchema?: z.ZodType<ModelConfig>
}

// Model configuration as of 2026-06-09
export const MODEL_INFO: Record<LLMModel, ModelInfo> = {
  // OpenAI GPT-5 (Reasoning Models)
  'gpt-5': {
    inputCostPer1M: 125, // $1.25 per 1M tokens
    outputCostPer1M: 1000, // $10.00 per 1M tokens
    minTokens: 4000, // Reasoning models need more
    recommendedTokens: 8000,
    maxTokens: 400000, // 400k context window
    category: 'reasoning',
    configSchema: GPT5ConfigSchema
  },
  'gpt-5.4-mini': {
    inputCostPer1M: 75, // $0.75 per 1M tokens
    outputCostPer1M: 450, // $4.50 per 1M tokens
    minTokens: 4000, // Reasoning models need more
    recommendedTokens: 8000,
    maxTokens: 400000, // 400k context window
    category: 'reasoning',
    configSchema: GPT5ConfigSchema
  },
  'gpt-5.4-nano': {
    inputCostPer1M: 20, // $0.20 per 1M tokens
    outputCostPer1M: 125, // $1.25 per 1M tokens
    minTokens: 4000,
    recommendedTokens: 8000,
    maxTokens: 400000, // 400k context window
    category: 'standard',
    configSchema: GPT5ConfigSchema
  },
  // Mock model for testing
  mock: {
    inputCostPer1M: 0, // Free for tests
    outputCostPer1M: 0,
    minTokens: 500, // Low minimum for test flexibility
    recommendedTokens: 1000,
    maxTokens: 100000, // 100k for testing
    category: 'standard',
    configSchema: MockConfigSchema
  },
  // OpenRouter Models (via openrouter.ai)
  'openrouter/z-ai/glm-5': {
    inputCostPer1M: 72, // $0.72 per 1M tokens
    outputCostPer1M: 230, // $2.30 per 1M tokens
    minTokens: 4000,
    recommendedTokens: 4000,
    maxTokens: 202752, // 202,752 context window
    category: 'standard',
    configSchema: OpenRouterConfigSchema
  },
  // Google Gemini Models (direct SDK access via @google/genai)
  'gemini-3-flash-preview': {
    inputCostPer1M: 50, // $0.50 per 1M tokens
    outputCostPer1M: 300, // $3.00 per 1M tokens
    minTokens: 4000,
    recommendedTokens: 8000,
    maxTokens: 1000000, // 1M context window
    category: 'standard',
    configSchema: GoogleConfigSchema
  },
  'gemini-3.1-flash-lite-preview': {
    inputCostPer1M: 25, // $0.25 per 1M tokens
    outputCostPer1M: 150, // $1.50 per 1M tokens
    minTokens: 4000,
    recommendedTokens: 8000,
    maxTokens: 1000000, // 1M context window
    category: 'standard',
    configSchema: GoogleConfigSchema
  },
  // Anthropic Claude Models (direct SDK access via @anthropic-ai/sdk)
  'claude-opus-4-8': {
    inputCostPer1M: 500, // $5.00 per 1M tokens
    outputCostPer1M: 2500, // $25.00 per 1M tokens
    minTokens: 4000,
    recommendedTokens: 8000,
    maxTokens: 1000000, // 1M context window
    maxOutputTokens: 128000,
    category: 'reasoning',
    configSchema: AnthropicConfigSchema
  },
  'claude-sonnet-4-6': {
    inputCostPer1M: 300, // $3.00 per 1M tokens
    outputCostPer1M: 1500, // $15.00 per 1M tokens
    minTokens: 4000,
    recommendedTokens: 8000,
    maxTokens: 1000000, // 1M context window
    maxOutputTokens: 64000,
    category: 'standard',
    configSchema: AnthropicConfigSchema
  },
  'claude-haiku-4-5-20251001': {
    inputCostPer1M: 100, // $1.00 per 1M tokens
    outputCostPer1M: 500, // $5.00 per 1M tokens
    minTokens: 4000,
    recommendedTokens: 8000,
    maxTokens: 200000, // 200k context window
    maxOutputTokens: 64000,
    category: 'standard',
    configSchema: AnthropicConfigSchema
  },
  'claude-haiku-4-5': {
    inputCostPer1M: 100, // $1.00 per 1M tokens
    outputCostPer1M: 500, // $5.00 per 1M tokens
    minTokens: 4000,
    recommendedTokens: 8000,
    maxTokens: 200000, // 200k context window
    maxOutputTokens: 64000,
    category: 'standard',
    configSchema: AnthropicConfigSchema
  },
  'claude-sonnet-4-5': {
    inputCostPer1M: 300, // $3.00 per 1M tokens
    outputCostPer1M: 1500, // $15.00 per 1M tokens
    minTokens: 4000,
    recommendedTokens: 8000,
    maxTokens: 200000, // 200k context window
    maxOutputTokens: 64000,
    category: 'standard',
    configSchema: AnthropicConfigSchema
  }
}

/**
 * Get model information (pricing and token requirements)
 * Supports exact match and prefix matching (e.g., 'gpt-5-preview' matches 'gpt-5')
 *
 * @param model - Model name (e.g., 'gpt-5.4-mini')
 * @returns Model info or undefined if not found
 */
export function getModelInfo(model: string): ModelInfo | undefined {
  // Check exact match first
  if (model in MODEL_INFO) {
    return MODEL_INFO[model as LLMModel]
  }

  // Check prefix match (e.g., gpt-5-preview -> gpt-5)
  for (const [knownModel, info] of Object.entries(MODEL_INFO)) {
    if (model.startsWith(knownModel)) {
      return info
    }
  }

  return undefined
}

/**
 * Calculate cost in USD for LLM usage
 *
 * @param model - Model name (e.g., 'gpt-5.4-mini')
 * @param inputTokens - Number of input tokens
 * @param outputTokens - Number of output tokens
 * @returns Cost in USD with full decimal precision (no rounding)
 */
export function calculateCost(model: string, inputTokens: number, outputTokens: number): number {
  const info = getModelInfo(model)

  if (!info) {
    // Unknown model - return 0 cost but log warning
    console.warn(
      `[CostCalculator] Unknown model '${model}' - cannot calculate cost. Available models:`,
      Object.keys(MODEL_INFO)
    )
    return 0
  }

  // Calculate in USD with full precision (pricing is in cents per 1M)
  const inputCostUsd = (inputTokens / 1_000_000) * (info.inputCostPer1M / 100)
  const outputCostUsd = (outputTokens / 1_000_000) * (info.outputCostPer1M / 100)
  const totalCostUsd = inputCostUsd + outputCostUsd

  return totalCostUsd
}
