/**
 * Agent class
 * Autonomous agents that self-orchestrate with LLM and tool calling
 * Uses universal protocol with vendor-independent message format
 */

import type { Contract, ExecutionContext } from '../../base/types'
import type { AgentConfig, IterationContext, AgentMemory, AgentDefinition, LLMAdapterFactory } from './types'
import type { Tool } from '../../tools/types'
import type { ModelConfig } from '../../llm/model-info'
import { createAgentLogger, type AgentScopedLogger } from '../observability/logging'
import { errorToString, getErrorDetails } from '../../../../platform/utils/error'
import { processReasoning } from '../reasoning/processor'
import { processActions } from '../actions/processor'
import { processMemory } from '../memory/processor'
import { MemoryManager } from '../memory/manager'
import { callLLMForAgentCompletion } from '../reasoning/adapters/agent-adapter-helpers'
import { zodToJsonSchema } from '@alcyone-labs/zod-to-json-schema'
import type { KnowledgeMap } from '../knowledge-map/types'
import { reloadKnowledgeMapTools } from '../knowledge-map/utils'
import type { z } from 'zod'
import { ZodError } from 'zod'
import {
  AgentInitializationError,
  AgentIterationError,
  AgentCancellationError,
  AgentCompletionError,
  AgentOutputValidationError,
  AgentMaxIterationsError,
  AgentTimeoutError,
  AgentStalledError
} from '../errors'
import { ExecutionError } from '../../base/errors'

/**
 * Create a per-execution shallow copy of the knowledge map
 * Prevents concurrent executions from mutating shared definition state
 * (node.loaded, node.prompt, and child node flattening)
 */
function initializeKnowledgeMap(knowledgeMap?: KnowledgeMap): KnowledgeMap | undefined {
  if (!knowledgeMap) return undefined
  return {
    nodes: Object.fromEntries(Object.entries(knowledgeMap.nodes).map(([id, node]) => [id, { ...node }]))
  }
}

/**
 * Autonomous agent that uses LLM reasoning and tool calling
 * to complete tasks without predefined orchestration
 *
 * @template TInput - Input type validated by contract.inputSchema
 * @template TOutput - Output type validated by contract.outputSchema
 */
export class Agent<TInput = unknown, TOutput = unknown> {
  // Base properties from definition
  public readonly config: AgentConfig
  public readonly contract: Contract
  private readonly toolRegistry: Map<string, Tool>
  public readonly modelConfig: ModelConfig
  private readonly knowledgeMap?: KnowledgeMap
  private readonly definition: AgentDefinition
  private readonly adapterFactory: LLMAdapterFactory

  // Derived properties (computed from definition)
  private readonly shouldGenerateOutput: boolean

  // Runtime state (initialized during execution)
  public memoryManager!: MemoryManager
  private logger!: AgentScopedLogger
  private executionContext!: ExecutionContext
  private iterationNumber: number = 0 // Current iteration number (used for memory context filtering)

  /**
   * Create a new agent instance from definition
   * Memory will be initialized during execution
   *
   * @param definition - Agent definition with config, contract, tools, and optional preloadMemory
   * @param adapterFactory - Factory for creating LLM adapters (decouples engine from provider SDKs)
   */
  constructor(definition: AgentDefinition, adapterFactory: LLMAdapterFactory) {
    this.definition = definition
    this.adapterFactory = adapterFactory
    this.config = definition.config
    this.contract = definition.contract
    this.modelConfig = definition.modelConfig // Store model config from definition
    this.knowledgeMap = initializeKnowledgeMap(definition.knowledgeMap)

    // Initialize tool registry with base tools
    this.toolRegistry = new Map()
    for (const tool of definition.tools) {
      this.toolRegistry.set(tool.name, tool)
    }

    // Derive shouldGenerateOutput from outputSchema presence (automatic detection)
    this.shouldGenerateOutput = !!definition.contract.outputSchema
  }

  /**
   * Execute the agent with validated input and context
   * Orchestrates the three lifecycle phases: initialization, iteration, completion
   *
   * @param input - Raw input (will be validated against contract.inputSchema)
   * @param context - Execution context (required for tracking, logging, and organization isolation)
   * @returns Validated output matching contract.outputSchema, or null if no output schema
   */
  async execute(input: TInput, context: ExecutionContext): Promise<TOutput | null> {
    // Store execution context for completion phase
    this.executionContext = context

    // Emit agent started event
    await context.onMessageEvent?.({ type: 'agent:started' })

    try {
      // Phase 1: Initialize
      await this.initialize(input, context)

      // Phase 2: Iterate (task execution)
      await this.iterate(context)

      // Phase 3: Complete (output generation)
      const output = await this.complete()

      // Emit agent completed event
      await context.onMessageEvent?.({ type: 'agent:completed' })

      return output
    } catch (error) {
      // Emit agent error event
      await context.onMessageEvent?.({ type: 'agent:error', error: String(error) })
      throw error
    }
  }

  /**
   * Register tools from a loaded knowledge node
   * Called by navigate_knowledge tool during execution
   *
   * @param tools - Array of tools to register
   * Note: Silently skips tools that are already registered
   */
  public registerTools(tools: Tool[]): void {
    for (const tool of tools) {
      if (!this.toolRegistry.has(tool.name)) {
        this.toolRegistry.set(tool.name, tool)
      }
      // Skip tools that are already registered (graceful handling)
    }
  }

  /**
   * Get all currently registered tools
   * Used for system prompt generation and introspection
   *
   * @returns Array of all registered tools
   */
  public getTools(): Tool[] {
    return Array.from(this.toolRegistry.values())
  }

  /**
   * Phase 1: Initialize the agent execution
   * Validates input, initializes memory manager
   *
   * @param input - Raw input to validate
   * @param context - Execution context
   */
  private async initialize(input: TInput, context: ExecutionContext): Promise<void> {
    const initStartTime = Date.now()

    try {
      // Create scoped logger for this execution
      // Pass sessionId if present in context (now directly on ExecutionMetadata)
      this.logger = createAgentLogger(context.logger, this.config.resourceId, context.sessionId)

      this.logger.lifecycle('initialization', 'started', {
        startTime: initStartTime
      })

      // Validate input
      const validatedInput = this.contract.inputSchema.parse(input) as TInput

      // Initialize memory manager (encapsulates all memory initialization)
      this.memoryManager = await this.initializeMemoryManager(validatedInput, context)

      const initEndTime = Date.now()
      this.logger.lifecycle('initialization', 'completed', {
        startTime: initStartTime,
        endTime: initEndTime,
        duration: initEndTime - initStartTime
      })
    } catch (error) {
      this.wrapAndLogError('initialization', initStartTime, error)
    }
  }

  /**
   * Initialize memory manager with preloaded memory and input entry
   * Encapsulates all memory initialization complexity
   *
   * Also handles cross-turn persistence: re-registers tools from knowledge nodes
   * that were loaded in previous session turns.
   *
   * @param validatedInput - Validated input to add to memory history
   * @param context - Execution context (passed to preloadMemory)
   * @returns Initialized MemoryManager instance
   */
  private async initializeMemoryManager(validatedInput: TInput, context: ExecutionContext): Promise<MemoryManager> {
    // Preload memory if callback defined
    let memory: AgentMemory
    if (this.definition.preloadMemory) {
      const preloadStartTime = Date.now()
      memory = await this.definition.preloadMemory(context)
      const preloadEndTime = Date.now()
      this.logger.action(
        'memory-preload',
        `Preloaded ${Object.keys(memory.sessionMemory).length} session memory entries`,
        0,
        preloadStartTime,
        preloadEndTime,
        preloadEndTime - preloadStartTime
      )

      // Reload tools from knowledge map if state exists (cross-turn persistence)
      await this.reloadKnowledgeMapTools(memory, context)
    } else {
      // Default: empty memory
      memory = {
        sessionMemory: {},
        history: []
      }
    }

    // Add input entry to history (iteration 0 = pre-iteration setup)
    const inputStartTime = Date.now()

    memory.history.push({
      type: 'input',
      content: JSON.stringify(validatedInput),
      timestamp: Date.now(),
      turnNumber: context.sessionTurnNumber ?? null,
      iterationNumber: 0
    })
    const inputEndTime = Date.now()

    this.logger.action(
      'memory-input',
      `Added input entry to history`,
      0,
      inputStartTime,
      inputEndTime,
      inputEndTime - inputStartTime
    )

    // Create and return memory manager (takes ownership of memory)
    return new MemoryManager(memory, this.config.constraints, this.logger)
  }

  /**
   * Reload tools from knowledge map state (cross-turn persistence)
   *
   * Reads the knowledge-map-state from sessionMemory and re-registers
   * tools from previously loaded knowledge nodes.
   *
   * @param memory - Agent memory with sessionMemory state
   * @param context - Execution context
   */
  private async reloadKnowledgeMapTools(memory: AgentMemory, context: ExecutionContext): Promise<void> {
    // Only reload if knowledge map exists
    if (!this.knowledgeMap) {
      return
    }

    const stateJson = memory.sessionMemory['knowledge-map-state']
    if (!stateJson) {
      return // No state to reload
    }

    const reloadStartTime = Date.now()

    try {
      const tools = await reloadKnowledgeMapTools(this.knowledgeMap, memory, context)

      // Register reloaded tools (skip duplicates gracefully)
      let registeredCount = 0
      let skippedCount = 0

      for (const tool of tools) {
        if (this.toolRegistry.has(tool.name)) {
          skippedCount++
        } else {
          this.toolRegistry.set(tool.name, tool)
          registeredCount++
        }
      }

      const reloadEndTime = Date.now()

      if (registeredCount > 0) {
        const state = JSON.parse(stateJson.content)
        this.logger.action(
          'knowledge-reload',
          `Reloaded ${registeredCount} tools from ${state.loadedNodes.length} knowledge nodes: ${state.loadedNodes.join(', ')}`,
          0,
          reloadStartTime,
          reloadEndTime,
          reloadEndTime - reloadStartTime
        )
      }

      if (skippedCount > 0) {
        this.logger.action(
          'knowledge-reload-skipped',
          `Skipped ${skippedCount} already-registered tools during reload`,
          0,
          reloadStartTime,
          reloadEndTime,
          reloadEndTime - reloadStartTime
        )
      }
    } catch (error) {
      const errorMessage = errorToString(error)
      const reloadEndTime = Date.now()
      this.logger.action(
        'knowledge-reload-failed',
        `Failed to reload knowledge map: ${errorMessage}`,
        0,
        reloadStartTime,
        reloadEndTime,
        reloadEndTime - reloadStartTime
      )
      // Continue execution - tools can be re-loaded on demand if needed
    }
  }

  /**
   * Phase 2: Run the agent iteration loop
   * Continues until LLM signals completion or max iterations reached
   *
   * @param context - Execution context
   */
  private async iterate(context: ExecutionContext): Promise<void> {
    const maxIterations = this.config.constraints?.maxIterations || 10
    let iteration = 1

    while (iteration <= maxIterations) {
      // Check for abort (timeout, stalled, or cancellation)
      if (context.signal?.aborted) {
        if (context.signal.reason === 'timeout') {
          throw new AgentTimeoutError(`Agent execution exceeded timeout (${this.config.constraints?.timeout}ms)`, {
            timeout: this.config.constraints?.timeout ?? 0,
            iteration
          })
        }
        if (context.signal.reason === 'stalled') {
          throw new AgentStalledError('Execution stalled: no heartbeat received within threshold', { iteration })
        }
        throw new AgentCancellationError('Execution cancelled by user', { iteration })
      }

      // Write heartbeat (non-fatal if it fails)
      try {
        await context.onHeartbeat?.()
      } catch {
        // Heartbeat failure is non-fatal — execution continues
      }

      const result = await this.runIteration(iteration, context)
      if (result.shouldComplete) {
        return // Exit iteration loop (output generated in completion phase)
      }

      iteration++
    }

    // Max iterations exceeded
    throw new AgentMaxIterationsError(`Agent exceeded maximum iterations (${maxIterations})`, {
      maxIterations,
      currentIteration: maxIterations
    })
  }

  /**
   * Run a single iteration of the agent loop
   *
   * Three-phase execution:
   * 1. REASON - Query LLM for next actions, store reasoning to memory
   * 2. MEMORY - Process memory operations (set/delete session memory entries)
   * 3. ACT - Execute planned actions, store tool results to memory
   *
   * @param iteration - Current iteration number (1-based)
   * @param context - Execution context
   * @returns Iteration result with completion flag (no finalAnswer - generated in completion phase)
   */
  private async runIteration(iteration: number, context: ExecutionContext): Promise<{ shouldComplete: boolean }> {
    const iterationStartTime = Date.now()

    // Track current iteration number (used by toContext() for memory filtering)
    this.iterationNumber = iteration

    try {
      this.logger.lifecycle('iteration', 'started', {
        iteration,
        startTime: iterationStartTime
      })

      // Build iteration context
      const iterationContext: IterationContext = this.buildIterationContext(iteration, context)

      // Phase 1: REASON - Get LLM's decision and store reasoning to memory
      const response = await processReasoning(iterationContext)

      // Phase 2: MEMORY - Process memory operations (agent-directed)
      await processMemory(this.memoryManager, response, this.logger, iteration)

      // Phase 3: ACT - Execute LLM's plan and store tool results to memory
      const { shouldComplete } = await processActions(iterationContext, response)

      // Log completion
      this.logIterationEnd(iteration, iterationStartTime)

      // Return result
      return { shouldComplete }
    } catch (error) {
      this.wrapAndLogError('iteration', iterationStartTime, error, { iteration })
    }
  }

  /**
   * Log iteration end (success only - failures handled by wrapAndLogError)
   */
  private logIterationEnd(iteration: number, startTime: number): void {
    const endTime = Date.now()
    const duration = endTime - startTime

    this.logger.lifecycle('iteration', 'completed', {
      iteration,
      startTime,
      endTime,
      duration
    })
  }

  /**
   * Phase 3: Complete the agent execution
   * Generates and validates structured output from execution history (if output schema present)
   * Captures memory snapshot for persistence
   *
   * @returns Validated output or null if no output schema
   */
  private async complete(): Promise<TOutput | null> {
    const completionStartTime = Date.now()

    try {
      this.logger.lifecycle('completion', 'started', {
        startTime: completionStartTime
      })

      // Conditionally generate output based on schema presence
      let output: TOutput | null = null
      let attempts: number | undefined

      if (this.shouldGenerateOutput) {
        // Generate output (includes retry logic internally)
        const result = await this.generateFinalOutput()
        output = result.output
        attempts = result.attempts
      } else {
        // Skip output generation (side-effect only agent)
        this.logger.action(
          'completion-skipped',
          'Output generation skipped (no output schema)',
          0,
          completionStartTime,
          completionStartTime,
          0
        )
      }

      // Capture snapshot automatically (MemoryManager stores it)
      this.memoryManager.toSnapshot()

      const snapshot = this.memoryManager.getSnapshot()!
      const completionEndTime = Date.now()
      this.logger.lifecycle('completion', 'completed', {
        startTime: completionStartTime,
        endTime: completionEndTime,
        duration: completionEndTime - completionStartTime,
        attempts,
        memorySize: {
          sessionMemoryKeys: Object.keys(snapshot.sessionMemory).length,
          historyEntries: snapshot.history.length
        }
      })

      return output
    } catch (error) {
      this.wrapAndLogError('completion', completionStartTime, error)
    }
  }

  /**
   * Generate final output from execution history with retry logic
   * Handles both initial generation and validation retry internally
   *
   * @returns Object with validated output and attempt count
   */
  private async generateFinalOutput(): Promise<{ output: TOutput; attempts: number }> {
    // Safety check: Ensure outputSchema exists before generating output
    if (!this.contract.outputSchema) {
      throw new AgentInitializationError(
        `Internal error: generateFinalOutput called but contract.outputSchema is undefined`,
        {
          agentId: this.config.resourceId,
          reason: 'missing_output_schema'
        }
      )
    }

    const outputSchema = zodToJsonSchema(this.contract.outputSchema as z.ZodSchema<any>, {
      $refStrategy: 'none',
      errorMessages: true
    })

    // Use model's configured temperature (some models like GPT-5-mini only support default)
    const modelTemperature = this.modelConfig.temperature ?? 0.7

    // Attempt 1: Initial generation
    const initialOutput = await this.callLLMForOutput(
      this.buildOutputGenerationPrompt(),
      outputSchema,
      modelTemperature,
      'output-generation'
    )

    const initialResult = this.contract.outputSchema.safeParse(initialOutput)

    if (initialResult.success) {
      return { output: initialResult.data as TOutput, attempts: 1 }
    }

    // Attempt 1 failed - log and retry with error context
    const validationTime = Date.now()
    this.logger.action(
      'completion-validation-failed',
      `Output validation failed: ${initialResult.error.message}. Retrying with error context...`,
      0,
      validationTime,
      validationTime,
      0
    )

    // Attempt 2: Retry with validation errors (use same temperature)
    const retryPrompt = this.buildRetryPrompt(initialOutput, initialResult.error)
    const retryOutput = await this.callLLMForOutput(
      retryPrompt,
      outputSchema,
      modelTemperature,
      'output-generation-retry'
    )

    // Strict validation on retry (wrap Zod errors in typed core error)
    try {
      const finalOutput = this.contract.outputSchema.parse(retryOutput) as TOutput
      return { output: finalOutput, attempts: 2 }
    } catch (error) {
      // Don't let Zod error bubble up - wrap in typed core error
      throw new AgentOutputValidationError('Agent output validation failed after retry', {
        agentId: this.config.resourceId,
        attempts: 2,
        zodError: error instanceof ZodError ? error.format() : error
      })
    }
  }

  /**
   * Call LLM for output generation
   * Shared logic for initial and retry attempts
   *
   * @param systemPrompt - System prompt for output generation
   * @param outputSchema - JSON schema for output validation
   * @param temperature - LLM temperature setting
   * @param actionType - Action type for logging (output-generation or output-generation-retry)
   * @returns Generated structured output
   */
  private async callLLMForOutput(
    systemPrompt: string,
    outputSchema: unknown,
    temperature: number,
    actionType: 'output-generation' | 'output-generation-retry'
  ): Promise<unknown> {
    const generationStartTime = Date.now()

    try {
      this.logger.action(actionType, `${actionType} started`, 0, generationStartTime, generationStartTime, 0)

      // Get attempt number from action type (1 for initial, 2 for retry)
      const attempt = actionType === 'output-generation' ? 1 : 2

      const adapter = this.adapterFactory(
        this.modelConfig,
        this.executionContext?.aiUsageCollector,
        'agent-completion',
        {
          type: 'agent-completion',
          attempt: attempt as 1 | 2,
          sessionId: this.executionContext?.sessionId,
          turnNumber: this.executionContext?.sessionTurnNumber
        }
      )

      // Call LLM for completion using agent-specific helper (returns output directly)
      const structuredOutput = await callLLMForAgentCompletion(adapter, {
        systemPrompt,
        memoryContext: this.memoryManager.toContext(this.iterationNumber, this.executionContext?.sessionTurnNumber),
        outputSchema,
        constraints: {
          maxOutputTokens: this.modelConfig.maxOutputTokens,
          temperature
        },
        model: this.modelConfig.model,
        signal: this.executionContext?.signal
      })

      const generationEndTime = Date.now()
      const generationDuration = generationEndTime - generationStartTime
      this.logger.action(
        actionType,
        `${actionType} completed (${generationDuration}ms)`,
        0,
        generationStartTime,
        generationEndTime,
        generationDuration
      )

      return structuredOutput
    } catch (error) {
      const errorMessage = errorToString(error)
      const generationEndTime = Date.now()
      const generationDuration = generationEndTime - generationStartTime
      this.logger.action(
        actionType,
        `${actionType} failed: ${errorMessage} (${generationDuration}ms)`,
        0,
        generationStartTime,
        generationEndTime,
        generationDuration
      )
      throw error
    }
  }

  /**
   * Build system prompt for output generation phase
   * Instructs LLM to synthesize execution history into structured output
   * Note: Only called from generateFinalOutput() which ensures outputSchema exists
   *
   * @returns System prompt for completion phase
   */
  private buildOutputGenerationPrompt(): string {
    // Type assertion safe: only called from generateFinalOutput() which checks outputSchema exists
    const schema = this.contract.outputSchema!

    const schemaJson = zodToJsonSchema(schema as z.ZodSchema<any>, {
      $refStrategy: 'none',
      errorMessages: true
    })

    return `
You have completed a task. Generate the final output based on the execution history.

## Output Schema

The output MUST match this exact structure:

${JSON.stringify(schemaJson, null, 2)}

## Task Context

Review the execution history in the memory context below. Extract and structure the relevant information according to the schema.

## Requirements

- Include all required fields
- Use exact field names and types
- Ensure data integrity (no hallucination)
- Base output on actual execution results

Generate the final output now.
`.trim()
  }

  /**
   * Build retry prompt with validation error context
   *
   * @param failedOutput - The output that failed validation
   * @param validationError - Zod validation error with details
   * @returns System prompt for retry attempt
   */
  private buildRetryPrompt(failedOutput: unknown, validationError: z.ZodError): string {
    return `
${this.buildOutputGenerationPrompt()}

## Previous Attempt (FAILED VALIDATION)

${JSON.stringify(failedOutput, null, 2)}

## Validation Errors

${validationError.issues.map((e) => `- ${e.path.join('.')}: ${e.message}`).join('\n')}

Fix the errors and generate a valid output.
`.trim()
  }

  /**
   * Get memory snapshot after execution
   * Delegates to MemoryManager.getSnapshot()
   * @returns Memory snapshot (only available after execute() completes)
   */
  public getMemorySnapshot(): AgentMemory | undefined {
    return this.memoryManager.getSnapshot()
  }

  /**
   * Build the execution context for the agent
   * @param iteration - Current iteration number (1-based)
   * @param context - Execution context
   * @returns Agent execution context
   */
  private buildIterationContext(iteration: number, context: ExecutionContext): IterationContext {
    return {
      config: this.config,
      contract: this.contract,
      toolRegistry: this.toolRegistry,
      memoryManager: this.memoryManager,
      executionContext: context,
      iteration,
      logger: this.logger,
      modelConfig: this.modelConfig,
      adapterFactory: this.adapterFactory,
      knowledgeMap: this.knowledgeMap
    }
  }

  /**
   * Helper to wrap errors with lifecycle logging
   * @param phase - Lifecycle phase (initialization, iteration, completion)
   * @param startTime - Phase start timestamp
   * @param error - Original error
   * @param context - Additional error context
   */
  private wrapAndLogError(
    phase: 'initialization' | 'iteration' | 'completion',
    startTime: number,
    error: unknown,
    context?: Record<string, unknown>
  ): never {
    const errorMessage = errorToString(error)
    const errorDetails = getErrorDetails(error)
    const endTime = Date.now()
    const duration = endTime - startTime

    // Logger may not be created yet if error occurs during initialization
    if (this.logger) {
      const logContext: {
        error: string
        errorDetails: ReturnType<typeof getErrorDetails>
        startTime: number
        endTime: number
        duration: number
        iteration?: number
      } = {
        error: errorMessage,
        errorDetails, // Include full error details for debugging
        startTime,
        endTime,
        duration
      }

      if (phase === 'iteration' && context?.iteration) {
        logContext.iteration = context.iteration as number
      }

      this.logger.lifecycle(phase, 'failed', logContext)
    }

    // If the error is already a typed ExecutionError, preserve it.
    // Phase info is already captured in the lifecycle log above.
    if (error instanceof ExecutionError) {
      throw error
    }

    // Wrap non-ExecutionError instances in phase-appropriate class
    const errorContext = {
      ...(context || {}),
      agentId: this.config.resourceId,
      originalError: error instanceof Error ? error.name : 'unknown',
      ...errorDetails // Include validation errors, stack traces, etc.
    }

    if (phase === 'initialization') {
      throw new AgentInitializationError(errorMessage, errorContext)
    } else if (phase === 'iteration') {
      throw new AgentIterationError(errorMessage, errorContext)
    } else {
      // completion phase
      throw new AgentCompletionError(errorMessage, errorContext)
    }
  }
}
