use crate::{
    invocation::{SkillContext, SkillInput},
    outcome::SkillOutcome,
    skill::{Skill, SkillId},
};
use forge::budget::estimator::TokenEstimator;
use serde_json::json;
use std::collections::HashMap;

/// Panorama: high-level project overview — languages, entry points, key directories, top symbols.
pub struct PanoramaSkill;

impl Skill for PanoramaSkill {
    fn id(&self) -> SkillId {
        SkillId("panorama")
    }
    fn display_name(&self) -> &str {
        "Panorama"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "root": { "type": "string", "default": ".", "description": "Project root" },
                "max_depth": { "type": "integer", "default": 3 }
            }
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "summary": { "type": "string" },
                "languages": { "type": "object" },
                "entry_points": { "type": "array", "items": { "type": "string" } },
                "top_dirs": { "type": "array", "items": { "type": "string" } },
                "file_count": { "type": "integer" },
                "tokens": { "type": "integer" }
            }
        })
    }

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

        let root_path = std::path::Path::new(&root);
        let mut lang_counts: HashMap<&'static str, u32> = HashMap::new();
        let mut top_dirs: Vec<String> = Vec::new();
        let mut entry_points: Vec<String> = Vec::new();
        let mut file_count = 0u32;

        scan_dir(
            root_path,
            0,
            max_depth,
            &mut lang_counts,
            &mut top_dirs,
            &mut entry_points,
            &mut file_count,
        );

        // Sort languages by count
        let mut lang_vec: Vec<(&str, u32)> = lang_counts.into_iter().collect();
        lang_vec.sort_by_key(|b| std::cmp::Reverse(b.1));
        top_dirs.sort();
        top_dirs.dedup();
        top_dirs.truncate(10);

        let lang_obj: serde_json::Value = lang_vec
            .iter()
            .take(10)
            .map(|(l, c)| (l.to_string(), json!(c)))
            .collect::<serde_json::Map<_, _>>()
            .into();

        let top_langs: Vec<&str> = lang_vec.iter().take(3).map(|(l, _)| *l).collect();
        let summary = format!(
            "{} files · {} — entry points: {}",
            file_count,
            top_langs.join(", "),
            entry_points
                .first()
                .cloned()
                .unwrap_or_else(|| "none".into())
        );

        let tokens = TokenEstimator::count(&summary);

        Ok(SkillOutcome::success(
            "panorama",
            json!({
                "summary": summary,
                "languages": lang_obj,
                "entry_points": entry_points,
                "top_dirs": top_dirs,
                "file_count": file_count,
                "tokens": tokens
            }),
        )
        .with_tokens(tokens))
    }
}

static ENTRY_PATTERNS: &[&str] = &[
    "main.rs",
    "main.go",
    "main.py",
    "index.ts",
    "index.js",
    "app.ts",
    "app.js",
    "server.ts",
    "server.js",
    "manage.py",
    "Cargo.toml",
    "package.json",
    "go.mod",
    "pyproject.toml",
    "setup.py",
];

fn ext_to_lang(ext: &str) -> Option<&'static str> {
    Some(match ext {
        "rs" => "Rust",
        "ts" | "tsx" => "TypeScript",
        "js" | "jsx" | "mjs" | "cjs" => "JavaScript",
        "py" => "Python",
        "go" => "Go",
        "java" => "Java",
        "kt" => "Kotlin",
        "swift" => "Swift",
        "rb" => "Ruby",
        "php" => "PHP",
        "zig" => "Zig",
        "c" | "h" => "C",
        "cpp" | "cc" | "cxx" | "hpp" => "C++",
        "cs" => "C#",
        "dart" => "Dart",
        "ex" | "exs" => "Elixir",
        "sh" | "bash" => "Bash",
        "scala" => "Scala",
        _ => return None,
    })
}

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

    #[test]
    fn rs_maps_to_rust() {
        assert_eq!(ext_to_lang("rs"), Some("Rust"));
    }

    #[test]
    fn ts_maps_to_typescript() {
        assert_eq!(ext_to_lang("ts"), Some("TypeScript"));
    }

    #[test]
    fn tsx_maps_to_typescript() {
        assert_eq!(ext_to_lang("tsx"), Some("TypeScript"));
    }

    #[test]
    fn py_maps_to_python() {
        assert_eq!(ext_to_lang("py"), Some("Python"));
    }

    #[test]
    fn go_maps_to_go() {
        assert_eq!(ext_to_lang("go"), Some("Go"));
    }

    #[test]
    fn sh_maps_to_bash() {
        assert_eq!(ext_to_lang("sh"), Some("Bash"));
    }

    #[test]
    fn cpp_maps_to_cpp() {
        assert_eq!(ext_to_lang("cpp"), Some("C++"));
    }

    #[test]
    fn zig_maps_to_zig() {
        assert_eq!(ext_to_lang("zig"), Some("Zig"));
    }

    #[test]
    fn unknown_ext_returns_none() {
        assert_eq!(ext_to_lang("xyz"), None);
        assert_eq!(ext_to_lang("md"), None);
    }

    #[test]
    fn entry_patterns_includes_main_rs() {
        assert!(ENTRY_PATTERNS.contains(&"main.rs"));
    }

    #[test]
    fn entry_patterns_includes_package_json() {
        assert!(ENTRY_PATTERNS.contains(&"package.json"));
    }

    #[test]
    fn entry_patterns_includes_go_mod() {
        assert!(ENTRY_PATTERNS.contains(&"go.mod"));
    }
}

fn scan_dir(
    dir: &std::path::Path,
    depth: usize,
    max_depth: usize,
    langs: &mut HashMap<&'static str, u32>,
    top_dirs: &mut Vec<String>,
    entries: &mut Vec<String>,
    count: &mut u32,
) {
    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"
            || name == "__pycache__"
        {
            continue;
        }
        if path.is_dir() {
            if depth == 0 {
                top_dirs.push(name.to_string());
            }
            if depth < max_depth {
                scan_dir(&path, depth + 1, max_depth, langs, top_dirs, entries, count);
            }
        } else if path.is_file() {
            *count += 1;
            if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
                if let Some(lang) = ext_to_lang(ext) {
                    *langs.entry(lang).or_insert(0) += 1;
                }
            }
            if ENTRY_PATTERNS.contains(&name) {
                entries.push(path.to_string_lossy().to_string());
            }
        }
    }
}
