use crate::{
    invocation::{SkillContext, SkillInput},
    outcome::SkillOutcome,
    skill::{Skill, SkillId},
};
use serde_json::json;
use std::collections::{HashMap, HashSet, VecDeque};

/// Ripple: impact analysis — given a file or symbol, list files that depend on it.
pub struct RippleSkill;

impl Skill for RippleSkill {
    fn id(&self) -> SkillId {
        SkillId("ripple")
    }
    fn display_name(&self) -> &str {
        "Ripple"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "path": { "type": "string", "description": "File whose dependents to find" },
                "root": { "type": "string", "default": ".", "description": "Project root to scan" },
                "depth": { "type": "integer", "default": 2, "description": "Transitive depth (1 = direct only)" }
            },
            "required": ["path"]
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "dependents": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "path": { "type": "string" },
                            "depth": { "type": "integer" }
                        }
                    }
                },
                "total": { "type": "integer" }
            }
        })
    }

    fn execute(&self, input: SkillInput, _ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let target = input.get_str("path").unwrap_or("").to_string();
        let root = input.get_str("root").unwrap_or(".").to_string();
        let max_depth = input.get_u64("depth").unwrap_or(2) as usize;

        let import_map = build_import_map(&root);
        let dependents = bfs_dependents(&target, &import_map, max_depth);
        let total = dependents.len();

        let result: Vec<serde_json::Value> = dependents
            .into_iter()
            .map(|(p, d)| json!({ "path": p, "depth": d }))
            .collect();

        Ok(SkillOutcome::success(
            "ripple",
            json!({ "dependents": result, "total": total }),
        ))
    }
}

fn build_import_map(root: &str) -> HashMap<String, Vec<String>> {
    let mut map: HashMap<String, Vec<String>> = HashMap::new();
    let root_path = std::path::Path::new(root);
    collect_imports(root_path, &mut map);
    map
}

fn collect_imports(dir: &std::path::Path, map: &mut HashMap<String, Vec<String>>) {
    let Ok(rd) = std::fs::read_dir(dir) else {
        return;
    };
    for entry in rd.flatten() {
        let path = entry.path();
        let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
        if name.starts_with('.') || name == "target" || name == "node_modules" {
            continue;
        }
        if path.is_dir() {
            collect_imports(&path, map);
        } else if path.is_file() {
            let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
            if !matches!(ext, "rs" | "ts" | "tsx" | "js" | "jsx" | "py" | "go") {
                continue;
            }
            let Ok(content) = std::fs::read_to_string(&path) else {
                continue;
            };
            let file_str = path.to_string_lossy().to_string();
            let deps = extract_imports(&content, &file_str);
            map.insert(file_str, deps);
        }
    }
}

pub(crate) fn extract_imports(content: &str, _file: &str) -> Vec<String> {
    let mut deps = Vec::new();
    for line in content.lines() {
        let t = line.trim();
        // Rust: use crate::..., mod xyz
        if t.starts_with("use ") || t.starts_with("mod ") {
            deps.push(t.to_string());
        }
        // TS/JS: import ... from "..."
        if t.starts_with("import ") {
            if let Some(from_idx) = t.rfind(" from ") {
                let rest = t[from_idx + 6..]
                    .trim()
                    .trim_end_matches(';')
                    .trim()
                    .trim_matches('"')
                    .trim_matches('\'');
                deps.push(rest.to_string());
            }
        }
        // Python: from x import y / import x
        if t.starts_with("from ") || (t.starts_with("import ") && !t.starts_with("import {")) {
            deps.push(t.to_string());
        }
        // Go: import "..."
        if t.starts_with('"') && t.ends_with('"') && t.contains('/') {
            deps.push(t.trim_matches('"').to_string());
        }
    }
    deps
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rust_use_statements_extracted() {
        let content = "use crate::atlas::skills;\nuse std::collections::HashMap;\nlet x = 1;";
        let deps = extract_imports(content, "src/main.rs");
        assert!(deps.contains(&"use crate::atlas::skills;".to_string()));
        assert!(deps.contains(&"use std::collections::HashMap;".to_string()));
        assert!(!deps.iter().any(|d| d.contains("let x")));
    }

    #[test]
    fn ts_import_from_extracted() {
        let content = "import { foo } from './utils';";
        let deps = extract_imports(content, "src/index.ts");
        assert!(deps.contains(&"./utils".to_string()));
    }

    #[test]
    fn python_from_import_extracted() {
        let content = "from pathlib import Path\nfrom collections import OrderedDict";
        let deps = extract_imports(content, "main.py");
        assert!(deps.contains(&"from pathlib import Path".to_string()));
    }

    #[test]
    fn plain_code_produces_no_imports() {
        let content = "let x = 1;\nconst y = 2;\nfoo();\n";
        let deps = extract_imports(content, "main.ts");
        assert!(deps.is_empty());
    }

    #[test]
    fn bfs_finds_direct_dependent_at_depth_1() {
        let mut map = HashMap::new();
        // consumer.rs imports "alpha" (our target)
        map.insert(
            "src/consumer.rs".to_string(),
            vec!["use crate::alpha;".to_string()],
        );
        // unrelated.rs does not import alpha
        map.insert(
            "src/unrelated.rs".to_string(),
            vec!["let z = 9;".to_string()],
        );

        let deps = bfs_dependents("src/alpha.rs", &map, 2);
        assert_eq!(deps.len(), 1);
        assert_eq!(deps[0].0, "src/consumer.rs");
        assert_eq!(deps[0].1, 1);
    }

    #[test]
    fn bfs_returns_empty_when_no_dependents() {
        let map = HashMap::new();
        let deps = bfs_dependents("src/isolated.rs", &map, 3);
        assert!(deps.is_empty());
    }

    #[test]
    fn bfs_finds_transitive_dependent_at_depth_2() {
        let mut map = HashMap::new();
        // foo.rs imports quux
        map.insert(
            "src/foo.rs".to_string(),
            vec!["use crate::quux;".to_string()],
        );
        // baz.rs imports foo
        map.insert(
            "src/baz.rs".to_string(),
            vec!["use crate::foo;".to_string()],
        );

        let deps = bfs_dependents("src/quux.rs", &map, 2);
        let paths: Vec<&str> = deps.iter().map(|(p, _)| p.as_str()).collect();
        assert!(paths.contains(&"src/foo.rs"), "direct dependent missing");
        assert!(
            paths.contains(&"src/baz.rs"),
            "transitive dependent missing"
        );
        let baz_depth = deps.iter().find(|(p, _)| p == "src/baz.rs").unwrap().1;
        assert_eq!(baz_depth, 2);
    }
}

pub(crate) fn bfs_dependents(
    target: &str,
    import_map: &HashMap<String, Vec<String>>,
    max_depth: usize,
) -> Vec<(String, usize)> {
    let target_base = std::path::Path::new(target)
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or(target)
        .to_string();

    let mut visited: HashSet<String> = HashSet::new();
    let mut result = Vec::new();
    let mut queue: VecDeque<(String, usize)> = VecDeque::new();

    // Seed: files that directly import the target
    for (file, deps) in import_map {
        if deps.iter().any(|d| d.contains(&target_base)) {
            queue.push_back((file.clone(), 1));
        }
    }

    while let Some((file, depth)) = queue.pop_front() {
        if visited.contains(&file) || depth > max_depth {
            continue;
        }
        visited.insert(file.clone());
        result.push((file.clone(), depth));

        if depth < max_depth {
            let file_base = std::path::Path::new(&file)
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("")
                .to_string();
            for (f2, deps) in import_map {
                if !visited.contains(f2) && deps.iter().any(|d| d.contains(&file_base)) {
                    queue.push_back((f2.clone(), depth + 1));
                }
            }
        }
    }

    result
}
