// Copyright 2026 Gregorio Momm
// Licensed under the Apache License, Version 2.0
//
// This file implements centrality algorithms ported from NetworKit C++ to pure Rust.
//
// NetworKit: https://networkit.github.io/ (MIT License)
//
// Academic References:
// - PageRank: Page, L., Brin, S., Motwani, R., & Winograd, T. (1999)
//   "The PageRank Citation Ranking: Bringing Order to the Web"
//   Stanford InfoLab Technical Report 1999-66
//
// - Betweenness Centrality: Brandes, U. (2001)
//   "A faster algorithm for betweenness centrality"
//   Journal of Mathematical Sociology, 25(2), 163-177
//
// - Closeness Centrality: Freeman, L. C. (1978)
//   "Centrality in social networks conceptual clarification"
//   Social Networks, 1(3), 215-239

//! Centrality algorithms ported from NetworKit.
//!
//! This module implements NetworKit's centrality measures:
//! - Degree Centrality
//! - Betweenness Centrality (Brandes algorithm)
//! - Closeness Centrality
//! - PageRank
//! - Eigenvector Centrality
//!
//! Reference: <https://networkit.github.io/dev-docs/python_api/centrality.html>

use std::collections::VecDeque;

use super::super::graph::{Graph, NodeId};
use super::distance::BFS;

/// Centrality scores for all nodes in a graph.
#[derive(Debug, Clone)]
pub struct CentralityResult {
    /// Centrality score for each node.
    pub scores: Vec<f64>,
}

impl CentralityResult {
    /// Get the centrality score for a specific node.
    pub fn score(&self, node: NodeId) -> f64 {
        self.scores.get(node as usize).copied().unwrap_or(0.0)
    }

    /// Get the node with the highest centrality.
    pub fn max_node(&self) -> Option<(NodeId, f64)> {
        self.scores
            .iter()
            .enumerate()
            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
            .map(|(id, &score)| (id as NodeId, score))
    }

    /// Get top-k nodes by centrality.
    pub fn top_k(&self, k: usize) -> Vec<(NodeId, f64)> {
        let mut scored: Vec<_> = self.scores
            .iter()
            .enumerate()
            .map(|(id, &score)| (id as NodeId, score))
            .collect();

        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
        scored.into_iter().take(k).collect()
    }
}

/// Degree Centrality - simplest centrality measure.
///
/// Counts the number of edges connected to each node.
/// Runtime: O(n + m)
///
/// Reference: NetworKit DegreeCentrality class
pub struct DegreeCentrality {
    normalized: bool,
}

impl DegreeCentrality {
    /// Create a new degree centrality calculator.
    pub fn new() -> Self {
        Self { normalized: false }
    }

    /// Enable normalization (divide by n-1).
    pub fn normalized(mut self) -> Self {
        self.normalized = true;
        self
    }

    /// Compute degree centrality for all nodes.
    pub fn run(&self, graph: &Graph) -> CentralityResult {
        let n = graph.upper_node_id_bound() as usize;
        let mut scores = vec![0.0; n];

        for node in graph.nodes() {
            let degree = graph.degree(node) as f64;

            scores[node as usize] = if self.normalized && n > 1 {
                degree / (n - 1) as f64
            } else {
                degree
            };
        }

        CentralityResult { scores }
    }
}

impl Default for DegreeCentrality {
    fn default() -> Self {
        Self::new()
    }
}

/// Betweenness Centrality using Brandes' algorithm.
///
/// Measures how often a node lies on shortest paths between other nodes.
/// Runtime: O(nm) for unweighted graphs, O(nm + n² log n) for weighted
///
/// Reference: NetworKit Betweenness class
/// Algorithm: Brandes, U. (2001)
pub struct BetweennessCentrality {
    normalized: bool,
}

impl BetweennessCentrality {
    /// Create a new betweenness centrality calculator.
    pub fn new() -> Self {
        Self { normalized: false }
    }

    /// Enable normalization.
    pub fn normalized(mut self) -> Self {
        self.normalized = true;
        self
    }

    /// Compute betweenness centrality using Brandes' algorithm.
    pub fn run(&self, graph: &Graph) -> CentralityResult {
        let n = graph.upper_node_id_bound() as usize;
        let mut betweenness = vec![0.0; n];

        // Brandes' algorithm: run BFS from each node
        for source in graph.nodes() {
            // Single-source shortest paths
            let mut stack = Vec::new();
            let mut predecessors: Vec<Vec<NodeId>> = vec![Vec::new(); n];
            let mut sigma = vec![0.0; n]; // Number of shortest paths
            let mut distance = vec![None; n];

            sigma[source as usize] = 1.0;
            distance[source as usize] = Some(0.0);

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

            // Forward pass: compute shortest paths
            while let Some(node) = queue.pop_front() {
                stack.push(node);
                let d = distance[node as usize].unwrap();

                for neighbor in graph.out_neighbors(node) {
                    let next = neighbor.target;

                    // First time visiting this node
                    if distance[next as usize].is_none() {
                        distance[next as usize] = Some(d + 1.0);
                        queue.push_back(next);
                    }

                    // Shortest path to next via node?
                    if distance[next as usize] == Some(d + 1.0) {
                        sigma[next as usize] += sigma[node as usize];
                        predecessors[next as usize].push(node);
                    }
                }
            }

            // Backward pass: accumulate dependencies
            let mut delta = vec![0.0; n];

            while let Some(node) = stack.pop() {
                for &pred in &predecessors[node as usize] {
                    delta[pred as usize] += (sigma[pred as usize] / sigma[node as usize])
                        * (1.0 + delta[node as usize]);
                }

                if node != source {
                    betweenness[node as usize] += delta[node as usize];
                }
            }
        }

        // Normalization
        if self.normalized && n > 2 {
            let norm_factor = ((n - 1) * (n - 2)) as f64;
            for score in &mut betweenness {
                *score /= norm_factor;
            }
        }

        CentralityResult { scores: betweenness }
    }
}

impl Default for BetweennessCentrality {
    fn default() -> Self {
        Self::new()
    }
}

/// Closeness Centrality.
///
/// Measures the average distance from a node to all other nodes.
/// Runtime: O(n * (n + m))
///
/// Reference: NetworKit Closeness class
pub struct ClosenessCentrality {
    normalized: bool,
    harmonic: bool,
}

impl ClosenessCentrality {
    /// Create a new closeness centrality calculator.
    pub fn new() -> Self {
        Self {
            normalized: false,
            harmonic: false,
        }
    }

    /// Enable normalization.
    pub fn normalized(mut self) -> Self {
        self.normalized = true;
        self
    }

    /// Use harmonic mean (better for disconnected graphs).
    pub fn harmonic(mut self) -> Self {
        self.harmonic = true;
        self
    }

    /// Compute closeness centrality.
    pub fn run(&self, graph: &Graph) -> CentralityResult {
        let n = graph.upper_node_id_bound() as usize;
        let mut scores = vec![0.0; n];

        for node in graph.nodes() {
            let result = BFS::new(graph, node).run();

            let (sum, count) = if self.harmonic {
                // Harmonic closeness: sum of 1/distance
                let mut sum = 0.0;
                let mut count = 0;
                for dist in result.distances.iter().flatten() {
                    if *dist > 0.0 {
                        sum += 1.0 / dist;
                        count += 1;
                    }
                }
                (sum, count)
            } else {
                // Standard closeness: 1 / sum of distances
                let sum: f64 = result.distances.iter().flatten().sum();
                let count = result.distances.iter().flatten().count();
                (sum, count)
            };

            if count > 0 {
                scores[node as usize] = if self.harmonic {
                    if self.normalized {
                        sum / (n - 1) as f64
                    } else {
                        sum
                    }
                } else {
                    let closeness = (count as f64) / sum;
                    if self.normalized {
                        closeness * ((count as f64) / (n - 1) as f64)
                    } else {
                        closeness
                    }
                };
            }
        }

        CentralityResult { scores }
    }
}

impl Default for ClosenessCentrality {
    fn default() -> Self {
        Self::new()
    }
}

/// PageRank centrality.
///
/// Iterative algorithm measuring importance based on incoming links.
/// Runtime: O(iterations * m)
///
/// Reference: NetworKit PageRank class
pub struct PageRank {
    damping: f64,
    tolerance: f64,
    max_iterations: usize,
}

impl PageRank {
    /// Create a new PageRank calculator with default parameters.
    pub fn new() -> Self {
        Self {
            damping: 0.85,
            tolerance: 1e-9,
            max_iterations: 100,
        }
    }

    /// Set damping factor (default: 0.85).
    pub fn damping(mut self, damping: f64) -> Self {
        self.damping = damping;
        self
    }

    /// Set convergence tolerance (default: 1e-9).
    pub fn tolerance(mut self, tol: f64) -> Self {
        self.tolerance = tol;
        self
    }

    /// Set maximum iterations (default: 100).
    pub fn max_iterations(mut self, max_iter: usize) -> Self {
        self.max_iterations = max_iter;
        self
    }

    /// Compute PageRank scores.
    pub fn run(&self, graph: &Graph) -> CentralityResult {
        let n = graph.upper_node_id_bound() as usize;
        let node_count = graph.node_count();

        if node_count == 0 {
            return CentralityResult { scores: vec![] };
        }

        let initial_rank = 1.0 / node_count as f64;
        let mut ranks = vec![initial_rank; n];
        let mut new_ranks = vec![0.0; n];

        let damping_factor = self.damping;
        let base_rank = (1.0 - damping_factor) / node_count as f64;

        for _ in 0..self.max_iterations {
            // Reset new ranks
            new_ranks.fill(base_rank);

            // Distribute rank from each node
            for node in graph.nodes() {
                let degree = graph.degree(node);
                if degree > 0 {
                    let contribution = ranks[node as usize] * damping_factor / degree as f64;

                    for neighbor in graph.out_neighbors(node) {
                        new_ranks[neighbor.target as usize] += contribution;
                    }
                }
            }

            // Check convergence
            let diff: f64 = ranks
                .iter()
                .zip(new_ranks.iter())
                .map(|(old, new)| (old - new).abs())
                .sum();

            std::mem::swap(&mut ranks, &mut new_ranks);

            if diff < self.tolerance {
                break;
            }
        }

        CentralityResult { scores: ranks }
    }
}

impl Default for PageRank {
    fn default() -> Self {
        Self::new()
    }
}

// ─── Eigenvector Centrality ───────────────────────────────────────────────────

/// Eigenvector Centrality.
///
/// Computes centrality scores as the principal eigenvector of the adjacency matrix,
/// using the power iteration method.
///
/// A node's score is proportional to the sum of its neighbors' scores —
/// being connected to high-scoring nodes increases your own score.
///
/// Runtime: O(iterations × m)
///
/// Reference: Bonacich, P. (1987). "Power and Centrality: A Family of Measures."
/// American Journal of Sociology, 92(5), 1170-1182.
pub struct EigenvectorCentrality {
    tolerance: f64,
    max_iterations: usize,
}

impl EigenvectorCentrality {
    pub fn new() -> Self {
        Self { tolerance: 1e-6, max_iterations: 100 }
    }

    pub fn tolerance(mut self, tol: f64) -> Self {
        self.tolerance = tol;
        self
    }

    pub fn max_iterations(mut self, max_iter: usize) -> Self {
        self.max_iterations = max_iter;
        self
    }

    /// Compute eigenvector centrality via power iteration.
    ///
    /// Uses a shifted iteration `x_new = (A + I) * x` to handle bipartite graphs
    /// (plain A*x oscillates on bipartite graphs like star graphs).
    ///
    /// Returns `None` if the algorithm did not converge.
    pub fn run(&self, graph: &Graph) -> Option<CentralityResult> {
        let n = graph.upper_node_id_bound() as usize;
        if n == 0 { return Some(CentralityResult { scores: vec![] }); }

        // Start with uniform vector
        let mut x = vec![1.0_f64 / (n as f64).sqrt(); n];
        let mut x_new = vec![0.0_f64; n];

        for _ in 0..self.max_iterations {
            // x_new = (A + I) * x  (shift breaks bipartite oscillation)
            for i in 0..n { x_new[i] = x[i]; } // identity part
            for u in graph.nodes() {
                for e in graph.out_neighbors(u).iter() {
                    x_new[e.target as usize] += x[u as usize];
                }
            }

            // Normalize by L2 norm
            let norm: f64 = x_new.iter().map(|v| v * v).sum::<f64>().sqrt();
            if norm < 1e-15 { return None; }
            for v in x_new.iter_mut() { *v /= norm; }

            // Check convergence
            let diff: f64 = x.iter().zip(x_new.iter())
                .map(|(a, b)| (a - b).abs()).sum();
            std::mem::swap(&mut x, &mut x_new);
            if diff < self.tolerance { break; }
        }

        // Normalize so max score = 1
        let max = x.iter().cloned().fold(0.0_f64, f64::max);
        if max > 0.0 { for v in x.iter_mut() { *v /= max; } }

        Some(CentralityResult { scores: x })
    }
}

impl Default for EigenvectorCentrality {
    fn default() -> Self { Self::new() }
}

// ─── Katz Centrality ─────────────────────────────────────────────────────────

/// Katz Centrality.
///
/// Generalizes eigenvector centrality by giving every node a free "base" centrality α.
/// The attenuation factor β (< 1/λ_max) controls how quickly influence decays with distance.
///
/// Formula: x = (I - β·Aᵀ)⁻¹ · α·1  ≈ power iteration
///
/// Runtime: O(iterations × m)
///
/// Reference: Katz, L. (1953). "A New Status Index Derived from Sociometric Analysis."
/// Psychometrika, 18(1), 39-43.
pub struct KatzCentrality {
    alpha: f64,
    beta: f64,
    tolerance: f64,
    max_iterations: usize,
    normalized: bool,
}

impl KatzCentrality {
    /// Create Katz centrality with default attenuation β=0.1 and base α=1.0.
    pub fn new() -> Self {
        Self {
            alpha: 1.0,
            beta: 0.1,
            tolerance: 1e-6,
            max_iterations: 100,
            normalized: false,
        }
    }

    pub fn alpha(mut self, alpha: f64) -> Self { self.alpha = alpha; self }
    pub fn beta(mut self, beta: f64) -> Self { self.beta = beta; self }
    pub fn tolerance(mut self, tol: f64) -> Self { self.tolerance = tol; self }
    pub fn max_iterations(mut self, max_iter: usize) -> Self { self.max_iterations = max_iter; self }
    pub fn normalized(mut self) -> Self { self.normalized = true; self }

    /// Compute Katz centrality via power iteration.
    pub fn run(&self, graph: &Graph) -> CentralityResult {
        let n = graph.upper_node_id_bound() as usize;
        if n == 0 { return CentralityResult { scores: vec![] }; }

        let mut x = vec![0.0_f64; n];
        let mut x_new = vec![0.0_f64; n];

        for _ in 0..self.max_iterations {
            // x_new[v] = α + β * Σ_{u: neighbor of v} x[u]
            for i in 0..n { x_new[i] = self.alpha; }
            for u in graph.nodes() {
                for e in graph.out_neighbors(u).iter() {
                    x_new[e.target as usize] += self.beta * x[u as usize];
                }
            }

            let diff: f64 = x.iter().zip(x_new.iter())
                .map(|(a, b)| (a - b).abs()).sum();
            std::mem::swap(&mut x, &mut x_new);
            if diff < self.tolerance { break; }
        }

        if self.normalized {
            // Subtract 1 (remove the base) and normalize by L2 norm
            for v in x.iter_mut() { *v -= 1.0; }
            let norm: f64 = x.iter().map(|v| v * v).sum::<f64>().sqrt();
            if norm > 0.0 { for v in x.iter_mut() { *v /= norm; } }
        }

        CentralityResult { scores: x }
    }
}

impl Default for KatzCentrality {
    fn default() -> Self { Self::new() }
}

// ─── Harmonic Centrality ─────────────────────────────────────────────────────

/// Harmonic Centrality.
///
/// Sum of inverse distances to all other nodes.
/// Unlike closeness centrality, it handles disconnected graphs gracefully
/// (unreachable nodes contribute 0 instead of making the sum infinite).
///
/// Runtime: O(n × (n + m))
///
/// Reference: Marchiori, M., & Latora, V. (2000). "Harmony in the small-world."
/// Physica A: Statistical Mechanics and its Applications, 285(3-4), 539-546.
pub fn harmonic_centrality(graph: &Graph) -> CentralityResult {
    let n = graph.upper_node_id_bound() as usize;
    let mut scores = vec![0.0_f64; n];

    for node in graph.nodes() {
        let result = BFS::new(graph, node).run();
        let score: f64 = result.distances.iter()
            .enumerate()
            .filter_map(|(i, d)| {
                let d = (*d)?;
                if i == node as usize || d == 0.0 { None } else { Some(1.0 / d) }
            })
            .sum();
        scores[node as usize] = score;
    }

    CentralityResult { scores }
}

// ─── VoteRank ────────────────────────────────────────────────────────────────

/// VoteRank: iteratively select the most influential spreaders.
///
/// Nodes vote for their neighbors; the node with the most votes is selected
/// and removed from the voting pool (along with its neighbors, who lose voting weight).
///
/// Returns an ordered list of influential nodes.
///
/// Reference: Zhang, J.-X., et al. (2016). "Identifying a set of influential spreaders in complex networks."
/// Scientific Reports, 6, 27823.
pub fn vote_rank(graph: &Graph, num_seeds: usize) -> Vec<NodeId> {
    let n = graph.upper_node_id_bound() as usize;
    if n == 0 { return vec![]; }

    let mut scores = vec![0.0_f64; n];
    let mut voting_ability = vec![1.0_f64; n];
    let avg_degree = if graph.node_count() > 0 {
        graph.nodes().map(|u| graph.degree(u) as f64).sum::<f64>() / graph.node_count() as f64
    } else { 1.0 };
    let reduction = if avg_degree > 0.0 { 1.0 / avg_degree } else { 0.0 };

    // Initial vote scores: degree of each node
    for u in graph.nodes() {
        scores[u as usize] = graph.degree(u) as f64;
    }

    let mut selected = Vec::new();
    let mut selected_set: Vec<bool> = vec![false; n];

    for _ in 0..num_seeds {
        // Find node with max score (not already selected)
        let winner = graph.nodes()
            .filter(|&u| !selected_set[u as usize])
            .max_by(|&a, &b| scores[a as usize].partial_cmp(&scores[b as usize]).unwrap());

        let winner = match winner { Some(w) => w, None => break };
        if scores[winner as usize] <= 0.0 { break; }

        selected.push(winner);
        selected_set[winner as usize] = true;

        // Winner's score = 0; reduce neighbors' voting ability
        scores[winner as usize] = 0.0;
        for e in graph.out_neighbors(winner).iter() {
            let nbr = e.target as usize;
            if !selected_set[nbr] {
                voting_ability[nbr] -= reduction;
                if voting_ability[nbr] < 0.0 { voting_ability[nbr] = 0.0; }
            }
        }

        // Recompute scores based on updated voting abilities
        for u in graph.nodes() {
            if selected_set[u as usize] { scores[u as usize] = 0.0; continue; }
            let s: f64 = graph.out_neighbors(u).iter()
                .filter(|e| !selected_set[e.target as usize])
                .map(|e| voting_ability[e.target as usize])
                .sum();
            scores[u as usize] = s;
        }
    }

    selected
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::graph::backends::networkit_rust::graph::GraphConfig;

    fn create_star_graph() -> Graph {
        let mut graph = Graph::new(GraphConfig::simple());

        // Create a star: center (0) connected to 4 outer nodes
        for _ in 0..5 {
            graph.add_node();
        }

        graph.add_edge(0, 1, None);
        graph.add_edge(0, 2, None);
        graph.add_edge(0, 3, None);
        graph.add_edge(0, 4, None);

        graph
    }

    #[test]
    fn test_degree_centrality() {
        let graph = create_star_graph();
        let result = DegreeCentrality::new().run(&graph);

        // Center has degree 4
        assert_eq!(result.score(0), 4.0);

        // Outer nodes have degree 1
        assert_eq!(result.score(1), 1.0);
    }

    #[test]
    fn test_degree_centrality_normalized() {
        let graph = create_star_graph();
        let result = DegreeCentrality::new().normalized().run(&graph);

        // Normalized by n-1 = 4
        assert_eq!(result.score(0), 1.0); // 4/4
        assert_eq!(result.score(1), 0.25); // 1/4
    }

    #[test]
    fn test_betweenness_centrality() {
        let graph = create_star_graph();
        let result = BetweennessCentrality::new().run(&graph);

        // Center has high betweenness (all paths go through it)
        assert!(result.score(0) > 0.0);

        // Outer nodes have zero betweenness
        assert_eq!(result.score(1), 0.0);
    }

    #[test]
    fn test_closeness_centrality() {
        let graph = create_star_graph();
        let result = ClosenessCentrality::new().run(&graph);

        // Center has highest closeness (average distance is 1)
        let center_closeness = result.score(0);
        let outer_closeness = result.score(1);

        assert!(center_closeness > outer_closeness);
    }

    #[test]
    fn test_pagerank() {
        let graph = create_star_graph();
        let result = PageRank::new().run(&graph);

        // All nodes should have some rank
        for node in graph.nodes() {
            assert!(result.score(node) > 0.0);
        }

        // Sum of all ranks should be ~1.0
        let total: f64 = graph.nodes().map(|n| result.score(n)).sum();
        assert!((total - 1.0).abs() < 0.01);
    }

    #[test]
    fn test_eigenvector_centrality_star() {
        let graph = create_star_graph();
        let result = EigenvectorCentrality::new().run(&graph)
            .expect("should converge");
        // Center (0) is most central
        assert!(result.score(0) > result.score(1),
            "center score {} should exceed leaf score {}", result.score(0), result.score(1));
    }

    #[test]
    fn test_eigenvector_centrality_path() {
        // Path 0-1-2: middle node (1) has highest eigenvector centrality
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        let result = EigenvectorCentrality::new().run(&g).expect("converge");
        assert!(result.score(1) >= result.score(0),
            "middle {} should >= end {}", result.score(1), result.score(0));
    }

    #[test]
    fn test_eigenvector_centrality_empty() {
        let g = Graph::new(GraphConfig::simple());
        let result = EigenvectorCentrality::new().run(&g).expect("empty ok");
        assert!(result.scores.is_empty());
    }

    #[test]
    fn test_katz_centrality_star() {
        let graph = create_star_graph();
        let result = KatzCentrality::new().run(&graph);
        // Center accumulates more from its 4 neighbors
        assert!(result.score(0) > result.score(1),
            "center {} > leaf {}", result.score(0), result.score(1));
    }

    #[test]
    fn test_katz_centrality_empty() {
        let g = Graph::new(GraphConfig::simple());
        let result = KatzCentrality::new().run(&g);
        assert!(result.scores.is_empty());
    }

    #[test]
    fn test_harmonic_centrality_star() {
        let graph = create_star_graph();
        let result = harmonic_centrality(&graph);
        // Center (node 0): distances to leaves = 1, sum = 4 × (1/1) = 4
        assert!((result.score(0) - 4.0).abs() < 1e-9,
            "center harmonic = {}", result.score(0));
        // Leaf (node 1): distance to center = 1, to other leaves = 2
        // => 1/1 + 3*(1/2) = 2.5
        assert!((result.score(1) - 2.5).abs() < 1e-9,
            "leaf harmonic = {}", result.score(1));
    }

    #[test]
    fn test_harmonic_centrality_empty() {
        let g = Graph::new(GraphConfig::simple());
        let r = harmonic_centrality(&g);
        assert!(r.scores.is_empty());
    }

    #[test]
    fn test_vote_rank_star() {
        let graph = create_star_graph();
        // Center (node 0) should be selected first — it's the most connected
        let seeds = vote_rank(&graph, 2);
        assert!(!seeds.is_empty());
        assert_eq!(seeds[0], 0, "center should be first seed");
    }

    #[test]
    fn test_vote_rank_empty() {
        let g = Graph::new(GraphConfig::simple());
        let seeds = vote_rank(&g, 5);
        assert!(seeds.is_empty());
    }

    #[test]
    fn test_vote_rank_fewer_than_k() {
        // Only 3 nodes; asking for 10 seeds should return at most 3
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        let seeds = vote_rank(&g, 10);
        assert!(seeds.len() <= 3);
    }
}

// ─── HITS ─────────────────────────────────────────────────────────────────────

/// Result of the HITS algorithm.
#[derive(Debug, Clone)]
pub struct HitsResult {
    /// Hub scores: nodes that link to many good authorities.
    pub hubs: Vec<f64>,
    /// Authority scores: nodes linked to by many good hubs.
    pub authorities: Vec<f64>,
}

/// HITS (Hyperlink-Induced Topic Search) algorithm.
///
/// Computes hub and authority scores for all nodes in a directed graph via
/// power iteration.
///
/// - Authority score  `a[v] = Σ hub[u]` for every edge u→v
/// - Hub score        `h[u] = Σ auth[v]` for every edge u→v
///
/// Normalized to unit L2 norm at each iteration.
///
/// Reference: Kleinberg, J.M. (1999) "Authoritative sources in a
/// hyperlinked environment." Journal of the ACM, 46(5), 604-632.
///
/// Runtime: O(max_iter * m)
pub fn hits(graph: &Graph, max_iter: usize, tol: f64) -> HitsResult {
    let n = graph.upper_node_id_bound() as usize;
    if n == 0 {
        return HitsResult { hubs: vec![], authorities: vec![] };
    }

    let mut hub = vec![1.0_f64; n];
    let mut auth = vec![1.0_f64; n];

    for _ in 0..max_iter {
        let mut new_auth = vec![0.0_f64; n];
        let mut new_hub = vec![0.0_f64; n];

        // Authority update: auth[v] += hub[u] for each edge u→v
        for u in graph.nodes() {
            for e in graph.out_neighbors(u).iter() {
                new_auth[e.target as usize] += hub[u as usize];
            }
        }

        // Hub update: hub[u] += auth[v] for each edge u→v
        for u in graph.nodes() {
            for e in graph.out_neighbors(u).iter() {
                new_hub[u as usize] += new_auth[e.target as usize];
            }
        }

        // Normalize to L2 norm
        let auth_norm: f64 = new_auth.iter().map(|x| x * x).sum::<f64>().sqrt().max(1e-12);
        let hub_norm:  f64 = new_hub.iter().map(|x| x * x).sum::<f64>().sqrt().max(1e-12);
        for v in graph.nodes() {
            new_auth[v as usize] /= auth_norm;
            new_hub[v as usize]  /= hub_norm;
        }

        // Check convergence
        let auth_delta: f64 = graph.nodes()
            .map(|v| (new_auth[v as usize] - auth[v as usize]).abs())
            .fold(0.0_f64, f64::max);
        let hub_delta: f64 = graph.nodes()
            .map(|v| (new_hub[v as usize] - hub[v as usize]).abs())
            .fold(0.0_f64, f64::max);

        auth = new_auth;
        hub  = new_hub;

        if auth_delta < tol && hub_delta < tol {
            break;
        }
    }

    HitsResult { hubs: hub, authorities: auth }
}

#[cfg(test)]
mod hits_tests {
    use super::*;
    use crate::graph::backends::networkit_rust::graph::GraphConfig;

    #[test]
    fn test_hits_empty() {
        let g = Graph::new(GraphConfig::directed());
        let r = hits(&g, 100, 1e-6);
        assert!(r.hubs.is_empty() && r.authorities.is_empty());
    }

    #[test]
    fn test_hits_single_node() {
        let mut g = Graph::new(GraphConfig::directed());
        g.add_node();
        let r = hits(&g, 100, 1e-6);
        assert_eq!(r.hubs.len(), 1);
        assert_eq!(r.authorities.len(), 1);
    }

    #[test]
    fn test_hits_star_directed() {
        // Hub 0 points to authorities 1,2,3
        let mut g = Graph::new(GraphConfig::directed());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(0, 2, None);
        g.add_edge(0, 3, None);
        let r = hits(&g, 200, 1e-8);
        // Node 0 should be best hub, nodes 1,2,3 best authorities
        let best_hub = r.hubs.iter().enumerate()
            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap()).unwrap().0;
        assert_eq!(best_hub, 0, "center should be best hub, hubs={:?}", r.hubs);
        let best_auth = r.authorities.iter().enumerate()
            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap()).unwrap().0;
        assert_ne!(best_auth, 0, "center should NOT be best authority, auth={:?}", r.authorities);
    }

    #[test]
    fn test_hits_chain() {
        // 0→1→2→3: scores should all be positive
        let mut g = Graph::new(GraphConfig::directed());
        for _ in 0..4 { g.add_node(); }
        for i in 0..3u64 { g.add_edge(i, i + 1, None); }
        let r = hits(&g, 100, 1e-6);
        for v in g.nodes() {
            assert!(r.hubs[v as usize] >= 0.0);
            assert!(r.authorities[v as usize] >= 0.0);
        }
    }
}
