use std::collections::HashMap;
use std::path::Path;

use chrono::Utc;
use compliance_core::error::CoreError;
use compliance_core::models::graph::{
    CodeEdge, CodeEdgeKind, CodeNode, GraphBuildRun, GraphBuildStatus,
};
use compliance_core::traits::graph_builder::ParseOutput;
use petgraph::graph::{DiGraph, NodeIndex};
use tracing::info;

use crate::parsers::registry::ParserRegistry;

use super::community::detect_communities;
use super::impact::ImpactAnalyzer;

/// The main graph engine that builds and manages code knowledge graphs
pub struct GraphEngine {
    parser_registry: ParserRegistry,
    max_nodes: u32,
}

/// In-memory representation of a built code graph
pub struct CodeGraph {
    pub graph: DiGraph<String, CodeEdgeKind>,
    pub node_map: HashMap<String, NodeIndex>,
    pub nodes: Vec<CodeNode>,
    pub edges: Vec<CodeEdge>,
}

impl GraphEngine {
    pub fn new(max_nodes: u32) -> Self {
        Self {
            parser_registry: ParserRegistry::new(),
            max_nodes,
        }
    }

    /// Build a code graph from a repository directory
    pub fn build_graph(
        &self,
        repo_path: &Path,
        repo_id: &str,
        graph_build_id: &str,
    ) -> Result<(CodeGraph, GraphBuildRun), CoreError> {
        let mut build_run = GraphBuildRun::new(repo_id.to_string());

        info!(repo_id, path = %repo_path.display(), "Starting graph build");

        // Phase 1: Parse all files
        let parse_output = self.parser_registry.parse_directory(
            repo_path,
            repo_id,
            graph_build_id,
            self.max_nodes,
        )?;

        // Phase 2: Build petgraph
        let code_graph = self.build_petgraph(parse_output)?;

        // Phase 3: Run community detection
        let community_count = detect_communities(&code_graph);

        // Collect language stats
        let mut languages: Vec<String> = code_graph
            .nodes
            .iter()
            .map(|n| n.language.clone())
            .collect::<std::collections::HashSet<_>>()
            .into_iter()
            .collect();
        languages.sort();

        build_run.node_count = code_graph.nodes.len() as u32;
        build_run.edge_count = code_graph.edges.len() as u32;
        build_run.community_count = community_count;
        build_run.languages_parsed = languages;
        build_run.status = GraphBuildStatus::Completed;
        build_run.completed_at = Some(Utc::now());

        info!(
            nodes = build_run.node_count,
            edges = build_run.edge_count,
            communities = build_run.community_count,
            "Graph build complete"
        );

        Ok((code_graph, build_run))
    }

    /// Build petgraph from parsed output, resolving edges to node indices
    fn build_petgraph(&self, parse_output: ParseOutput) -> Result<CodeGraph, CoreError> {
        let mut graph = DiGraph::new();
        let mut node_map: HashMap<String, NodeIndex> = HashMap::new();
        let mut nodes = parse_output.nodes;

        // Add all nodes to the graph
        for node in &mut nodes {
            let idx = graph.add_node(node.qualified_name.clone());
            node.graph_index = Some(idx.index() as u32);
            node_map.insert(node.qualified_name.clone(), idx);
        }

        // Resolve and add edges — rewrite target to the resolved qualified name
        // so the persisted edge references match node qualified_names.
        let mut resolved_edges = Vec::new();
        for mut edge in parse_output.edges {
            let source_idx = node_map.get(&edge.source);
            let resolved = self.resolve_edge_target(&edge.target, &node_map);

            if let (Some(&src), Some(tgt)) = (source_idx, resolved) {
                // Update target to the resolved qualified name
                let resolved_name = node_map
                    .iter()
                    .find(|(_, &idx)| idx == tgt)
                    .map(|(name, _)| name.clone());
                if let Some(name) = resolved_name {
                    edge.target = name;
                }
                graph.add_edge(src, tgt, edge.kind.clone());
                resolved_edges.push(edge);
            }
            // Skip unresolved edges (cross-file, external deps) — conservative approach
        }

        Ok(CodeGraph {
            graph,
            node_map,
            nodes,
            edges: resolved_edges,
        })
    }

    /// Try to resolve an edge target to a known node
    fn resolve_edge_target(
        &self,
        target: &str,
        node_map: &HashMap<String, NodeIndex>,
    ) -> Option<NodeIndex> {
        // Direct match
        if let Some(idx) = node_map.get(target) {
            return Some(*idx);
        }

        // Try matching just the function/type name (intra-file resolution)
        for (qualified, idx) in node_map {
            // Match "foo" to "path/file.rs::foo" or "path/file.rs::Type::foo"
            if qualified.ends_with(&format!("::{target}"))
                || qualified.ends_with(&format!(".{target}"))
            {
                return Some(*idx);
            }
        }

        // Try matching method calls like "self.method" -> look for "::method"
        if let Some(method_name) = target.strip_prefix("self.") {
            for (qualified, idx) in node_map {
                if qualified.ends_with(&format!("::{method_name}"))
                    || qualified.ends_with(&format!(".{method_name}"))
                {
                    return Some(*idx);
                }
            }
        }

        None
    }

    /// Get the impact analyzer for a built graph
    pub fn impact_analyzer(code_graph: &CodeGraph) -> ImpactAnalyzer<'_> {
        ImpactAnalyzer::new(code_graph)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use compliance_core::models::graph::{CodeEdgeKind, CodeNode, CodeNodeKind};

    fn make_node(qualified_name: &str) -> CodeNode {
        CodeNode {
            id: None,
            repo_id: "test".to_string(),
            graph_build_id: "build1".to_string(),
            qualified_name: qualified_name.to_string(),
            name: qualified_name
                .split("::")
                .last()
                .unwrap_or(qualified_name)
                .to_string(),
            kind: CodeNodeKind::Function,
            file_path: "src/main.rs".to_string(),
            start_line: 1,
            end_line: 10,
            language: "rust".to_string(),
            community_id: None,
            is_entry_point: false,
            graph_index: None,
        }
    }

    fn build_test_node_map(names: &[&str]) -> HashMap<String, NodeIndex> {
        let mut graph: DiGraph<String, String> = DiGraph::new();
        let mut map = HashMap::new();
        for name in names {
            let idx = graph.add_node(name.to_string());
            map.insert(name.to_string(), idx);
        }
        map
    }

    #[test]
    fn test_resolve_edge_target_direct_match() {
        let engine = GraphEngine::new(1000);
        let node_map = build_test_node_map(&["src/main.rs::foo", "src/main.rs::bar"]);
        let result = engine.resolve_edge_target("src/main.rs::foo", &node_map);
        assert!(result.is_some());
        assert_eq!(result.unwrap(), node_map["src/main.rs::foo"]);
    }

    #[test]
    fn test_resolve_edge_target_short_name_match() {
        let engine = GraphEngine::new(1000);
        let node_map = build_test_node_map(&["src/main.rs::foo", "src/main.rs::bar"]);
        let result = engine.resolve_edge_target("foo", &node_map);
        assert!(result.is_some());
        assert_eq!(result.unwrap(), node_map["src/main.rs::foo"]);
    }

    #[test]
    fn test_resolve_edge_target_method_match() {
        let engine = GraphEngine::new(1000);
        let node_map = build_test_node_map(&["src/main.rs::MyStruct::do_thing"]);
        let result = engine.resolve_edge_target("do_thing", &node_map);
        assert!(result.is_some());
    }

    #[test]
    fn test_resolve_edge_target_self_method() {
        let engine = GraphEngine::new(1000);
        let node_map = build_test_node_map(&["src/main.rs::MyStruct::process"]);
        let result = engine.resolve_edge_target("self.process", &node_map);
        assert!(result.is_some());
    }

    #[test]
    fn test_resolve_edge_target_no_match() {
        let engine = GraphEngine::new(1000);
        let node_map = build_test_node_map(&["src/main.rs::foo"]);
        let result = engine.resolve_edge_target("nonexistent", &node_map);
        assert!(result.is_none());
    }

    #[test]
    fn test_resolve_edge_target_empty_map() {
        let engine = GraphEngine::new(1000);
        let node_map = HashMap::new();
        let result = engine.resolve_edge_target("anything", &node_map);
        assert!(result.is_none());
    }

    #[test]
    fn test_resolve_edge_target_dot_notation() {
        let engine = GraphEngine::new(1000);
        let node_map = build_test_node_map(&["src/app.js.handler"]);
        let result = engine.resolve_edge_target("handler", &node_map);
        assert!(result.is_some());
    }

    #[test]
    fn test_build_petgraph_empty() {
        let engine = GraphEngine::new(1000);
        let output = ParseOutput::default();
        let code_graph = engine.build_petgraph(output).unwrap();
        assert_eq!(code_graph.nodes.len(), 0);
        assert_eq!(code_graph.edges.len(), 0);
        assert_eq!(code_graph.graph.node_count(), 0);
    }

    #[test]
    fn test_build_petgraph_nodes_get_graph_index() {
        let engine = GraphEngine::new(1000);
        let mut output = ParseOutput::default();
        output.nodes.push(make_node("src/main.rs::foo"));
        output.nodes.push(make_node("src/main.rs::bar"));

        let code_graph = engine.build_petgraph(output).unwrap();
        assert_eq!(code_graph.nodes.len(), 2);
        assert_eq!(code_graph.graph.node_count(), 2);
        // All nodes should have a graph_index assigned
        for node in &code_graph.nodes {
            assert!(node.graph_index.is_some());
        }
    }

    #[test]
    fn test_build_petgraph_resolves_edges() {
        let engine = GraphEngine::new(1000);
        let mut output = ParseOutput::default();
        output.nodes.push(make_node("src/main.rs::foo"));
        output.nodes.push(make_node("src/main.rs::bar"));
        output.edges.push(CodeEdge {
            id: None,
            repo_id: "test".to_string(),
            graph_build_id: "build1".to_string(),
            source: "src/main.rs::foo".to_string(),
            target: "bar".to_string(), // short name, should resolve
            kind: CodeEdgeKind::Calls,
            file_path: "src/main.rs".to_string(),
            line_number: Some(5),
        });

        let code_graph = engine.build_petgraph(output).unwrap();
        assert_eq!(code_graph.edges.len(), 1);
        assert_eq!(code_graph.graph.edge_count(), 1);
        // The resolved edge target should be the full qualified name
        assert_eq!(code_graph.edges[0].target, "src/main.rs::bar");
    }

    #[test]
    fn test_build_petgraph_skips_unresolved_edges() {
        let engine = GraphEngine::new(1000);
        let mut output = ParseOutput::default();
        output.nodes.push(make_node("src/main.rs::foo"));
        output.edges.push(CodeEdge {
            id: None,
            repo_id: "test".to_string(),
            graph_build_id: "build1".to_string(),
            source: "src/main.rs::foo".to_string(),
            target: "external_crate::something".to_string(),
            kind: CodeEdgeKind::Calls,
            file_path: "src/main.rs".to_string(),
            line_number: Some(5),
        });

        let code_graph = engine.build_petgraph(output).unwrap();
        assert_eq!(code_graph.edges.len(), 0);
        assert_eq!(code_graph.graph.edge_count(), 0);
    }

    #[test]
    fn test_code_graph_node_map_consistency() {
        let engine = GraphEngine::new(1000);
        let mut output = ParseOutput::default();
        output.nodes.push(make_node("a::b"));
        output.nodes.push(make_node("a::c"));
        output.nodes.push(make_node("a::d"));

        let code_graph = engine.build_petgraph(output).unwrap();
        assert_eq!(code_graph.node_map.len(), 3);
        assert!(code_graph.node_map.contains_key("a::b"));
        assert!(code_graph.node_map.contains_key("a::c"));
        assert!(code_graph.node_map.contains_key("a::d"));
    }
}
