use crate::{
    invocation::{SkillContext, SkillInput},
    outcome::SkillOutcome,
    skill::{Skill, SkillId},
};
use prism::ingestion::scanner::DirectoryScanner;
use serde_json::json;
use std::collections::HashMap;

pub struct CartographSkill;

impl Skill for CartographSkill {
    fn id(&self) -> SkillId {
        SkillId("cartograph")
    }
    fn display_name(&self) -> &str {
        "Cartograph"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "root": { "type": "string" },
                "depth": { "type": "integer", "default": 3 },
                "output_format": { "type": "string", "enum": ["tree", "json"], "default": "tree" }
            },
            "required": ["root"]
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "map": { "type": "string" },
                "file_count": { "type": "integer" },
                "languages": { "type": "object" },
                "estimated_tokens": { "type": "integer" }
            }
        })
    }

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

        let mut scanner = DirectoryScanner::new();
        scanner.max_depth = depth;
        let mut files = scanner
            .scan(std::path::Path::new(&root))
            .unwrap_or_default();

        // Also collect project config and infra files that have no source extension.
        let config_names: &[&str] = &[
            "Dockerfile",
            "docker-compose.yml",
            "docker-compose.yaml",
            "Makefile",
            "CMakeLists.txt",
            "package.json",
            "package-lock.json",
            "yarn.lock",
            "pnpm-lock.yaml",
            "Cargo.toml",
            "Cargo.lock",
            "pyproject.toml",
            "setup.py",
            "requirements.txt",
            "go.mod",
            "go.sum",
            ".env.example",
            "tsconfig.json",
            "vite.config.ts",
            "webpack.config.js",
            "tailwind.config.js",
            "eslint.config.js",
            ".eslintrc.json",
            ".prettierrc",
            "README.md",
            "CHANGELOG.md",
        ];
        let config_exts: &[&str] = &["toml", "yaml", "yml", "json", "lock", "md", "txt", "env"];

        use walkdir::WalkDir;
        for entry in WalkDir::new(&root)
            .max_depth(depth)
            .into_iter()
            .filter_map(|e| e.ok())
        {
            let path = entry.path();
            if !path.is_file() {
                continue;
            }
            // Skip hidden files/dirs and common noise dirs
            if path.components().any(|c| {
                let s = c.as_os_str().to_string_lossy();
                s.starts_with('.')
                    || matches!(s.as_ref(), "node_modules" | "target" | "dist" | "build")
            }) {
                continue;
            }
            let fname = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
            let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
            let is_config = config_names.contains(&fname) || config_exts.contains(&ext);
            if is_config && !files.contains(&path.to_path_buf()) {
                files.push(path.to_path_buf());
            }
        }

        let mut lang_counts: HashMap<String, usize> = HashMap::new();
        for f in &files {
            let lang = prism::parse::Language::from_path(&f.to_string_lossy());
            *lang_counts.entry(lang.name().to_string()).or_insert(0) += 1;
        }

        let map = files
            .iter()
            .map(|f| f.to_string_lossy().to_string())
            .take(100)
            .collect::<Vec<_>>()
            .join("\n");
        let est_tokens = files.len() * 200; // rough estimate

        Ok(SkillOutcome::success(
            "cartograph",
            json!({
                "map": map,
                "file_count": files.len(),
                "languages": lang_counts,
                "estimated_tokens": est_tokens
            }),
        ))
    }
}
