/**
 * Lightweight JSON Schema validator for LLM responseSchema compliance
 *
 * Validates that LLM output matches the responseSchema that was sent to the provider.
 * Not a full JSON Schema validator -- covers the subset actually used by LLM structured output:
 * required fields, property types, enum values, nested objects, and arrays.
 *
 * Throws LLMResponseParseError (retryable) so the universal adapter retry loop can re-attempt.
 */

import { LLMResponseParseError } from './errors'

interface JsonSchema {
  type?: string
  properties?: Record<string, JsonSchema>
  required?: string[]
  enum?: unknown[]
  items?: JsonSchema
  const?: unknown
}

/**
 * Validate LLM output against the provided JSON Schema.
 * Throws LLMResponseParseError with a descriptive message listing all violations.
 */
export function validateResponseSchema(output: unknown, schema: unknown): void {
  if (!schema || typeof schema !== 'object') return

  const errors = collectErrors(output, schema as JsonSchema, '')
  if (errors.length === 0) return

  throw new LLMResponseParseError(`LLM output does not match responseSchema: ${errors.join('; ')}`, {
    errors,
    output: summarizeOutput(output),
    schemaRequired: (schema as JsonSchema).required
  })
}

function collectErrors(value: unknown, schema: JsonSchema, path: string): string[] {
  const errors: string[] = []
  const label = path || 'root'

  // Null/undefined check
  if (value === null || value === undefined) {
    errors.push(`${label} is ${value === null ? 'null' : 'undefined'}`)
    return errors
  }

  // Type check
  if (schema.type) {
    const actualType = getJsonType(value)
    if (schema.type !== actualType) {
      errors.push(`${label}: expected type '${schema.type}', got '${actualType}'`)
      return errors // Can't check deeper if type is wrong
    }
  }

  // Enum check
  if (schema.enum && !schema.enum.includes(value)) {
    errors.push(`${label}: value '${String(value)}' not in enum [${schema.enum.map(String).join(', ')}]`)
  }

  // Const check
  if (schema.const !== undefined && value !== schema.const) {
    errors.push(`${label}: expected const '${String(schema.const)}', got '${String(value)}'`)
  }

  // Object: check required fields and property types
  if (schema.type === 'object' && schema.properties && typeof value === 'object' && value !== null) {
    const obj = value as Record<string, unknown>

    // Check required fields exist
    if (schema.required) {
      for (const field of schema.required) {
        if (!(field in obj)) {
          errors.push(`missing required field '${path ? `${path}.${field}` : field}'`)
        }
      }
    }

    // Validate each property that exists
    for (const [key, propSchema] of Object.entries(schema.properties)) {
      if (key in obj) {
        const propPath = path ? `${path}.${key}` : key
        errors.push(...collectErrors(obj[key], propSchema, propPath))
      }
    }
  }

  // Array: validate items
  if (schema.type === 'array' && schema.items && Array.isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      const itemPath = `${path}[${i}]`
      errors.push(...collectErrors(value[i], schema.items, itemPath))
    }
  }

  return errors
}

function getJsonType(value: unknown): string {
  if (Array.isArray(value)) return 'array'
  if (value === null) return 'null'
  return typeof value // 'string' | 'number' | 'boolean' | 'object'
}

/** Truncate output for error context to avoid bloating logs */
function summarizeOutput(output: unknown): unknown {
  if (output === null || output === undefined) return output
  const str = JSON.stringify(output)
  if (str.length <= 500) return output
  return str.substring(0, 500) + '...'
}
