use crate::{
    invocation::{SkillContext, SkillInput},
    outcome::SkillOutcome,
    skill::{Skill, SkillId},
};
use serde_json::json;

/// Scanner: find files/symbols matching a query across the project (fuzzy + substring).
pub struct ScannerSkill;

impl Skill for ScannerSkill {
    fn id(&self) -> SkillId {
        SkillId("scanner")
    }
    fn display_name(&self) -> &str {
        "Scanner"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "query": { "type": "string", "description": "Search term (file name, symbol name, or content snippet)" },
                "root": { "type": "string", "default": ".", "description": "Directory to scan" },
                "top_k": { "type": "integer", "default": 10 },
                "search_type": {
                    "type": "string",
                    "enum": ["files", "symbols", "content", "all"],
                    "default": "all"
                }
            },
            "required": ["query"]
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "results": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "path": { "type": "string" },
                            "match_type": { "type": "string" },
                            "line": { "type": "integer" },
                            "snippet": { "type": "string" },
                            "score": { "type": "number" }
                        }
                    }
                },
                "count": { "type": "integer" }
            }
        })
    }

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

        let mut results = Vec::new();
        scan_directory(
            std::path::Path::new(&root),
            &query,
            &search_type,
            &mut results,
        );

        // Sort by score descending
        results.sort_by(|a, b| {
            b["score"]
                .as_f64()
                .unwrap_or(0.0)
                .partial_cmp(&a["score"].as_f64().unwrap_or(0.0))
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        results.truncate(top_k);
        let count = results.len();

        Ok(SkillOutcome::success(
            "scanner",
            json!({ "results": results, "count": count }),
        ))
    }
}

fn scan_directory(
    dir: &std::path::Path,
    query: &str,
    search_type: &str,
    results: &mut Vec<serde_json::Value>,
) {
    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() {
            scan_directory(&path, query, search_type, results);
        } else if path.is_file() {
            let path_str = path.to_string_lossy().to_string();

            // File name match
            if matches!(search_type, "files" | "all") {
                let score = fuzzy_score(name, query);
                if score > 0.0 {
                    results.push(json!({
                        "path": path_str,
                        "match_type": "file_name",
                        "score": score,
                        "snippet": name
                    }));
                }
            }

            // Content / symbol match
            if matches!(search_type, "symbols" | "content" | "all") {
                let Ok(content) = std::fs::read_to_string(&path) else {
                    continue;
                };
                for (i, line) in content.lines().enumerate() {
                    if line.contains(query) {
                        let match_type = if is_definition_line(line) {
                            "symbol"
                        } else {
                            "content"
                        };
                        if search_type == "symbols" && match_type != "symbol" {
                            continue;
                        }
                        results.push(json!({
                            "path": path_str,
                            "match_type": match_type,
                            "line": i + 1,
                            "snippet": line.trim().chars().take(120).collect::<String>(),
                            "score": 1.0
                        }));
                        if results.len() >= 500 {
                            return;
                        }
                    }
                }
            }
        }
    }
}

pub(crate) fn fuzzy_score(haystack: &str, needle: &str) -> f64 {
    let hl = haystack.to_lowercase();
    let nl = needle.to_lowercase();
    if hl == nl {
        return 2.0;
    }
    if hl.contains(&nl) {
        return 1.5;
    }
    // Simple character overlap score
    let matches = nl.chars().filter(|c| hl.contains(*c)).count();
    if matches == 0 {
        return 0.0;
    }
    matches as f64 / nl.len() as f64
}

pub(crate) fn is_definition_line(line: &str) -> bool {
    let t = line.trim();
    t.starts_with("pub fn ")
        || t.starts_with("fn ")
        || t.starts_with("pub struct ")
        || t.starts_with("struct ")
        || t.starts_with("pub enum ")
        || t.starts_with("enum ")
        || t.starts_with("pub trait ")
        || t.starts_with("trait ")
        || t.starts_with("def ")
        || t.starts_with("class ")
        || t.starts_with("function ")
        || t.starts_with("const ")
        || t.starts_with("export function ")
        || t.starts_with("export class ")
        || t.starts_with("func ")
}

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

    #[test]
    fn fuzzy_exact_match_scores_2() {
        assert_eq!(fuzzy_score("foo", "foo"), 2.0);
    }

    #[test]
    fn fuzzy_substring_scores_1_5() {
        assert_eq!(fuzzy_score("foobar", "foo"), 1.5);
    }

    #[test]
    fn fuzzy_no_overlap_scores_0() {
        assert_eq!(fuzzy_score("xyz", "abc"), 0.0);
    }

    #[test]
    fn fuzzy_case_insensitive_exact() {
        assert_eq!(fuzzy_score("FooBar", "foobar"), 2.0);
    }

    #[test]
    fn is_def_pub_fn() {
        assert!(is_definition_line("pub fn process(x: u32) -> u32 {"));
    }

    #[test]
    fn is_def_fn() {
        assert!(is_definition_line("fn helper() {}"));
    }

    #[test]
    fn is_def_pub_struct() {
        assert!(is_definition_line("pub struct Config {"));
    }

    #[test]
    fn is_def_class() {
        assert!(is_definition_line("class MyClass:"));
    }

    #[test]
    fn is_def_func_go() {
        assert!(is_definition_line("func Handler(w http.ResponseWriter) {"));
    }

    #[test]
    fn not_def_plain_assignment() {
        assert!(!is_definition_line("let x = 1;"));
    }

    #[test]
    fn not_def_comment_with_fn() {
        assert!(!is_definition_line("// fn foo() is documented above"));
    }
}
