//! Agent loader - main entry point for loading and executing agents

use crate::cache::CacheManager;
use crate::error::{Error, Result};
use crate::executor::{AgentExecutor, ExecutionResult};
use crate::nickel::{AgentDefinition, NickelEvaluator};
use crate::parser::MarkupParser;
use crate::transpiler::NickelTranspiler;
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex};

/// Agent loader - main entry point for 3-layer pipeline execution
pub struct AgentLoader {
    parser: MarkupParser,
    transpiler: NickelTranspiler,
    evaluator: NickelEvaluator,
    executor: AgentExecutor,
    cache: Option<Arc<Mutex<CacheManager>>>,
}

impl AgentLoader {
    /// Create new agent loader with cache enabled
    pub fn new() -> Self {
        Self {
            parser: MarkupParser::new(),
            transpiler: NickelTranspiler::new(),
            evaluator: NickelEvaluator::new(),
            executor: AgentExecutor::new(),
            cache: Some(Arc::new(Mutex::new(CacheManager::default()))),
        }
    }

    /// Create without cache
    pub fn without_cache() -> Self {
        Self {
            parser: MarkupParser::new(),
            transpiler: NickelTranspiler::new(),
            evaluator: NickelEvaluator::new(),
            executor: AgentExecutor::new(),
            cache: None,
        }
    }

    /// Load agent from file
    ///
    /// Executes the 3-layer pipeline:
    /// 1. Parse MDX → AST
    /// 2. Transpile AST → Nickel
    /// 3. Evaluate Nickel → AgentDefinition
    pub async fn load(&self, path: &Path) -> Result<AgentDefinition> {
        // Read file content
        let content = std::fs::read_to_string(path).map_err(|e| {
            Error::io(
                format!("Failed to read agent file: {:?}", path),
                e.to_string(),
            )
        })?;

        // Check cache for transpiled Nickel code
        let file_mtime = std::fs::metadata(path)
            .ok()
            .and_then(|m| m.modified().ok())
            .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
            .map(|d| d.as_secs())
            .unwrap_or(0);

        let path_str = path.to_string_lossy().to_string();

        // Try to get from cache
        let nickel_code = if let Some(cache_arc) = &self.cache {
            // Try to get from cache first
            let mut cache = cache_arc.lock().unwrap();
            if let Some(cached) = cache.get_transpiled(&path_str, file_mtime) {
                cached
            } else {
                // Not in cache, do full transpilation
                drop(cache); // Release lock before parsing

                let ast = self.parser.parse(&content)?;
                let nickel = self.transpiler.transpile(&ast)?;

                // Store in cache
                let mut cache_mut = cache_arc.lock().unwrap();
                // Ignore cache errors - we still have the nickel code to use
                cache_mut
                    .put_transpiled(&path_str, file_mtime, &nickel)
                    .ok();
                nickel
            }
        } else {
            // No cache, do full transpilation
            let ast = self.parser.parse(&content)?;
            self.transpiler.transpile(&ast)?
        };

        // Evaluate Nickel code to get AgentDefinition
        self.evaluator.evaluate(&nickel_code)
    }

    /// Execute agent
    ///
    /// Delegates to AgentExecutor for actual execution with LLM.
    /// Returns ExecutionResult with output, validation status, and metadata.
    pub async fn execute(
        &self,
        agent: &AgentDefinition,
        inputs: HashMap<String, serde_json::Value>,
    ) -> Result<ExecutionResult> {
        self.executor.execute(agent, inputs).await
    }

    /// Execute agent with streaming output
    ///
    /// The callback is invoked for each chunk of output as it arrives from the LLM.
    /// Useful for real-time display in CLI or web interfaces.
    pub async fn execute_streaming<F>(
        &self,
        agent: &AgentDefinition,
        inputs: HashMap<String, serde_json::Value>,
        on_chunk: F,
    ) -> Result<ExecutionResult>
    where
        F: FnMut(&str),
    {
        self.executor
            .execute_streaming(agent, inputs, on_chunk)
            .await
    }

    /// Load and execute in one call
    ///
    /// Convenience method that combines load() and execute().
    pub async fn load_and_execute(
        &self,
        path: &Path,
        inputs: HashMap<String, serde_json::Value>,
    ) -> Result<ExecutionResult> {
        let agent = self.load(path).await?;
        self.execute(&agent, inputs).await
    }

    /// Load and execute with streaming
    ///
    /// Convenience method that combines load() and execute_streaming().
    pub async fn load_and_execute_streaming<F>(
        &self,
        path: &Path,
        inputs: HashMap<String, serde_json::Value>,
        on_chunk: F,
    ) -> Result<ExecutionResult>
    where
        F: FnMut(&str),
    {
        let agent = self.load(path).await?;
        self.execute_streaming(&agent, inputs, on_chunk).await
    }
}

impl Default for AgentLoader {
    fn default() -> Self {
        Self::new()
    }
}
