use anyhow::{Result, anyhow};
use common_contracts::configs::{ContextSelectorConfig, SelectionMode, LlmConfig};
use common_contracts::config_models::LlmProvidersConfig;
use workflow_context::{Vgcs, EntryKind, ContextStore};
use globset::{GlobBuilder, GlobMatcher};
use std::sync::Arc;
use std::collections::HashMap;
use tracing::info; 

use crate::llm_client::LlmClient;

pub struct ResolutionResult {
    pub paths: Vec<String>,
    pub trace: String,
}

pub struct ContextResolver {
    vgcs: Arc<Vgcs>,
}

impl ContextResolver {
    pub fn new(vgcs: Arc<Vgcs>) -> Self {
        Self { vgcs }
    }

    pub async fn resolve_input(
        &self,
        selector: &ContextSelectorConfig,
        req_id: &str,
        commit_hash: &str,
        variables: &HashMap<String, String>,
        llm_providers: &LlmProvidersConfig,
        analysis_prompt: &str, 
    ) -> Result<ResolutionResult> {
        match selector {
            SelectionMode::Manual { rules } => {
                let resolved_rules = rules.iter().map(|r| {
                    let mut rule = r.clone();
                    for (k, v) in variables {
                        rule = rule.replace(&format!("{{{{{}}}}}", k), v);
                    }
                    rule
                }).collect::<Vec<_>>();

                self.resolve_manual(&resolved_rules, req_id, commit_hash).await
            }
            SelectionMode::Auto { llm_config } => {
                let system_prompt = "You are an intelligent file selector for a financial analysis system. \
                Your goal is to select the specific files from the repository that are necessary to fulfill the user's analysis request.\n\
                Return ONLY a JSON array of string file paths (e.g. [\"path/to/file1\", \"path/to/file2\"]). \
                Do not include any explanation, markdown formatting, or code blocks.\n\
                IMPORTANT: Ignore any files starting with an underscore (e.g. '_metrics_dump.json', '_execution.md'). \
                These are internal debug artifacts and should NOT be selected for analysis.";

                let user_prompt = format!(
                    "I need to perform the following analysis task:\n\n\"{}\"\n\n\
                     Below is the current file structure of the repository. Please select the files that contain the data needed for this task.\n\
                     (Note: 'financials.json' usually contains financial statements, 'profile.json' contains company info, 'news.json' contains news).",
                    analysis_prompt
                );

                self.resolve_with_llm(system_prompt, &user_prompt, llm_config.as_ref(), llm_providers, req_id, commit_hash, "Auto").await
            }
            SelectionMode::Hybrid { selection_prompt, llm_config } => {
                let mut user_req = selection_prompt.clone();
                 // Replace variables in prompt
                for (k, v) in variables {
                    user_req = user_req.replace(&format!("{{{{{}}}}}", k), v);
                }

                let system_prompt = "You are an intelligent file selector. Return ONLY a JSON array of string file paths.";
                let user_prompt = format!("Request: {}\n\nTask Context: {}\n\nSelect relevant files from the tree below.", user_req, analysis_prompt);

                self.resolve_with_llm(system_prompt, &user_prompt, llm_config.as_ref(), llm_providers, req_id, commit_hash, "Hybrid").await
            }
        }
    }

    async fn resolve_manual(
        &self,
        rules: &[String],
        req_id: &str,
        commit_hash: &str,
    ) -> Result<ResolutionResult> {
        let mut matchers = Vec::new();

        for rule in rules {
             let glob = GlobBuilder::new(rule)
                .literal_separator(true)
                .build()
                .map_err(|e| anyhow!("Invalid glob pattern '{}': {}", rule, e))?;
            matchers.push(glob.compile_matcher());
        }

        let mut files = Vec::new();
        self.traverse(req_id, commit_hash, "", &matchers, &mut files, true)?;
        
        let paths = files.into_iter().map(|(p, _)| p).collect::<Vec<_>>();

        // Generate Trace
        let mut trace = String::new();
        trace.push_str("# Context Selection Trace: Manual\n\n");
        trace.push_str("## Rules\n");
        for rule in rules {
            trace.push_str(&format!("- `{}`\n", rule));
        }
        trace.push_str("\n## Matched Files\n");
        for file in &paths {
            trace.push_str(&format!("- `{}`\n", file));
        }

        Ok(ResolutionResult {
            paths,
            trace,
        })
    }

    #[allow(clippy::too_many_arguments)]
    async fn resolve_with_llm(
        &self,
        system_prompt: &str,
        user_instruction: &str,
        llm_config: Option<&LlmConfig>,
        providers: &LlmProvidersConfig,
        req_id: &str,
        commit_hash: &str,
        mode_name: &str,
    ) -> Result<ResolutionResult> {
        // 1. List all files with metadata
        let mut all_files = Vec::new();
        // Pass empty matchers to list everything
        self.traverse(req_id, commit_hash, "", &[], &mut all_files, false)?;

        if all_files.is_empty() {
            return Ok(ResolutionResult {
                paths: vec![],
                trace: format!("# Context Selection Trace: {}\n\nNo files found in repository to select from.", mode_name),
            });
        }

        // 2. Construct Tree String with Metadata
        let tree_str = all_files.iter().map(|(path, meta)| {
            format!("{} ({})", path, meta)
        }).collect::<Vec<_>>().join("\n");

        // 3. Create Client
        let client = self.create_llm_client(llm_config, providers)?;

        // 4. Call LLM
        let full_user_prompt = format!("{}\n\n---\nFile Tree (Format: Path (Size | Lines | Words)):\n{}", user_instruction, tree_str);
        
        info!("Calling LLM for file selection...");
        let response = client.chat_completion(system_prompt, &full_user_prompt).await?;

        // 5. Parse Response
        // Clean up markdown code blocks if present
        let cleaned = response.trim();
        let cleaned = if cleaned.starts_with("```json") {
            cleaned.trim_start_matches("```json").trim_end_matches("```").trim()
        } else if cleaned.starts_with("```") {
             cleaned.trim_start_matches("```").trim_end_matches("```").trim()
        } else {
            cleaned
        };

        let paths: Vec<String> = serde_json::from_str(cleaned)
            .map_err(|e| anyhow!("Failed to parse LLM response as JSON list: {}. Response: {}", e, response))?;
            
        // Validate paths exist in all_files to prevent hallucination
        let all_paths_set: std::collections::HashSet<&String> = all_files.iter().map(|(p, _)| p).collect();
        let valid_paths: Vec<String> = paths.into_iter()
            .filter(|p| all_paths_set.contains(p))
            .collect();
            
        info!("LLM selected {} valid files out of {} total.", valid_paths.len(), all_files.len());
        
        // Generate Trace
        let mut trace = String::new();
        trace.push_str(&format!("# Context Selection Trace: {}\n\n", mode_name));
        
        trace.push_str("## System Prompt\n");
        trace.push_str("```\n");
        trace.push_str(system_prompt);
        trace.push_str("\n```\n\n");

        trace.push_str("## Full User Prompt (Intent + Context)\n");
        trace.push_str("```\n");
        trace.push_str(&full_user_prompt);
        trace.push_str("\n```\n\n");

        trace.push_str("## Model Output\n");
        trace.push_str(cleaned);
        trace.push_str("\n\n");

        trace.push_str("## Final Selected Files\n");
        for path in &valid_paths {
            trace.push_str(&format!("- `{}`\n", path));
        }
        
        Ok(ResolutionResult {
            paths: valid_paths,
            trace,
        })
    }

    fn traverse(
        &self,
        req_id: &str,
        commit_hash: &str,
        path: &str,
        matchers: &[GlobMatcher],
        results: &mut Vec<(String, String)>, // (Path, MetadataString)
        filter_mode: bool, // If true, use matchers. If false, list all.
    ) -> Result<()> {
        // list_dir might fail if path is not dir, but we only recurse on dirs.
        // root "" is dir.
        let entries = self.vgcs.list_dir(req_id, commit_hash, path)?;

        for entry in entries {
            // Skip internal files
            if entry.name.starts_with(".") || entry.name == "index.md" || entry.name == "_meta.json" {
                continue;
            }

            let full_path = if path.is_empty() || path == "/" {
                entry.name.clone()
            } else {
                format!("{}/{}", path, entry.name)
            };

            match entry.kind {
                EntryKind::File => {
                    let should_include = if !filter_mode {
                        true
                    } else {
                         matchers.iter().any(|m| m.is_match(&full_path))
                    };

                    if should_include {
                        let meta_str = format!(
                            "Size: {}, Lines: {}, Words: {}", 
                            entry.size.unwrap_or(0), 
                            entry.line_count.unwrap_or(0), 
                            entry.word_count.unwrap_or(0)
                        );
                        results.push((full_path.clone(), meta_str));
                    }
                }
                EntryKind::Dir => {
                    self.traverse(req_id, commit_hash, &full_path, matchers, results, filter_mode)?;
                }
            }
        }
        Ok(())
    }

    fn create_llm_client(&self, llm_config: Option<&LlmConfig>, providers: &LlmProvidersConfig) -> Result<LlmClient> {
        let model_id = llm_config.and_then(|c| c.model_id.as_deref()).unwrap_or("default");
        
        let mut target_provider = None;
        for provider in providers.values() {
            if provider.models.iter().any(|m| m.model_id == model_id) {
                target_provider = Some(provider);
                break;
            }
        }
        
        // Fallback to first provider if default
        if target_provider.is_none() && model_id == "default" {
             target_provider = providers.values().next();
        }

        let provider = target_provider.ok_or_else(|| anyhow!("No provider found for model {}", model_id))?;

        Ok(LlmClient::new(
            provider.api_base_url.clone(),
            provider.api_key.clone(),
            model_id.to_string(),
        ))
    }
}
