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))
    }
}
