/**
 * Agent-Specific LLM Adapter Helpers
 * Wraps generic LLM adapter with agent-specific request/response formatting
 */

import { z, ZodError } from 'zod'
import type { LLMAdapter, LLMMessage } from '../../../llm/types'
import type { ToolDefinition } from '../../../tools/types'
import type { AgentAction } from '../../actions/types'
import { AgentActionSchema } from '../../actions/types'
import { getModelInfo } from '../../../llm/model-info'
import { InsufficientTokensError } from '../../../llm/errors'
import { AgentOutputValidationError } from '../../errors'

// ============================================================================
// Memory Operations Schema
// ============================================================================

const MemoryOperationsSchema = z.object({
  set: z.record(z.string(), z.any()).optional(), // Accept any type - framework will stringify
  delete: z.array(z.string()).optional()
})

// ============================================================================
// Zod Schemas for Agent Responses
// ============================================================================

/**
 * Agent iteration output schema
 * Validates LLM response during iteration phase
 */
const AgentIterationOutputSchema = z.object({
  reasoning: z.string(),
  memoryOps: MemoryOperationsSchema.optional(),
  nextActions: z.array(AgentActionSchema)
})

// ============================================================================
// Token Validation
// ============================================================================

/**
 * Validate token configuration meets model minimums
 * Uses centralized MODEL_INFO from model-info.ts
 *
 * @param model - Model name (e.g., 'gpt-5-mini')
 * @param maxOutputTokens - Configured max output tokens (optional)
 * @throws InsufficientTokensError if below minimum
 */
function validateTokenConfiguration(model: string, maxOutputTokens?: number): void {
  const modelInfo = getModelInfo(model)
  const configured = maxOutputTokens || 1000 // Default if not specified

  if (!modelInfo) {
    // Unknown model - use conservative default (2000 tokens)
    if (configured < 2000) {
      throw new InsufficientTokensError(
        `Unknown model '${model}' requires at least 2000 tokens (conservative default), but only ${configured} configured.`,
        { model, required: 2000, configured }
      )
    }
    return
  }

  // Check against model-specific minimum
  if (configured < modelInfo.minTokens) {
    throw new InsufficientTokensError(
      `Model ${model} requires at least ${modelInfo.minTokens} tokens, but only ${configured} configured. ` +
        `${modelInfo.category === 'reasoning' ? 'Reasoning models need more tokens to generate both internal reasoning and output.' : ''}`,
      {
        model,
        required: modelInfo.minTokens,
        recommended: modelInfo.recommendedTokens,
        configured
      }
    )
  }
}

// ============================================================================
// Helper Functions
// ============================================================================

/**
 * Call LLM for agent iteration phase
 * Wraps generic adapter with agent-specific request/response formats
 *
 * Dual validation:
 * 1. OpenAI validates with JSON Schema (structured output)
 * 2. Zod validates at runtime (TypeScript type safety)
 *
 * @param adapter - Generic LLM adapter
 * @param request - Agent-specific iteration request
 * @returns Reasoning and next actions
 */
export async function callLLMForAgentIteration(
  adapter: LLMAdapter,
  request: {
    systemPrompt: string
    memoryContext: string
    tools: ToolDefinition[]
    constraints: { maxOutputTokens?: number; temperature?: number }
    model: string // Model name for validation
    includeMessageAction: boolean // Whether to include message action (session-based only)
    includeNavigateKnowledge: boolean // Whether to include navigate-knowledge action (knowledge map present)
    includeMemoryOps: boolean // Whether to include memoryOps in schema (based on memoryPreferences)
    signal?: AbortSignal // Execution-level abort signal for timeout/cancellation
  }
): Promise<{
  reasoning: string
  memoryOps?: { set?: Record<string, any>; delete?: string[] }
  nextActions: AgentAction[]
}> {
  // Step 0: VALIDATE token configuration before API call
  validateTokenConfiguration(request.model, request.constraints.maxOutputTokens)

  const messages: LLMMessage[] = [
    { role: 'system', content: request.systemPrompt },
    { role: 'user', content: request.memoryContext }
  ]

  // Step 1: Call generic adapter with JSON Schema
  const response = await adapter.generate<unknown>({
    messages,
    responseSchema: buildIterationResponseSchema(
      request.tools,
      request.includeMessageAction,
      request.includeNavigateKnowledge,
      request.includeMemoryOps
    ),
    maxOutputTokens: request.constraints.maxOutputTokens,
    temperature: request.constraints.temperature,
    signal: request.signal
  })

  // Step 2: Validate with Zod (defense in depth, wrap errors in typed core error)
  try {
    const validated = AgentIterationOutputSchema.parse(response.output)

    // Step 3: Return typed response (cast to AgentAction[] for proper typing)
    return {
      reasoning: validated.reasoning,
      memoryOps: validated.memoryOps,
      nextActions: validated.nextActions as AgentAction[]
    }
  } catch (error) {
    // Don't let Zod error bubble up - wrap in typed core error
    throw new AgentOutputValidationError('Agent iteration output validation failed', {
      zodError: error instanceof ZodError ? error.format() : error
    })
  }
}

/**
 * Call LLM for agent completion phase
 * Same dual validation flow as iteration
 *
 * @param adapter - Generic LLM adapter
 * @param request - Agent-specific completion request
 * @returns Structured output (no reasoning - output should be self-explanatory)
 */
export async function callLLMForAgentCompletion(
  adapter: LLMAdapter,
  request: {
    systemPrompt: string
    memoryContext: string
    outputSchema: unknown
    constraints: { maxOutputTokens?: number; temperature?: number }
    model: string // Model name for validation
    signal?: AbortSignal // Execution-level abort signal for timeout/cancellation
  }
): Promise<unknown> {
  // Step 0: VALIDATE token configuration before API call
  validateTokenConfiguration(request.model, request.constraints.maxOutputTokens)

  // Step 1: Call generic adapter with JSON Schema (directly use output schema)
  const response = await adapter.generate<unknown>({
    messages: [
      { role: 'system', content: request.systemPrompt },
      { role: 'user', content: request.memoryContext }
    ],
    responseSchema: request.outputSchema, // Use output schema directly
    temperature: request.constraints.temperature || 0.3,
    maxOutputTokens: request.constraints.maxOutputTokens,
    signal: request.signal
  })

  // Step 2: Return output directly (already validated by LLM against schema)
  return response.output
}

// ============================================================================
// JSON Schema Builders
// ============================================================================

/**
 * Clean a JSON Schema for LLM consumption
 * - Removes $schema property (Gemini doesn't accept it)
 * - Ensures object types have non-empty properties (Gemini requirement)
 */
function cleanJsonSchemaForLLM(schema: unknown): unknown {
  if (!schema || typeof schema !== 'object') {
    return schema
  }

  const cleaned: Record<string, unknown> = {}
  for (const [key, value] of Object.entries(schema as Record<string, unknown>)) {
    // Skip $schema - Gemini doesn't accept it
    if (key === '$schema') {
      continue
    }
    // Recursively clean nested objects
    if (value && typeof value === 'object') {
      if (Array.isArray(value)) {
        cleaned[key] = value.map((item) => cleanJsonSchemaForLLM(item))
      } else {
        cleaned[key] = cleanJsonSchemaForLLM(value)
      }
    } else {
      cleaned[key] = value
    }
  }

  // SAFEGUARD: Gemini requires objects to have non-empty properties
  // If this is an object type with empty properties, add a placeholder
  // Google's API strictly requires properties to be non-empty for OBJECT type.
  if (
    cleaned.type === 'object' &&
    cleaned.properties &&
    typeof cleaned.properties === 'object' &&
    Object.keys(cleaned.properties as object).length === 0
  ) {
    // Add a placeholder that signals "no input required"
    // The LLM will see this and know to pass an empty object or omit input
    ;(cleaned.properties as Record<string, unknown>).noInputRequired = {
      type: 'boolean',
      description: 'No input required for this tool. Pass true or omit entirely.'
    }
  }

  return cleaned
}

/**
 * Build iteration response JSON Schema for OpenAI
 * Converts agent action schemas to JSON Schema format
 *
 * @param tools - Tool definitions with their JSON Schema input schemas
 * @param includeMessageAction - Whether to include message action (session-based only)
 * @param includeNavigateKnowledge - Whether to include navigate-knowledge action (knowledge map present)
 * @param includeMemoryOps - Whether to include memoryOps in schema (based on memoryPreferences)
 */
function buildIterationResponseSchema(
  tools: ToolDefinition[],
  includeMessageAction: boolean,
  includeNavigateKnowledge: boolean,
  includeMemoryOps: boolean
): unknown {
  // Build action schemas array
  const actionSchemas: any[] = []

  // Add tool-specific schemas for each available tool
  // This gives the LLM precise input schemas, preventing malformed tool calls
  for (const tool of tools) {
    actionSchemas.push({
      type: 'object',
      properties: {
        type: { type: 'string', enum: ['tool-call'] },
        id: { type: 'string' },
        name: { type: 'string', enum: [tool.name] }, // Constrain to this specific tool
        input: cleanJsonSchemaForLLM(tool.inputSchema) // Clean and use the actual JSON Schema
      },
      required: ['type', 'id', 'name', 'input'],
      additionalProperties: false
    })
  }

  // complete action (always available)
  actionSchemas.push({
    type: 'object',
    properties: {
      type: { type: 'string', enum: ['complete'] }
    },
    required: ['type'],
    additionalProperties: false
  })

  // Conditionally add message action (session-based agents only)
  if (includeMessageAction) {
    actionSchemas.push({
      type: 'object',
      properties: {
        type: { type: 'string', enum: ['message'] },
        text: { type: 'string' }
      },
      required: ['type', 'text'],
      additionalProperties: false
    })
  }

  // Conditionally add navigate-knowledge action (knowledge map present)
  if (includeNavigateKnowledge) {
    actionSchemas.push({
      type: 'object',
      properties: {
        type: { type: 'string', enum: ['navigate-knowledge'] },
        id: { type: 'string' },
        nodeId: { type: 'string' }
      },
      required: ['type', 'id', 'nodeId'],
      additionalProperties: false
    })
  }

  // Build base properties
  const properties: any = {
    reasoning: { type: 'string', description: 'Your reasoning process' },
    nextActions: {
      type: 'array',
      items: {
        anyOf: actionSchemas
      }
    }
  }

  // CONDITIONAL: Only include memoryOps if agent opted in
  if (includeMemoryOps) {
    properties.memoryOps = {
      type: 'object',
      properties: {
        set: {
          type: 'object',
          // Memory keys are dynamic - allow any string keys with any values
          // Validated at runtime by the memory manager
          additionalProperties: true
        },
        delete: { type: 'array', items: { type: 'string' } }
      },
      additionalProperties: false
    }
  }

  return {
    type: 'object',
    properties,
    required: ['reasoning', 'nextActions'],
    additionalProperties: false
  }
}
