//! Pure graph algorithms for the Graph Intelligence layer.
//! All functions operate on petgraph types - no DB, no async, fully testable.

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

use petgraph::graph::NodeIndex;
use petgraph::visit::EdgeRef;
use petgraph::Graph;
use uuid::Uuid;

use super::EdgeData;
use super::RiskScores;

// ---------------------------------------------------------------------------
// PageRank - 20 power iterations, damping factor 0.85
// ---------------------------------------------------------------------------

pub fn compute_pagerank(
    graph: &Graph<Uuid, EdgeData, petgraph::Undirected>,
) -> HashMap<NodeIndex, f64> {
    let n = graph.node_count();
    if n == 0 {
        return HashMap::new();
    }

    let d = 0.85_f64;
    let base = (1.0 - d) / n as f64;
    let mut rank: HashMap<NodeIndex, f64> = graph
        .node_indices()
        .map(|idx| (idx, 1.0 / n as f64))
        .collect();

    for _ in 0..20 {
        let mut next_rank: HashMap<NodeIndex, f64> =
            graph.node_indices().map(|idx| (idx, base)).collect();

        for node in graph.node_indices() {
            let degree = graph.edges(node).count();
            if degree == 0 {
                continue;
            }
            let share = rank[&node] / degree as f64;
            for edge_ref in graph.edges(node) {
                let neighbor = if edge_ref.source() == node {
                    edge_ref.target()
                } else {
                    edge_ref.source()
                };
                *next_rank.entry(neighbor).or_insert(base) += d * share;
            }
        }
        rank = next_rank;
    }

    rank
}

// ---------------------------------------------------------------------------
// Approximate Betweenness Centrality - BFS from `samples` random sources
// ---------------------------------------------------------------------------

pub fn compute_betweenness(
    graph: &Graph<Uuid, EdgeData, petgraph::Undirected>,
    samples: usize,
) -> HashMap<NodeIndex, f64> {
    let n = graph.node_count();
    if n <= 2 {
        return graph.node_indices().map(|idx| (idx, 0.0)).collect();
    }

    let mut centrality: HashMap<NodeIndex, f64> =
        graph.node_indices().map(|idx| (idx, 0.0)).collect();

    let nodes: Vec<NodeIndex> = graph.node_indices().collect();
    let actual_samples = samples.min(nodes.len());

    // Deterministic sampling: use evenly-spaced indices sorted by node index
    let step = if actual_samples > 0 {
        nodes.len() as f64 / actual_samples as f64
    } else {
        1.0
    };

    for i in 0..actual_samples {
        let source = nodes[(i as f64 * step) as usize % nodes.len()];
        brandes_single_source(graph, source, &mut centrality);
    }

    // Normalize by N*(N-1) and scale by (N / actual_samples) for approximation
    let norm = (n as f64) * (n as f64 - 1.0);
    let scale = if actual_samples > 0 {
        n as f64 / actual_samples as f64
    } else {
        1.0
    };

    for val in centrality.values_mut() {
        *val = (*val * scale) / norm;
    }

    centrality
}

/// Single-source shortest-path dependency accumulation (Brandes algorithm).
fn brandes_single_source(
    graph: &Graph<Uuid, EdgeData, petgraph::Undirected>,
    source: NodeIndex,
    centrality: &mut HashMap<NodeIndex, f64>,
) {
    let mut stack: Vec<NodeIndex> = Vec::new();
    let mut predecessors: HashMap<NodeIndex, Vec<NodeIndex>> = HashMap::new();
    let mut sigma: HashMap<NodeIndex, f64> = HashMap::new();
    let mut dist: HashMap<NodeIndex, i64> = HashMap::new();

    for node in graph.node_indices() {
        sigma.insert(node, 0.0);
        dist.insert(node, -1);
    }
    sigma.insert(source, 1.0);
    dist.insert(source, 0);

    let mut queue = VecDeque::new();
    queue.push_back(source);

    while let Some(v) = queue.pop_front() {
        stack.push(v);
        let v_dist = dist[&v];

        for edge_ref in graph.edges(v) {
            let w = if edge_ref.source() == v {
                edge_ref.target()
            } else {
                edge_ref.source()
            };

            if dist[&w] < 0 {
                dist.insert(w, v_dist + 1);
                queue.push_back(w);
            }
            if dist[&w] == v_dist + 1 {
                *sigma.entry(w).or_insert(0.0) += sigma[&v];
                predecessors.entry(w).or_default().push(v);
            }
        }
    }

    let mut delta: HashMap<NodeIndex, f64> = graph.node_indices().map(|idx| (idx, 0.0)).collect();

    while let Some(w) = stack.pop() {
        if let Some(preds) = predecessors.get(&w) {
            for v in preds {
                let coeff = (sigma[v] / sigma[&w]) * (1.0 + delta[&w]);
                *delta.entry(*v).or_insert(0.0) += coeff;
            }
        }
        if w != source {
            *centrality.entry(w).or_insert(0.0) += delta[&w];
        }
    }
}

// ---------------------------------------------------------------------------
// Bridge Score - neighbor disconnection ratio
// ---------------------------------------------------------------------------

pub fn compute_bridge_scores(
    graph: &Graph<Uuid, EdgeData, petgraph::Undirected>,
) -> HashMap<NodeIndex, f64> {
    let mut scores: HashMap<NodeIndex, f64> = HashMap::new();

    for node in graph.node_indices() {
        let neighbors: Vec<NodeIndex> = graph
            .edges(node)
            .map(|e| {
                if e.source() == node {
                    e.target()
                } else {
                    e.source()
                }
            })
            .collect();

        let degree = neighbors.len();
        if degree <= 1 {
            scores.insert(node, 0.0);
            continue;
        }

        // Count how many neighbor pairs are directly connected (without going through this node)
        let mut connected_pairs = 0;
        let total_pairs = degree * (degree - 1) / 2;

        for (i, &a) in neighbors.iter().enumerate() {
            for &b in &neighbors[i + 1..] {
                let a_connected_to_b = graph.edges(a).any(|e| {
                    let other = if e.source() == a {
                        e.target()
                    } else {
                        e.source()
                    };
                    other == b
                });
                if a_connected_to_b {
                    connected_pairs += 1;
                }
            }
        }

        let score = if total_pairs > 0 {
            1.0 - (connected_pairs as f64 / total_pairs as f64)
        } else {
            0.0
        };
        scores.insert(node, score);
    }

    scores
}

// ---------------------------------------------------------------------------
// Connected Components
// ---------------------------------------------------------------------------

pub fn compute_components(
    graph: &Graph<Uuid, EdgeData, petgraph::Undirected>,
) -> (Vec<Vec<NodeIndex>>, HashMap<NodeIndex, usize>) {
    let mut visited: HashSet<NodeIndex> = HashSet::new();
    let mut components: Vec<Vec<NodeIndex>> = Vec::new();
    let mut node_component: HashMap<NodeIndex, usize> = HashMap::new();

    for node in graph.node_indices() {
        if visited.contains(&node) {
            continue;
        }

        let component_idx = components.len();
        let mut component = Vec::new();
        let mut queue = VecDeque::new();
        queue.push_back(node);
        visited.insert(node);

        while let Some(v) = queue.pop_front() {
            component.push(v);
            node_component.insert(v, component_idx);

            for edge_ref in graph.edges(v) {
                let neighbor = if edge_ref.source() == v {
                    edge_ref.target()
                } else {
                    edge_ref.source()
                };
                if visited.insert(neighbor) {
                    queue.push_back(neighbor);
                }
            }
        }

        // Sort by node index for determinism
        component.sort_by_key(|n| n.index());
        components.push(component);
    }

    (components, node_component)
}

// ---------------------------------------------------------------------------
// Jaccard Similarity of neighbor sets
// ---------------------------------------------------------------------------

pub fn jaccard_similarity(
    graph: &Graph<Uuid, EdgeData, petgraph::Undirected>,
    node_a: NodeIndex,
    node_b: NodeIndex,
) -> f64 {
    let neighbors_a: HashSet<NodeIndex> = graph
        .edges(node_a)
        .map(|e| {
            if e.source() == node_a {
                e.target()
            } else {
                e.source()
            }
        })
        .collect();

    let neighbors_b: HashSet<NodeIndex> = graph
        .edges(node_b)
        .map(|e| {
            if e.source() == node_b {
                e.target()
            } else {
                e.source()
            }
        })
        .collect();

    let intersection = neighbors_a.intersection(&neighbors_b).count();
    let union = neighbors_a.union(&neighbors_b).count();

    if union == 0 {
        0.0
    } else {
        intersection as f64 / union as f64
    }
}

// ---------------------------------------------------------------------------
// Risk Scores - composite risk from graph metrics
// ---------------------------------------------------------------------------

pub fn compute_risk_scores(
    graph: &Graph<Uuid, EdgeData, petgraph::Undirected>,
    pagerank: &HashMap<NodeIndex, f64>,
    _components: &[Vec<NodeIndex>],
    node_component: &HashMap<NodeIndex, usize>,
) -> HashMap<NodeIndex, RiskScores> {
    let mut risks: HashMap<NodeIndex, RiskScores> = HashMap::new();

    // Pre-compute max pagerank for normalization
    let max_pr = pagerank.values().cloned().fold(0.0_f64, f64::max);

    for node in graph.node_indices() {
        let degree = graph.edges(node).count();

        // Average edge weight for this node
        let avg_weight = if degree > 0 {
            let total_weight: f64 = graph.edges(node).map(|e| e.weight().weight).sum();
            total_weight / degree as f64
        } else {
            0.0
        };

        // Churn risk: low degree + low avg edge weight = weakly connected
        let degree_factor = 1.0 / (1.0 + degree as f64);
        let weight_factor = 1.0 - avg_weight.min(1.0);
        let churn_risk = (degree_factor * 0.6 + weight_factor * 0.4).min(1.0);

        // Contagion risk: high PageRank + high degree = changes propagate widely
        let pr_norm = if max_pr > 0.0 {
            pagerank.get(&node).copied().unwrap_or(0.0) / max_pr
        } else {
            0.0
        };
        let degree_norm = (degree as f64 / 20.0).min(1.0);
        let contagion_risk = (pr_norm * 0.7 + degree_norm * 0.3).min(1.0);

        // Merge risk: check if any neighbor is in a different component (via soft edges)
        let my_comp = node_component.get(&node).copied().unwrap_or(0);
        let cross_component_neighbors = graph
            .edges(node)
            .filter(|e| {
                let neighbor = if e.source() == node {
                    e.target()
                } else {
                    e.source()
                };
                let neighbor_comp = node_component.get(&neighbor).copied().unwrap_or(0);
                neighbor_comp != my_comp && e.weight().edge_type == "relationship"
            })
            .count();

        let merge_risk = if degree > 0 {
            (cross_component_neighbors as f64 / degree as f64).min(1.0)
        } else {
            0.0
        };

        risks.insert(
            node,
            RiskScores {
                churn_risk,
                contagion_risk,
                merge_risk,
            },
        );
    }

    risks
}

// ---------------------------------------------------------------------------
// 2-hop reach count
// ---------------------------------------------------------------------------

pub fn two_hop_reach(
    graph: &Graph<Uuid, EdgeData, petgraph::Undirected>,
    node: NodeIndex,
) -> usize {
    let mut visited: HashSet<NodeIndex> = HashSet::new();
    visited.insert(node);

    // 1-hop neighbors
    let mut hop1: Vec<NodeIndex> = Vec::new();
    for edge_ref in graph.edges(node) {
        let neighbor = if edge_ref.source() == node {
            edge_ref.target()
        } else {
            edge_ref.source()
        };
        if visited.insert(neighbor) {
            hop1.push(neighbor);
        }
    }

    // 2-hop neighbors
    for n in hop1 {
        for edge_ref in graph.edges(n) {
            let neighbor = if edge_ref.source() == n {
                edge_ref.target()
            } else {
                edge_ref.source()
            };
            visited.insert(neighbor);
        }
    }

    // Exclude self
    visited.len() - 1
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    fn make_edge(weight: f64, edge_type: &str) -> EdgeData {
        EdgeData {
            weight,
            edge_type: edge_type.to_string(),
            attributes: None,
        }
    }

    fn triangle_graph() -> Graph<Uuid, EdgeData, petgraph::Undirected> {
        let mut g = Graph::new_undirected();
        let a = g.add_node(Uuid::nil());
        let b = g.add_node(Uuid::nil());
        let c = g.add_node(Uuid::nil());
        g.add_edge(a, b, make_edge(1.0, "identity"));
        g.add_edge(b, c, make_edge(1.0, "identity"));
        g.add_edge(c, a, make_edge(1.0, "identity"));
        g
    }

    fn star_graph() -> Graph<Uuid, EdgeData, petgraph::Undirected> {
        let mut g = Graph::new_undirected();
        let center = g.add_node(Uuid::nil());
        for _ in 0..4 {
            let leaf = g.add_node(Uuid::nil());
            g.add_edge(center, leaf, make_edge(1.0, "identity"));
        }
        g
    }

    fn path_graph() -> Graph<Uuid, EdgeData, petgraph::Undirected> {
        let mut g = Graph::new_undirected();
        let nodes: Vec<_> = (0..5).map(|_| g.add_node(Uuid::nil())).collect();
        for i in 0..4 {
            g.add_edge(nodes[i], nodes[i + 1], make_edge(1.0, "identity"));
        }
        g
    }

    fn disconnected_graph() -> Graph<Uuid, EdgeData, petgraph::Undirected> {
        let mut g = Graph::new_undirected();
        let a = g.add_node(Uuid::nil());
        let b = g.add_node(Uuid::nil());
        let c = g.add_node(Uuid::nil());
        g.add_edge(a, b, make_edge(1.0, "identity"));
        g.add_edge(b, c, make_edge(1.0, "identity"));
        g.add_edge(c, a, make_edge(1.0, "identity"));
        let d = g.add_node(Uuid::nil());
        let e = g.add_node(Uuid::nil());
        let f = g.add_node(Uuid::nil());
        g.add_edge(d, e, make_edge(1.0, "identity"));
        g.add_edge(e, f, make_edge(1.0, "identity"));
        g.add_edge(f, d, make_edge(1.0, "identity"));
        g
    }

    #[test]
    fn test_pagerank_empty() {
        let g: Graph<Uuid, EdgeData, petgraph::Undirected> = Graph::new_undirected();
        assert!(compute_pagerank(&g).is_empty());
    }

    #[test]
    fn test_pagerank_triangle_equal() {
        let g = triangle_graph();
        let pr = compute_pagerank(&g);
        assert_eq!(pr.len(), 3);
        let values: Vec<f64> = pr.values().copied().collect();
        let max = values.iter().cloned().fold(0.0_f64, f64::max);
        let min = values.iter().cloned().fold(1.0_f64, f64::min);
        assert!((max - min).abs() < 0.01);
    }

    #[test]
    fn test_pagerank_star_center() {
        let g = star_graph();
        let pr = compute_pagerank(&g);
        let center = NodeIndex::new(0);
        let center_rank = pr[&center];
        for (node, rank) in &pr {
            if *node != center {
                assert!(center_rank > *rank);
            }
        }
    }

    #[test]
    fn test_betweenness_path() {
        let g = path_graph();
        let bc = compute_betweenness(&g, 5);
        let center = NodeIndex::new(2);
        assert!(bc[&center] > bc[&NodeIndex::new(0)]);
    }

    #[test]
    fn test_components_single() {
        let g = triangle_graph();
        let (comps, _) = compute_components(&g);
        assert_eq!(comps.len(), 1);
    }

    #[test]
    fn test_components_disconnected() {
        let g = disconnected_graph();
        let (comps, _) = compute_components(&g);
        assert_eq!(comps.len(), 2);
    }

    #[test]
    fn test_jaccard() {
        let g = triangle_graph();
        let j = jaccard_similarity(&g, NodeIndex::new(0), NodeIndex::new(1));
        assert!((j - 1.0 / 3.0).abs() < 0.01);
    }

    #[test]
    fn test_two_hop_star() {
        let g = star_graph();
        assert_eq!(two_hop_reach(&g, NodeIndex::new(0)), 4);
    }

    #[test]
    fn test_risk_isolated() {
        let mut g: Graph<Uuid, EdgeData, petgraph::Undirected> = Graph::new_undirected();
        let a = g.add_node(Uuid::nil());
        let pr = compute_pagerank(&g);
        let (comps, nc) = compute_components(&g);
        let risks = compute_risk_scores(&g, &pr, &comps, &nc);
        assert!(risks[&a].churn_risk > 0.5);
    }
}
