use std::collections::{HashSet, VecDeque};

use compliance_core::models::graph::ImpactAnalysis;
use petgraph::graph::NodeIndex;
use petgraph::visit::EdgeRef;
use petgraph::Direction;

use super::engine::CodeGraph;

/// Analyzes the impact/blast radius of findings within a code graph
pub struct ImpactAnalyzer<'a> {
    code_graph: &'a CodeGraph,
}

impl<'a> ImpactAnalyzer<'a> {
    pub fn new(code_graph: &'a CodeGraph) -> Self {
        Self { code_graph }
    }

    /// Compute impact analysis for a finding at the given file path and line number
    pub fn analyze(
        &self,
        repo_id: &str,
        finding_id: &str,
        graph_build_id: &str,
        file_path: &str,
        line_number: Option<u32>,
    ) -> ImpactAnalysis {
        let mut analysis = ImpactAnalysis::new(
            repo_id.to_string(),
            finding_id.to_string(),
            graph_build_id.to_string(),
        );

        // Find the node containing the finding
        let target_node = self.find_node_at_location(file_path, line_number);
        let target_idx = match target_node {
            Some(idx) => idx,
            None => return analysis,
        };

        // BFS forward: compute blast radius (what this node affects)
        let forward_reachable = self.bfs_reachable(target_idx, Direction::Outgoing);
        analysis.blast_radius = forward_reachable.len() as u32;

        // BFS backward: find entry points that reach this node
        let backward_reachable = self.bfs_reachable(target_idx, Direction::Incoming);

        // Find affected entry points
        for &idx in &backward_reachable {
            if let Some(node) = self.get_node_by_index(idx) {
                if node.is_entry_point {
                    analysis
                        .affected_entry_points
                        .push(node.qualified_name.clone());
                }
            }
        }

        // Extract call chains from entry points to the target (limited depth)
        for entry_name in &analysis.affected_entry_points.clone() {
            if let Some(&entry_idx) = self.code_graph.node_map.get(entry_name) {
                if let Some(chain) = self.find_path(entry_idx, target_idx, 10) {
                    analysis.call_chains.push(chain);
                }
            }
        }

        // Direct callers (incoming edges to target)
        for edge in self
            .code_graph
            .graph
            .edges_directed(target_idx, Direction::Incoming)
        {
            if let Some(node) = self.get_node_by_index(edge.source()) {
                analysis.direct_callers.push(node.qualified_name.clone());
            }
        }

        // Direct callees (outgoing edges from target)
        for edge in self.code_graph.graph.edges(target_idx) {
            if let Some(node) = self.get_node_by_index(edge.target()) {
                analysis.direct_callees.push(node.qualified_name.clone());
            }
        }

        // Affected communities
        let mut affected_comms: HashSet<u32> = HashSet::new();
        for &idx in forward_reachable.iter().chain(std::iter::once(&target_idx)) {
            if let Some(node) = self.get_node_by_index(idx) {
                if let Some(cid) = node.community_id {
                    affected_comms.insert(cid);
                }
            }
        }
        analysis.affected_communities = affected_comms.into_iter().collect();
        analysis.affected_communities.sort();

        analysis
    }

    /// Find the graph node at a given file/line location
    fn find_node_at_location(
        &self,
        file_path: &str,
        line_number: Option<u32>,
    ) -> Option<NodeIndex> {
        let mut best: Option<(NodeIndex, u32)> = None; // (index, line_span)

        for node in &self.code_graph.nodes {
            if node.file_path != file_path {
                continue;
            }

            if let Some(line) = line_number {
                if line >= node.start_line && line <= node.end_line {
                    let span = node.end_line - node.start_line;
                    // Prefer the narrowest containing node
                    if best.is_none() || span < best.as_ref().map(|b| b.1).unwrap_or(u32::MAX) {
                        if let Some(gi) = node.graph_index {
                            best = Some((NodeIndex::new(gi as usize), span));
                        }
                    }
                }
            } else {
                // No line number, use file node
                if node.kind == compliance_core::models::graph::CodeNodeKind::File {
                    if let Some(gi) = node.graph_index {
                        return Some(NodeIndex::new(gi as usize));
                    }
                }
            }
        }

        best.map(|(idx, _)| idx)
    }

    /// BFS to find all reachable nodes in a given direction
    fn bfs_reachable(&self, start: NodeIndex, direction: Direction) -> HashSet<NodeIndex> {
        let mut visited = HashSet::new();
        let mut queue = VecDeque::new();
        queue.push_back(start);

        while let Some(current) = queue.pop_front() {
            if !visited.insert(current) {
                continue;
            }

            let neighbors: Vec<NodeIndex> = match direction {
                Direction::Outgoing => self
                    .code_graph
                    .graph
                    .edges(current)
                    .map(|e| e.target())
                    .collect(),
                Direction::Incoming => self
                    .code_graph
                    .graph
                    .edges_directed(current, Direction::Incoming)
                    .map(|e| e.source())
                    .collect(),
            };

            for neighbor in neighbors {
                if !visited.contains(&neighbor) {
                    queue.push_back(neighbor);
                }
            }
        }

        visited.remove(&start);
        visited
    }

    /// Find a path from source to target (BFS, limited depth)
    fn find_path(&self, from: NodeIndex, to: NodeIndex, max_depth: usize) -> Option<Vec<String>> {
        let mut visited = HashSet::new();
        let mut queue: VecDeque<(NodeIndex, Vec<NodeIndex>)> = VecDeque::new();
        queue.push_back((from, vec![from]));

        while let Some((current, path)) = queue.pop_front() {
            if current == to {
                return Some(
                    path.iter()
                        .filter_map(|&idx| {
                            self.get_node_by_index(idx)
                                .map(|n| n.qualified_name.clone())
                        })
                        .collect(),
                );
            }

            if path.len() >= max_depth {
                continue;
            }

            if !visited.insert(current) {
                continue;
            }

            for edge in self.code_graph.graph.edges(current) {
                let next = edge.target();
                if !visited.contains(&next) {
                    let mut new_path = path.clone();
                    new_path.push(next);
                    queue.push_back((next, new_path));
                }
            }
        }

        None
    }

    fn get_node_by_index(
        &self,
        idx: NodeIndex,
    ) -> Option<&compliance_core::models::graph::CodeNode> {
        let target_gi = idx.index() as u32;
        self.code_graph
            .nodes
            .iter()
            .find(|n| n.graph_index == Some(target_gi))
    }
}

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

    fn make_node(
        qualified_name: &str,
        file_path: &str,
        start: u32,
        end: u32,
        graph_index: u32,
        is_entry: bool,
        kind: CodeNodeKind,
    ) -> 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,
            file_path: file_path.to_string(),
            start_line: start,
            end_line: end,
            language: "rust".to_string(),
            community_id: None,
            is_entry_point: is_entry,
            graph_index: Some(graph_index),
        }
    }

    fn make_fn_node(
        qualified_name: &str,
        file_path: &str,
        start: u32,
        end: u32,
        gi: u32,
    ) -> CodeNode {
        make_node(
            qualified_name,
            file_path,
            start,
            end,
            gi,
            false,
            CodeNodeKind::Function,
        )
    }

    /// Build a simple linear graph: A -> B -> C
    fn build_linear_graph() -> CodeGraph {
        let mut graph = DiGraph::new();
        let a = graph.add_node("a".to_string());
        let b = graph.add_node("b".to_string());
        let c = graph.add_node("c".to_string());
        graph.add_edge(a, b, CodeEdgeKind::Calls);
        graph.add_edge(b, c, CodeEdgeKind::Calls);

        let mut node_map = HashMap::new();
        node_map.insert("a".to_string(), a);
        node_map.insert("b".to_string(), b);
        node_map.insert("c".to_string(), c);

        CodeGraph {
            graph,
            node_map,
            nodes: vec![
                make_fn_node("a", "src/main.rs", 1, 5, 0),
                make_fn_node("b", "src/main.rs", 7, 12, 1),
                make_fn_node("c", "src/main.rs", 14, 20, 2),
            ],
            edges: Vec::new(),
        }
    }

    #[test]
    fn test_bfs_reachable_outgoing_linear() {
        let cg = build_linear_graph();
        let analyzer = ImpactAnalyzer::new(&cg);
        let start = cg.node_map["a"];
        let reachable = analyzer.bfs_reachable(start, Direction::Outgoing);
        // From a, we can reach b and c
        assert_eq!(reachable.len(), 2);
        assert!(reachable.contains(&cg.node_map["b"]));
        assert!(reachable.contains(&cg.node_map["c"]));
    }

    #[test]
    fn test_bfs_reachable_incoming_linear() {
        let cg = build_linear_graph();
        let analyzer = ImpactAnalyzer::new(&cg);
        let start = cg.node_map["c"];
        let reachable = analyzer.bfs_reachable(start, Direction::Incoming);
        // c is reached by a and b
        assert_eq!(reachable.len(), 2);
        assert!(reachable.contains(&cg.node_map["a"]));
        assert!(reachable.contains(&cg.node_map["b"]));
    }

    #[test]
    fn test_bfs_reachable_no_neighbors() {
        let mut graph = DiGraph::new();
        let a = graph.add_node("a".to_string());
        let cg = CodeGraph {
            graph,
            node_map: [("a".to_string(), a)].into_iter().collect(),
            nodes: vec![make_fn_node("a", "src/main.rs", 1, 5, 0)],
            edges: Vec::new(),
        };
        let analyzer = ImpactAnalyzer::new(&cg);
        let reachable = analyzer.bfs_reachable(a, Direction::Outgoing);
        assert!(reachable.is_empty());
    }

    #[test]
    fn test_bfs_reachable_cycle() {
        let mut graph = DiGraph::new();
        let a = graph.add_node("a".to_string());
        let b = graph.add_node("b".to_string());
        graph.add_edge(a, b, CodeEdgeKind::Calls);
        graph.add_edge(b, a, CodeEdgeKind::Calls);

        let cg = CodeGraph {
            graph,
            node_map: [("a".to_string(), a), ("b".to_string(), b)]
                .into_iter()
                .collect(),
            nodes: vec![
                make_fn_node("a", "f.rs", 1, 5, 0),
                make_fn_node("b", "f.rs", 6, 10, 1),
            ],
            edges: Vec::new(),
        };
        let analyzer = ImpactAnalyzer::new(&cg);
        let reachable = analyzer.bfs_reachable(a, Direction::Outgoing);
        // Should handle cycle without infinite loop
        assert_eq!(reachable.len(), 1);
        assert!(reachable.contains(&b));
    }

    #[test]
    fn test_find_path_exists() {
        let cg = build_linear_graph();
        let analyzer = ImpactAnalyzer::new(&cg);
        let path = analyzer.find_path(cg.node_map["a"], cg.node_map["c"], 10);
        assert!(path.is_some());
        let names = path.unwrap();
        assert_eq!(names, vec!["a", "b", "c"]);
    }

    #[test]
    fn test_find_path_direct() {
        let cg = build_linear_graph();
        let analyzer = ImpactAnalyzer::new(&cg);
        let path = analyzer.find_path(cg.node_map["a"], cg.node_map["b"], 10);
        assert!(path.is_some());
        let names = path.unwrap();
        assert_eq!(names, vec!["a", "b"]);
    }

    #[test]
    fn test_find_path_same_node() {
        let cg = build_linear_graph();
        let analyzer = ImpactAnalyzer::new(&cg);
        let path = analyzer.find_path(cg.node_map["a"], cg.node_map["a"], 10);
        assert!(path.is_some());
        let names = path.unwrap();
        assert_eq!(names, vec!["a"]);
    }

    #[test]
    fn test_find_path_no_connection() {
        let mut graph = DiGraph::new();
        let a = graph.add_node("a".to_string());
        let b = graph.add_node("b".to_string());
        // No edge between a and b
        let cg = CodeGraph {
            graph,
            node_map: [("a".to_string(), a), ("b".to_string(), b)]
                .into_iter()
                .collect(),
            nodes: vec![
                make_fn_node("a", "f.rs", 1, 5, 0),
                make_fn_node("b", "f.rs", 6, 10, 1),
            ],
            edges: Vec::new(),
        };
        let analyzer = ImpactAnalyzer::new(&cg);
        let path = analyzer.find_path(a, b, 10);
        assert!(path.is_none());
    }

    #[test]
    fn test_find_path_depth_limited() {
        // Build a long chain: a -> b -> c -> d -> e
        let mut graph = DiGraph::new();
        let a = graph.add_node("a".to_string());
        let b = graph.add_node("b".to_string());
        let c = graph.add_node("c".to_string());
        let d = graph.add_node("d".to_string());
        let e = graph.add_node("e".to_string());
        graph.add_edge(a, b, CodeEdgeKind::Calls);
        graph.add_edge(b, c, CodeEdgeKind::Calls);
        graph.add_edge(c, d, CodeEdgeKind::Calls);
        graph.add_edge(d, e, CodeEdgeKind::Calls);

        let mut node_map = HashMap::new();
        node_map.insert("a".to_string(), a);
        node_map.insert("b".to_string(), b);
        node_map.insert("c".to_string(), c);
        node_map.insert("d".to_string(), d);
        node_map.insert("e".to_string(), e);

        let cg = CodeGraph {
            graph,
            node_map,
            nodes: vec![
                make_fn_node("a", "f.rs", 1, 2, 0),
                make_fn_node("b", "f.rs", 3, 4, 1),
                make_fn_node("c", "f.rs", 5, 6, 2),
                make_fn_node("d", "f.rs", 7, 8, 3),
                make_fn_node("e", "f.rs", 9, 10, 4),
            ],
            edges: Vec::new(),
        };
        let analyzer = ImpactAnalyzer::new(&cg);
        // Depth 3 won't reach e from a (path length 5)
        let path = analyzer.find_path(a, e, 3);
        assert!(path.is_none());
        // Depth 5 should reach
        let path = analyzer.find_path(a, e, 5);
        assert!(path.is_some());
    }

    #[test]
    fn test_find_node_at_location_exact_line() {
        let cg = build_linear_graph();
        let analyzer = ImpactAnalyzer::new(&cg);
        // Node "b" is at lines 7-12
        let result = analyzer.find_node_at_location("src/main.rs", Some(9));
        assert!(result.is_some());
        assert_eq!(result.unwrap(), cg.node_map["b"]);
    }

    #[test]
    fn test_find_node_at_location_narrowest_match() {
        // Outer function 1-20, inner nested 5-10
        let mut graph = DiGraph::new();
        let outer = graph.add_node("outer".to_string());
        let inner = graph.add_node("inner".to_string());

        let cg = CodeGraph {
            graph,
            node_map: [("outer".to_string(), outer), ("inner".to_string(), inner)]
                .into_iter()
                .collect(),
            nodes: vec![
                make_fn_node("outer", "src/main.rs", 1, 20, 0),
                make_fn_node("inner", "src/main.rs", 5, 10, 1),
            ],
            edges: Vec::new(),
        };
        let analyzer = ImpactAnalyzer::new(&cg);
        // Line 7 is inside both, but inner is narrower
        let result = analyzer.find_node_at_location("src/main.rs", Some(7));
        assert!(result.is_some());
        assert_eq!(result.unwrap(), inner);
    }

    #[test]
    fn test_find_node_at_location_no_line_returns_file_node() {
        let mut graph = DiGraph::new();
        let file_node = graph.add_node("src/main.rs".to_string());
        let fn_node = graph.add_node("src/main.rs::foo".to_string());

        let cg = CodeGraph {
            graph,
            node_map: [
                ("src/main.rs".to_string(), file_node),
                ("src/main.rs::foo".to_string(), fn_node),
            ]
            .into_iter()
            .collect(),
            nodes: vec![
                make_node(
                    "src/main.rs",
                    "src/main.rs",
                    1,
                    100,
                    0,
                    false,
                    CodeNodeKind::File,
                ),
                make_fn_node("src/main.rs::foo", "src/main.rs", 5, 10, 1),
            ],
            edges: Vec::new(),
        };
        let analyzer = ImpactAnalyzer::new(&cg);
        let result = analyzer.find_node_at_location("src/main.rs", None);
        assert!(result.is_some());
        assert_eq!(result.unwrap(), file_node);
    }

    #[test]
    fn test_find_node_at_location_wrong_file() {
        let cg = build_linear_graph();
        let analyzer = ImpactAnalyzer::new(&cg);
        let result = analyzer.find_node_at_location("nonexistent.rs", Some(5));
        assert!(result.is_none());
    }

    #[test]
    fn test_find_node_at_location_line_out_of_range() {
        let cg = build_linear_graph();
        let analyzer = ImpactAnalyzer::new(&cg);
        let result = analyzer.find_node_at_location("src/main.rs", Some(999));
        assert!(result.is_none());
    }

    #[test]
    fn test_analyze_basic() {
        // A (entry) -> B -> C
        let mut graph = DiGraph::new();
        let a = graph.add_node("a".to_string());
        let b = graph.add_node("b".to_string());
        let c = graph.add_node("c".to_string());
        graph.add_edge(a, b, CodeEdgeKind::Calls);
        graph.add_edge(b, c, CodeEdgeKind::Calls);

        let mut node_map = HashMap::new();
        node_map.insert("a".to_string(), a);
        node_map.insert("b".to_string(), b);
        node_map.insert("c".to_string(), c);

        let cg = CodeGraph {
            graph,
            node_map,
            nodes: vec![
                make_node("a", "src/main.rs", 1, 5, 0, true, CodeNodeKind::Function),
                make_fn_node("b", "src/main.rs", 7, 12, 1),
                make_fn_node("c", "src/main.rs", 14, 20, 2),
            ],
            edges: Vec::new(),
        };

        let analyzer = ImpactAnalyzer::new(&cg);
        let result = analyzer.analyze("repo1", "finding1", "build1", "src/main.rs", Some(9));
        // B's blast radius: C is reachable forward
        assert_eq!(result.blast_radius, 1);
        // B has A as direct caller
        assert_eq!(result.direct_callers, vec!["a"]);
        // B calls C
        assert_eq!(result.direct_callees, vec!["c"]);
        // A is an entry point that reaches B
        assert_eq!(result.affected_entry_points, vec!["a"]);
    }

    #[test]
    fn test_analyze_no_matching_node() {
        let cg = build_linear_graph();
        let analyzer = ImpactAnalyzer::new(&cg);
        let result = analyzer.analyze("repo1", "f1", "b1", "nonexistent.rs", Some(1));
        assert_eq!(result.blast_radius, 0);
        assert!(result.affected_entry_points.is_empty());
        assert!(result.direct_callers.is_empty());
        assert!(result.direct_callees.is_empty());
    }
}
