// Copyright 2026 Gregorio Momm
// Licensed under the Apache License, Version 2.0

//! Native algorithm methods for `NetworKitRustBackend`.
//!
//! This module implements algorithm methods directly on `NetworKitRustBackend`
//! by calling the existing algorithm free functions with NKNodeId ↔ u64 conversion.

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

use crate::graph::backends::networkit_rust::graph::NodeId as NKNodeId;
use crate::graph::backends::networkit_rust::algorithms::{
    // Centrality
    DegreeCentrality, BetweennessCentrality, ClosenessCentrality, PageRank,
    // Path
    BFS, Dijkstra,
    astar, bellman_ford, floyd_warshall, all_pairs_dijkstra,
    // Spanning trees
    minimum_spanning_tree_kruskal, maximum_spanning_tree_kruskal,
    // DAG
    topological_sort, is_dag, find_cycles,
    dag_longest_path, dag_longest_path_weighted, transitive_closure,
    // Flow
    edmonds_karp, min_cut, FlowResult,
    // Coloring
    greedy_node_color, greedy_edge_color, chromatic_number_approx,
    // Matching
    max_weight_matching, max_cardinality_matching,
    // Community
    louvain, label_propagation, girvan_newman,
    // Components
    ConnectedComponents, StronglyConnectedComponents,
    // Euler
    has_euler_circuit, has_euler_path, euler_circuit, euler_path,
    // Planarity
    is_planar,
};
use crate::graph::backends::petgraph_algorithms::CommunityInfo;

use super::backend::NetworKitRustBackend;

/// Return type for max-flow: (max_flow_value, per-edge flow amounts)
type MaxFlowResult = Option<(f64, HashMap<(u64, u64), f64>)>;

impl NetworKitRustBackend {
    // ── ID Conversion Helpers ──────────────────────────────────────────────

    /// Convert a stable u64 ID to a NetworKit node ID.
    pub(crate) fn u64_to_nk(&self, id: u64) -> Option<NKNodeId> {
        self.node_id_map.get(&id).copied()
    }

    // ── Centrality ────────────────────────────────────────────────────────

    /// Degree centrality for all nodes.
    ///
    /// Returns: HashMap mapping stable node_id → score
    pub fn degree_centrality(&self) -> HashMap<u64, f64> {
        let result = DegreeCentrality::new().run(&self.graph);
        self.graph
            .nodes()
            .filter_map(|nk_id| {
                let stable = self.nk_to_stable.get(&nk_id).copied()?;
                let score = result.scores.get(nk_id as usize).copied().unwrap_or(0.0);
                Some((stable, score))
            })
            .collect()
    }

    /// Betweenness centrality (Brandes algorithm).
    ///
    /// Returns: HashMap mapping stable node_id → score
    pub fn betweenness_centrality(&self, normalized: bool) -> HashMap<u64, f64> {
        let mut bc = BetweennessCentrality::new();
        if normalized {
            bc = bc.normalized();
        }
        let result = bc.run(&self.graph);
        self.graph
            .nodes()
            .filter_map(|nk_id| {
                let stable = self.nk_to_stable.get(&nk_id).copied()?;
                let score = result.scores.get(nk_id as usize).copied().unwrap_or(0.0);
                Some((stable, score))
            })
            .collect()
    }

    /// Closeness centrality.
    ///
    /// Returns: HashMap mapping stable node_id → score
    pub fn closeness_centrality(&self, normalized: bool) -> HashMap<u64, f64> {
        let mut cc = ClosenessCentrality::new();
        if normalized {
            cc = cc.normalized();
        }
        let result = cc.run(&self.graph);
        self.graph
            .nodes()
            .filter_map(|nk_id| {
                let stable = self.nk_to_stable.get(&nk_id).copied()?;
                let score = result.scores.get(nk_id as usize).copied().unwrap_or(0.0);
                Some((stable, score))
            })
            .collect()
    }

    /// PageRank scores.
    ///
    /// Returns: HashMap mapping stable node_id → score
    pub fn pagerank(&self, damping: f64, max_iter: usize) -> HashMap<u64, f64> {
        let result = PageRank::new()
            .damping(damping)
            .max_iterations(max_iter)
            .run(&self.graph);
        self.graph
            .nodes()
            .filter_map(|nk_id| {
                let stable = self.nk_to_stable.get(&nk_id).copied()?;
                let score = result.scores.get(nk_id as usize).copied().unwrap_or(0.0);
                Some((stable, score))
            })
            .collect()
    }

    // ── Pathfinding ───────────────────────────────────────────────────────

    /// BFS shortest path (unweighted).
    ///
    /// Returns: Some(Vec of stable node IDs) or None if unreachable
    pub fn bfs_path(&self, source: u64, target: u64) -> Option<Vec<u64>> {
        let src_nk = self.u64_to_nk(source)?;
        let tgt_nk = self.u64_to_nk(target)?;
        let result = BFS::new(&self.graph, src_nk)
            .with_target(tgt_nk)
            .store_paths(true)
            .run();
        result.path(tgt_nk).map(|path| {
            path.into_iter()
                .filter_map(|nk_id| self.nk_to_stable.get(&nk_id).copied())
                .collect()
        })
    }

    /// Dijkstra weighted shortest path.
    ///
    /// Returns: Some((cost, Vec of stable node IDs)) or None if unreachable
    pub fn dijkstra_path(&self, source: u64, target: u64) -> Option<(f64, Vec<u64>)> {
        let src_nk = self.u64_to_nk(source)?;
        let tgt_nk = self.u64_to_nk(target)?;
        let result = Dijkstra::new(&self.graph, src_nk)
            .with_target(tgt_nk)
            .store_paths(true)
            .run();
        let cost = result.distance(tgt_nk)?;
        let path = result.path(tgt_nk)?.into_iter()
            .filter_map(|nk_id| self.nk_to_stable.get(&nk_id).copied())
            .collect();
        Some((cost, path))
    }

    /// A* shortest path with heuristic.
    ///
    /// Returns: Some((cost, Vec of stable node IDs)) or None if unreachable
    pub fn astar_path<F>(&self, source: u64, target: u64, heuristic: F) -> Option<(f64, Vec<u64>)>
    where
        F: Fn(u64) -> f64,
    {
        let src_nk = self.u64_to_nk(source)?;
        let tgt_nk = self.u64_to_nk(target)?;
        let h = |nk_id: NKNodeId| {
            let stable = self.nk_to_stable.get(&nk_id).copied().unwrap_or(u64::MAX);
            heuristic(stable)
        };
        astar(&self.graph, src_nk, tgt_nk, h).map(|(cost, path)| {
            let stable_path = path.into_iter()
                .filter_map(|nk_id| self.nk_to_stable.get(&nk_id).copied())
                .collect();
            (cost, stable_path)
        })
    }

    /// Bellman-Ford single-source distances (handles negative weights).
    ///
    /// Returns: Some(HashMap of stable node_id → distance) or None if negative cycle
    pub fn bellman_ford_distances(&self, source: u64) -> Option<HashMap<u64, f64>> {
        let src_nk = self.u64_to_nk(source)?;
        bellman_ford(&self.graph, src_nk).ok().map(|dists| {
            dists.into_iter()
                .filter_map(|(nk_id, dist)| {
                    let stable = self.nk_to_stable.get(&nk_id).copied()?;
                    Some((stable, dist))
                })
                .collect()
        })
    }

    /// Floyd-Warshall all-pairs shortest distances.
    ///
    /// Returns: HashMap mapping (src_stable, tgt_stable) → distance
    pub fn floyd_warshall_distances(&self) -> HashMap<(u64, u64), f64> {
        floyd_warshall(&self.graph)
            .into_iter()
            .filter_map(|((a, b), d)| {
                let sa = self.nk_to_stable.get(&a).copied()?;
                let sb = self.nk_to_stable.get(&b).copied()?;
                Some(((sa, sb), d))
            })
            .collect()
    }

    /// All-pairs shortest distances via Dijkstra from each node.
    ///
    /// Returns: HashMap mapping src_stable → {tgt_stable → distance}
    pub fn all_pairs_distances(&self) -> HashMap<u64, HashMap<u64, f64>> {
        all_pairs_dijkstra(&self.graph)
            .into_iter()
            .filter_map(|(src_nk, dists)| {
                let src_stable = self.nk_to_stable.get(&src_nk).copied()?;
                let stable_dists: HashMap<u64, f64> = dists
                    .into_iter()
                    .filter_map(|(tgt_nk, d)| {
                        let tgt_stable = self.nk_to_stable.get(&tgt_nk).copied()?;
                        Some((tgt_stable, d))
                    })
                    .collect();
                Some((src_stable, stable_dists))
            })
            .collect()
    }

    // ── Spanning Trees ────────────────────────────────────────────────────

    /// Minimum spanning tree (Kruskal).
    ///
    /// Returns: Vec of (src_stable, tgt_stable, weight) triples
    pub fn minimum_spanning_tree(&self) -> Vec<(u64, u64, f64)> {
        minimum_spanning_tree_kruskal(&self.graph)
            .into_iter()
            .filter_map(|(a, b, w)| {
                let sa = self.nk_to_stable.get(&a).copied()?;
                let sb = self.nk_to_stable.get(&b).copied()?;
                Some((sa, sb, w))
            })
            .collect()
    }

    /// Maximum spanning tree (Kruskal, inverted weights).
    ///
    /// Returns: Vec of (src_stable, tgt_stable, weight) triples
    pub fn maximum_spanning_tree(&self) -> Vec<(u64, u64, f64)> {
        maximum_spanning_tree_kruskal(&self.graph)
            .into_iter()
            .filter_map(|(a, b, w)| {
                let sa = self.nk_to_stable.get(&a).copied()?;
                let sb = self.nk_to_stable.get(&b).copied()?;
                Some((sa, sb, w))
            })
            .collect()
    }

    // ── DAG Algorithms ────────────────────────────────────────────────────

    /// Topological sort.
    ///
    /// Returns: Ok(Vec of stable node IDs) or Err(cycle nodes)
    pub fn topological_sort(&self) -> Result<Vec<u64>, Vec<u64>> {
        let nk_to_stable_fn = |nk_id: NKNodeId| {
            self.nk_to_stable.get(&nk_id).copied().unwrap_or(u64::MAX)
        };
        topological_sort(&self.graph)
            .map(|order| order.into_iter().map(&nk_to_stable_fn).collect())
            .map_err(|cycle| cycle.into_iter().map(&nk_to_stable_fn).collect())
    }

    /// Return true if the graph has no directed cycles.
    pub fn is_dag(&self) -> bool {
        is_dag(&self.graph)
    }

    /// Find all directed cycles.
    ///
    /// Returns: Vec of cycle node ID lists (each in stable u64)
    pub fn find_cycles(&self) -> Vec<Vec<u64>> {
        find_cycles(&self.graph)
            .into_iter()
            .map(|cycle| {
                cycle.into_iter()
                    .filter_map(|nk_id| self.nk_to_stable.get(&nk_id).copied())
                    .collect()
            })
            .collect()
    }

    /// Longest path in a DAG (unweighted — counts hops).
    ///
    /// Returns: Some((length, Vec of stable node IDs)) or None if cycles exist
    pub fn dag_longest_path(&self) -> Option<(usize, Vec<u64>)> {
        dag_longest_path(&self.graph).map(|(len, path)| {
            let stable_path = path.into_iter()
                .filter_map(|nk_id| self.nk_to_stable.get(&nk_id).copied())
                .collect();
            (len, stable_path)
        })
    }

    /// Longest weighted path in a DAG.
    ///
    /// Returns: Some((cost, Vec of stable node IDs)) or None if cycles exist
    pub fn dag_longest_path_weighted(&self) -> Option<(f64, Vec<u64>)> {
        dag_longest_path_weighted(&self.graph).map(|(cost, path)| {
            let stable_path = path.into_iter()
                .filter_map(|nk_id| self.nk_to_stable.get(&nk_id).copied())
                .collect();
            (cost, stable_path)
        })
    }

    /// Transitive closure — all (src, tgt) pairs where tgt is reachable from src.
    ///
    /// Returns: Vec of (src_stable, tgt_stable) pairs
    pub fn transitive_closure(&self) -> Vec<(u64, u64)> {
        transitive_closure(&self.graph)
            .into_iter()
            .filter_map(|(a, b)| {
                let sa = self.nk_to_stable.get(&a).copied()?;
                let sb = self.nk_to_stable.get(&b).copied()?;
                Some((sa, sb))
            })
            .collect()
    }

    // ── Network Flow ──────────────────────────────────────────────────────

    /// Edmonds-Karp maximum flow.
    ///
    /// Returns: Some((max_flow, edge_flows)) or None if source/sink not found
    pub fn max_flow(&self, source: u64, sink: u64) -> MaxFlowResult {
        let src_nk = self.u64_to_nk(source)?;
        let snk_nk = self.u64_to_nk(sink)?;
        let FlowResult { max_flow, edge_flows } = edmonds_karp(&self.graph, src_nk, snk_nk);
        let stable_flows = edge_flows
            .into_iter()
            .filter_map(|((a, b), f)| {
                let sa = self.nk_to_stable.get(&a).copied()?;
                let sb = self.nk_to_stable.get(&b).copied()?;
                Some(((sa, sb), f))
            })
            .collect();
        Some((max_flow, stable_flows))
    }

    /// Minimum cut capacity between source and sink.
    ///
    /// Returns: Some(capacity) or None if source/sink not found
    pub fn min_cut_capacity(&self, source: u64, sink: u64) -> Option<f64> {
        let src_nk = self.u64_to_nk(source)?;
        let snk_nk = self.u64_to_nk(sink)?;
        Some(min_cut(&self.graph, src_nk, snk_nk).min_cut)
    }

    // ── Graph Coloring ────────────────────────────────────────────────────

    /// Greedy vertex coloring.
    ///
    /// Returns: HashMap mapping stable node_id → color (0-indexed)
    pub fn node_coloring(&self) -> HashMap<u64, usize> {
        greedy_node_color(&self.graph)
            .into_iter()
            .filter_map(|(nk_id, color)| {
                let stable = self.nk_to_stable.get(&nk_id).copied()?;
                Some((stable, color))
            })
            .collect()
    }

    /// Greedy edge coloring.
    ///
    /// Returns: HashMap mapping (src_stable, tgt_stable) → color (0-indexed)
    pub fn edge_coloring(&self) -> HashMap<(u64, u64), usize> {
        greedy_edge_color(&self.graph)
            .into_iter()
            .filter_map(|((a, b), c)| {
                let sa = self.nk_to_stable.get(&a).copied()?;
                let sb = self.nk_to_stable.get(&b).copied()?;
                Some(((sa, sb), c))
            })
            .collect()
    }

    /// Approximate chromatic number.
    ///
    /// Returns: minimum number of colors needed
    pub fn chromatic_number(&self) -> usize {
        chromatic_number_approx(&self.graph)
    }

    // ── Matching ──────────────────────────────────────────────────────────

    /// Maximum weight matching (greedy).
    ///
    /// Returns: Vec of matched (stable_a, stable_b) pairs
    pub fn max_weight_matching_edges(&self) -> Vec<(u64, u64)> {
        max_weight_matching(&self.graph, false)
            .into_iter()
            .filter_map(|(a, b)| {
                let sa = self.nk_to_stable.get(&a).copied()?;
                let sb = self.nk_to_stable.get(&b).copied()?;
                Some((sa, sb))
            })
            .collect()
    }

    /// Maximum cardinality matching.
    ///
    /// Returns: Vec of matched (stable_a, stable_b) pairs
    pub fn max_cardinality_matching_edges(&self) -> Vec<(u64, u64)> {
        max_cardinality_matching(&self.graph)
            .into_iter()
            .filter_map(|(a, b)| {
                let sa = self.nk_to_stable.get(&a).copied()?;
                let sb = self.nk_to_stable.get(&b).copied()?;
                Some((sa, sb))
            })
            .collect()
    }

    // ── Community Detection ───────────────────────────────────────────────

    /// Helper: Convert CommunityResult to CommunityInfo with stable IDs.
    fn community_result_to_info(
        &self,
        result: crate::graph::backends::networkit_rust::algorithms::community::CommunityResult,
    ) -> CommunityInfo {
        let node_to_community = result
            .node_to_community
            .into_iter()
            .filter_map(|(nk_id, comm)| {
                let stable = self.nk_to_stable.get(&nk_id).copied()?;
                Some((stable, comm))
            })
            .collect();
        let communities = result
            .communities
            .into_iter()
            .map(|comp| {
                comp.into_iter()
                    .filter_map(|nk_id| self.nk_to_stable.get(&nk_id).copied())
                    .collect()
            })
            .collect();
        CommunityInfo {
            node_to_community,
            communities,
            modularity: result.modularity,
        }
    }

    /// Louvain community detection.
    ///
    /// Returns: CommunityInfo with stable node IDs
    pub fn louvain_communities(&self, resolution: Option<f64>) -> CommunityInfo {
        let result = louvain(&self.graph, resolution, 100);
        self.community_result_to_info(result)
    }

    /// Label propagation community detection.
    ///
    /// Returns: CommunityInfo with stable node IDs
    pub fn label_propagation_communities(&self) -> CommunityInfo {
        let result = label_propagation(&self.graph, 50);
        self.community_result_to_info(result)
    }

    /// Girvan-Newman community detection.
    ///
    /// Args:
    ///     k: Number of communities to produce
    ///
    /// Returns: CommunityInfo with stable node IDs
    pub fn girvan_newman_communities(&self, k: usize) -> CommunityInfo {
        let result = girvan_newman(&self.graph, k);
        self.community_result_to_info(result)
    }

    // ── Components ────────────────────────────────────────────────────────

    /// Find weakly connected components.
    ///
    /// Returns: Vec of components, each component is a Vec of stable node IDs
    pub fn connected_components_list(&self) -> Vec<Vec<u64>> {
        let result = ConnectedComponents::run(&self.graph);
        let mut map: HashMap<usize, Vec<u64>> = HashMap::new();
        for nk_id in self.graph.nodes() {
            if let Some(Some(comp_id)) = result.components.get(nk_id as usize) {
                if let Some(stable) = self.nk_to_stable.get(&nk_id).copied() {
                    map.entry(*comp_id).or_default().push(stable);
                }
            }
        }
        map.into_values().collect()
    }

    /// Find strongly connected components (Tarjan's algorithm).
    ///
    /// Returns: Vec of SCCs, each SCC is a Vec of stable node IDs
    pub fn strongly_connected_components_list(&self) -> Vec<Vec<u64>> {
        let result = StronglyConnectedComponents::run(&self.graph);
        let mut map: HashMap<usize, Vec<u64>> = HashMap::new();
        for nk_id in self.graph.nodes() {
            if let Some(Some(comp_id)) = result.components.get(nk_id as usize) {
                if let Some(stable) = self.nk_to_stable.get(&nk_id).copied() {
                    map.entry(*comp_id).or_default().push(stable);
                }
            }
        }
        map.into_values().collect()
    }

    // ── Eulerian ──────────────────────────────────────────────────────────

    /// Returns true if the graph has an Euler circuit.
    pub fn has_euler_circuit(&self) -> bool {
        has_euler_circuit(&self.graph)
    }

    /// Returns true if the graph has an Euler path.
    pub fn has_euler_path(&self) -> bool {
        has_euler_path(&self.graph)
    }

    /// Find an Euler circuit (sequence of stable node IDs, first == last).
    ///
    /// Returns None if no Euler circuit exists.
    pub fn euler_circuit(&self) -> Option<Vec<u64>> {
        euler_circuit(&self.graph).map(|circuit| {
            circuit.into_iter()
                .filter_map(|nk_id| self.nk_to_stable.get(&nk_id).copied())
                .collect()
        })
    }

    /// Find an Euler path (sequence of stable node IDs).
    ///
    /// Returns None if no Euler path exists.
    pub fn euler_path(&self) -> Option<Vec<u64>> {
        euler_path(&self.graph).map(|path| {
            path.into_iter()
                .filter_map(|nk_id| self.nk_to_stable.get(&nk_id).copied())
                .collect()
        })
    }

    // ── Planarity ─────────────────────────────────────────────────────────

    /// Returns true if the graph is planar.
    pub fn is_planar(&self) -> bool {
        is_planar(&self.graph)
    }

    // ── Unified transitive_closure (HashSet variant) ───────────────────────

    /// Transitive closure as a HashSet (unified with AlgorithmEngine interface).
    pub fn transitive_closure_set(&self) -> HashSet<(u64, u64)> {
        self.transitive_closure().into_iter().collect()
    }

    // ── Matching (HashSet variants) ────────────────────────────────────────

    /// Maximum weight matching edges as a HashSet.
    pub fn max_weight_matching_set(&self) -> HashSet<(u64, u64)> {
        self.max_weight_matching_edges().into_iter().collect()
    }

    /// Maximum cardinality matching edges as a HashSet.
    pub fn max_cardinality_matching_set(&self) -> HashSet<(u64, u64)> {
        self.max_cardinality_matching_edges().into_iter().collect()
    }
}
