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

/// Render: generate a Mermaid or DOT diagram from the project's import dependency graph.
/// Scans source files, extracts import relationships, and renders a visual graph.
pub struct RenderSkill;

impl Skill for RenderSkill {
    fn id(&self) -> SkillId {
        SkillId("render")
    }
    fn display_name(&self) -> &str {
        "Render"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "root": {
                    "type": "string",
                    "default": ".",
                    "description": "Project root directory to scan"
                },
                "format": {
                    "type": "string",
                    "enum": ["mermaid", "dot", "adjacency"],
                    "default": "mermaid",
                    "description": "Output format. mermaid: Mermaid flowchart syntax. dot: Graphviz DOT. adjacency: JSON adjacency list."
                },
                "focus_paths": {
                    "type": "array",
                    "items": { "type": "string" },
                    "description": "Limit the graph to files matching these path prefixes. Empty = whole project."
                },
                "depth": {
                    "type": "integer",
                    "default": 3,
                    "description": "Maximum import hop depth from focus_paths (or from root if no focus)"
                },
                "max_nodes": {
                    "type": "integer",
                    "default": 40,
                    "description": "Maximum nodes in the diagram. Largest hubs are kept when truncating."
                },
                "direction": {
                    "type": "string",
                    "enum": ["TD", "LR", "BT", "RL"],
                    "default": "LR",
                    "description": "Mermaid diagram direction (top-down, left-right, etc.)"
                }
            },
            "required": []
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "diagram":      { "type": "string" },
                "format":       { "type": "string" },
                "node_count":   { "type": "integer" },
                "edge_count":   { "type": "integer" },
                "truncated":    { "type": "boolean" },
                "files_scanned": { "type": "integer" }
            }
        })
    }

    fn execute(&self, input: SkillInput, _ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let root = input.get_str("root").unwrap_or(".").to_string();
        let format = input.get_str("format").unwrap_or("mermaid").to_string();
        let focus_paths: Vec<String> = input.get_str_array("focus_paths").unwrap_or_default();
        let max_nodes = input.get_u64("max_nodes").unwrap_or(40) as usize;
        let direction = input.get_str("direction").unwrap_or("LR").to_string();

        let mut edges: HashMap<String, HashSet<String>> = HashMap::new();
        let mut files_scanned = 0usize;

        collect_imports(Path::new(&root), &root, &mut edges, &mut files_scanned);

        // Apply focus filter
        let mut nodes: HashSet<String> = edges.keys().cloned().collect();
        for targets in edges.values() {
            nodes.extend(targets.iter().cloned());
        }

        if !focus_paths.is_empty() {
            nodes.retain(|n| focus_paths.iter().any(|fp| n.contains(fp.as_str())));
            edges.retain(|k, _| nodes.contains(k));
            for targets in edges.values_mut() {
                targets.retain(|t| nodes.contains(t));
            }
        }

        // Truncate to max_nodes — keep highest out-degree nodes
        let truncated = nodes.len() > max_nodes;
        if truncated {
            let mut scored: Vec<(String, usize)> = nodes
                .iter()
                .map(|n| (n.clone(), edges.get(n).map(|e| e.len()).unwrap_or(0)))
                .collect();
            scored.sort_by_key(|b| std::cmp::Reverse(b.1));
            scored.truncate(max_nodes);
            let kept: HashSet<String> = scored.into_iter().map(|(n, _)| n).collect();
            nodes.retain(|n| kept.contains(n));
            edges.retain(|k, _| kept.contains(k));
            for targets in edges.values_mut() {
                targets.retain(|t| kept.contains(t));
            }
        }

        // Count edges
        let edge_count: usize = edges.values().map(|s| s.len()).sum();
        let node_count = nodes.len();

        let diagram = match format.as_str() {
            "dot" => render_dot(&edges, &nodes),
            "adjacency" => render_adjacency(&edges),
            _ => render_mermaid(&edges, &nodes, &direction),
        };

        Ok(SkillOutcome::success(
            "render",
            json!({
                "diagram": diagram,
                "format": format,
                "node_count": node_count,
                "edge_count": edge_count,
                "truncated": truncated,
                "files_scanned": files_scanned
            }),
        ))
    }
}

fn collect_imports(
    dir: &Path,
    root: &str,
    edges: &mut HashMap<String, HashSet<String>>,
    scanned: &mut usize,
) {
    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() {
            collect_imports(&path, root, edges, scanned);
        } 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;
            }
            if let Ok(content) = std::fs::read_to_string(&path) {
                *scanned += 1;
                let rel = path.strip_prefix(root).unwrap_or(&path);
                let node = rel.to_string_lossy().to_string();
                let imports = extract_imports(&content, ext);
                if !imports.is_empty() {
                    edges.entry(node).or_default().extend(imports);
                }
            }
        }
    }
}

fn extract_imports(content: &str, ext: &str) -> Vec<String> {
    let mut imports = Vec::new();
    for line in content.lines() {
        let t = line.trim();
        match ext {
            "rs" if t.starts_with("use ") || t.starts_with("mod ") => {
                // use crate::foo::bar; or mod foo;
                let s = t
                    .trim_start_matches("use ")
                    .trim_start_matches("mod ")
                    .trim_end_matches(';')
                    .trim()
                    .to_string();
                if !s.is_empty() && !s.starts_with("std::") && !s.starts_with("super::") {
                    imports.push(s);
                }
            }
            "rs" => {}
            "ts" | "tsx" | "js" | "jsx" if t.starts_with("import ") => {
                // import ... from './path' or require('./path')
                if let Some(from) = t.rfind("from ") {
                    let module = t[from + 5..]
                        .trim()
                        .trim_end_matches(';')
                        .trim()
                        .trim_matches('"')
                        .trim_matches('\'')
                        .to_string();
                    if module.starts_with('.') {
                        imports.push(module);
                    }
                }
            }
            "ts" | "tsx" | "js" | "jsx" => {}
            "py" => {
                // from .module import X or import module
                if t.starts_with("from .") {
                    let module = t
                        .trim_start_matches("from ")
                        .split_whitespace()
                        .next()
                        .unwrap_or("")
                        .to_string();
                    imports.push(module);
                } else if t.starts_with("import ") {
                    let module = t
                        .trim_start_matches("import ")
                        .split_whitespace()
                        .next()
                        .unwrap_or("")
                        .to_string();
                    if !module.is_empty() {
                        imports.push(module);
                    }
                }
            }
            "go" if t.starts_with('"') => {
                // import "path" or "alias path"
                let module = t.trim_matches('"').to_string();
                if !module.is_empty() {
                    imports.push(module);
                }
            }
            "go" => {}
            _ => {}
        }
    }
    imports
}

fn sanitize_node(s: &str) -> String {
    // Mermaid node IDs must not contain special chars
    s.replace(
        [
            '/', '.', '-', ' ', ':', '"', '\'', '(', ')', '[', ']', '{', '}', '!', '?', '@', '#',
            '$', '%', '^', '&', '*',
        ],
        "_",
    )
}

fn shorten_label(s: &str) -> &str {
    // Show only the last path component for readability
    s.rsplit('/').next().unwrap_or(s)
}

fn render_mermaid(
    edges: &HashMap<String, HashSet<String>>,
    nodes: &HashSet<String>,
    direction: &str,
) -> String {
    let mut lines = vec![format!("flowchart {direction}")];

    // Node definitions
    let mut sorted_nodes: Vec<&String> = nodes.iter().collect();
    sorted_nodes.sort();
    for node in &sorted_nodes {
        let id = sanitize_node(node);
        let label = shorten_label(node);
        lines.push(format!("  {id}[\"{label}\"]"));
    }

    // Edges
    let mut sorted_edges: Vec<(&String, &String)> = edges
        .iter()
        .flat_map(|(from, tos)| tos.iter().map(move |to| (from, to)))
        .collect();
    sorted_edges.sort();
    for (from, to) in sorted_edges {
        if nodes.contains(from) && nodes.contains(to) {
            lines.push(format!(
                "  {} --> {}",
                sanitize_node(from),
                sanitize_node(to)
            ));
        }
    }

    lines.join("\n")
}

fn render_dot(edges: &HashMap<String, HashSet<String>>, nodes: &HashSet<String>) -> String {
    let mut lines = vec![
        "digraph dependencies {".to_string(),
        "  rankdir=LR;".to_string(),
        "  node [shape=box, fontsize=10];".to_string(),
    ];

    let mut sorted_nodes: Vec<&String> = nodes.iter().collect();
    sorted_nodes.sort();
    for node in &sorted_nodes {
        let label = shorten_label(node);
        lines.push(format!("  \"{node}\" [label=\"{label}\"];"));
    }

    let mut sorted_edges: Vec<(&String, &String)> = edges
        .iter()
        .flat_map(|(from, tos)| tos.iter().map(move |to| (from, to)))
        .collect();
    sorted_edges.sort();
    for (from, to) in sorted_edges {
        if nodes.contains(from) && nodes.contains(to) {
            lines.push(format!("  \"{from}\" -> \"{to}\";"));
        }
    }

    lines.push("}".to_string());
    lines.join("\n")
}

fn render_adjacency(edges: &HashMap<String, HashSet<String>>) -> String {
    let map: serde_json::Map<String, serde_json::Value> = edges
        .iter()
        .map(|(k, v)| {
            let mut deps: Vec<&String> = v.iter().collect();
            deps.sort();
            (k.clone(), json!(deps))
        })
        .collect();
    serde_json::to_string_pretty(&serde_json::Value::Object(map)).unwrap_or_default()
}

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

    #[test]
    fn render_mermaid_produces_flowchart() {
        let mut edges = HashMap::new();
        let mut deps = HashSet::new();
        deps.insert("src/b.rs".to_string());
        edges.insert("src/a.rs".to_string(), deps);
        let nodes: HashSet<String> = ["src/a.rs", "src/b.rs"]
            .iter()
            .map(|s| s.to_string())
            .collect();
        let out = render_mermaid(&edges, &nodes, "LR");
        assert!(out.starts_with("flowchart LR"));
        assert!(out.contains("-->"));
    }

    #[test]
    fn render_dot_produces_digraph() {
        let mut edges = HashMap::new();
        let mut deps = HashSet::new();
        deps.insert("b.rs".to_string());
        edges.insert("a.rs".to_string(), deps);
        let nodes: HashSet<String> = ["a.rs", "b.rs"].iter().map(|s| s.to_string()).collect();
        let out = render_dot(&edges, &nodes);
        assert!(out.starts_with("digraph"));
        assert!(out.contains("->"));
    }

    #[test]
    fn render_adjacency_is_json() {
        let mut edges = HashMap::new();
        let mut deps = HashSet::new();
        deps.insert("b.ts".to_string());
        edges.insert("a.ts".to_string(), deps);
        let out = render_adjacency(&edges);
        let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
        assert!(parsed["a.ts"].is_array());
    }

    #[test]
    fn extract_imports_rust() {
        let code = "use crate::foo::bar;\nuse std::collections::HashMap;\nmod baz;";
        let imports = extract_imports(code, "rs");
        assert!(imports.contains(&"crate::foo::bar".to_string()));
        assert!(!imports.iter().any(|i| i.starts_with("std::")));
    }

    #[test]
    fn extract_imports_typescript() {
        let code = "import { foo } from './foo';\nimport React from 'react';";
        let imports = extract_imports(code, "ts");
        assert!(imports.contains(&"./foo".to_string()));
        assert!(!imports.iter().any(|i| i == "react")); // external — not relative
    }

    #[test]
    fn extract_imports_python_relative() {
        let code = "from .utils import helper\nimport os";
        let imports = extract_imports(code, "py");
        assert!(imports.iter().any(|i| i.contains(".utils")));
    }

    #[test]
    fn sanitize_node_removes_slashes() {
        let result = sanitize_node("src/foo/bar.rs");
        assert!(!result.contains('/'));
    }

    #[test]
    fn shorten_label_gets_last_component() {
        assert_eq!(shorten_label("src/foo/bar.rs"), "bar.rs");
        assert_eq!(shorten_label("main.rs"), "main.rs");
    }

    #[test]
    fn max_nodes_truncates() {
        use crate::invocation::{SkillContext, SkillInput};
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        // Create 10 TS files importing from each other so we get nodes
        for i in 0..10 {
            std::fs::write(
                dir.path().join(format!("file{i}.ts")),
                format!(
                    "import {{ x }} from './file{}';\nexport const x{i} = {i};",
                    (i + 1) % 10
                ),
            )
            .unwrap();
        }

        let skill = RenderSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "root": dir.path().to_str().unwrap(),
                    "max_nodes": 3
                })),
                &SkillContext::new("test", "."),
            )
            .unwrap();
        assert!(out.result["node_count"].as_u64().unwrap() <= 3);
    }

    #[test]
    fn mermaid_direction_used() {
        let edges: HashMap<String, HashSet<String>> = HashMap::new();
        let nodes: HashSet<String> = HashSet::new();
        let out = render_mermaid(&edges, &nodes, "TD");
        assert!(out.contains("flowchart TD"));
    }
}
