//! Graph matching algorithms.
//!
//! Ported from classical algorithm sources, adapted to NetworKit's Graph structure.
//!
//! Algorithms:
//! - max_weight_matching: Maximum weight matching (greedy approximation)
//! - max_cardinality_matching: Maximum cardinality matching
//! - is_matching: Verify a matching is valid
//! - is_perfect_matching: Verify a perfect matching

use super::super::graph::{Graph, NodeId};
use std::collections::{HashMap, HashSet, BinaryHeap};
use std::cmp::Ordering;

// ─────────────────────────────────────────────────────────────────────────────
// Matching Types

/// A matching: set of non-adjacent edges.
pub type Matching = HashSet<(NodeId, NodeId)>;

// ─────────────────────────────────────────────────────────────────────────────
// Max Weight Matching

#[derive(Clone)]
struct EdgeWeight {
    weight: f64,
    u: NodeId,
    v: NodeId,
}

impl PartialEq for EdgeWeight {
    fn eq(&self, other: &Self) -> bool {
        self.weight == other.weight
    }
}

impl Eq for EdgeWeight {}

impl PartialOrd for EdgeWeight {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for EdgeWeight {
    fn cmp(&self, other: &Self) -> Ordering {
        self.weight.partial_cmp(&other.weight).unwrap_or(Ordering::Equal)
    }
}

/// Maximum weight matching using a greedy approach.
///
/// Greedily selects edges in decreasing weight order, adding each edge to the
/// matching if neither endpoint is already matched.
///
/// This is a 1/2-approximation of the optimal maximum weight matching.
/// For an exact solution, use the Blossom V algorithm (not implemented here).
///
/// Ported from RustworkxCore:
/// <https://github.com/Qiskit/rustworkx/blob/main/rustworkx-core/src/max_weight_matching.rs>
///
/// # Arguments
/// * `graph` - The graph
/// * `max_cardinality` - If true, maximize the number of edges first (then weight)
///
/// # Returns
/// A `Matching` (set of `(source, target)` pairs).
///
/// # Example
/// ```ignore
/// let matching = max_weight_matching(&graph, false);
/// let total_weight: f64 = matching.iter()
///     .filter_map(|&(u, v)| graph.out_neighbors(u).iter().find(|n| n.target == v))
///     .map(|n| n.weight.unwrap_or(1.0))
///     .sum();
/// ```
pub fn max_weight_matching(graph: &Graph, max_cardinality: bool) -> Matching {
    // Sort edges by weight (descending)
    let mut edges: BinaryHeap<EdgeWeight> = graph.edges()
        .map(|(u, v, w, _)| EdgeWeight { weight: w.unwrap_or(1.0), u, v })
        .collect();

    let mut matched: HashSet<NodeId> = HashSet::new();
    let mut matching: Matching = HashSet::new();

    // If max_cardinality, use a different strategy: augmenting paths
    if max_cardinality {
        return max_cardinality_matching(graph);
    }

    // Greedy: pick highest weight edge where neither endpoint is matched
    while let Some(EdgeWeight { weight: _, u, v }) = edges.pop() {
        if !matched.contains(&u) && !matched.contains(&v) {
            matched.insert(u);
            matched.insert(v);
            // Normalize edge direction for consistent output
            let edge = if u < v { (u, v) } else { (v, u) };
            matching.insert(edge);
        }
    }

    matching
}

// ─────────────────────────────────────────────────────────────────────────────
// Max Cardinality Matching (Augmenting Paths)

/// Maximum cardinality matching using augmenting paths.
///
/// Finds the largest possible matching (most edges).
/// Uses the augmenting path approach from classical matching theory.
///
/// Time complexity: O(V * E)
///
/// # Returns
/// A `Matching` with the maximum number of edges.
///
/// # Example
/// ```ignore
/// let matching = max_cardinality_matching(&graph);
/// println!("Matched {} pairs of nodes", matching.len());
/// ```
pub fn max_cardinality_matching(graph: &Graph) -> Matching {
    let mut match_map: HashMap<NodeId, NodeId> = HashMap::new();

    for start in graph.nodes() {
        if !match_map.contains_key(&start) {
            // Try to find augmenting path from `start`
            let mut visited: HashSet<NodeId> = HashSet::new();
            augment(graph, start, &mut match_map, &mut visited);
        }
    }

    // Convert match_map to Matching (each edge appears once)
    let mut matching: Matching = HashSet::new();
    for (&u, &v) in &match_map {
        let edge = if u < v { (u, v) } else { (v, u) };
        matching.insert(edge);
    }

    matching
}

/// Attempt to find an augmenting path from `node` using DFS.
/// Returns true if augmenting path was found.
fn augment(
    graph: &Graph,
    node: NodeId,
    match_map: &mut HashMap<NodeId, NodeId>,
    visited: &mut HashSet<NodeId>,
) -> bool {
    for neighbor in graph.out_neighbors(node) {
        let v = neighbor.target;

        if visited.contains(&v) {
            continue;
        }
        visited.insert(v);

        // If v is unmatched or we can re-augment from v's current match
        if !match_map.contains_key(&v) {
            match_map.insert(node, v);
            match_map.insert(v, node);
            return true;
        }

        let current_match = match_map[&v];
        if augment(graph, current_match, match_map, visited) {
            match_map.insert(node, v);
            match_map.insert(v, node);
            return true;
        }
    }

    false
}

// ─────────────────────────────────────────────────────────────────────────────
// Matching Validation

/// Check if a matching is valid (no two edges share an endpoint).
///
/// # Example
/// ```ignore
/// let matching = max_weight_matching(&graph, false);
/// assert!(is_matching(&graph, &matching));
/// ```
pub fn is_matching(graph: &Graph, matching: &Matching) -> bool {
    // Check each matched edge exists in graph and endpoints appear at most once
    let mut endpoints: HashSet<NodeId> = HashSet::new();

    for &(u, v) in matching {
        if !graph.has_edge(u, v) && !graph.has_edge(v, u) {
            return false; // Edge doesn't exist
        }

        if !endpoints.insert(u) || !endpoints.insert(v) {
            return false; // Endpoint appears twice
        }
    }

    true
}

/// Check if a matching is a perfect matching (every node is matched).
///
/// # Example
/// ```ignore
/// if is_perfect_matching(&graph, &matching) {
///     println!("Perfect matching found!");
/// }
/// ```
pub fn is_perfect_matching(graph: &Graph, matching: &Matching) -> bool {
    if !is_matching(graph, matching) {
        return false;
    }

    let matched_nodes: HashSet<NodeId> = matching.iter()
        .flat_map(|&(u, v)| [u, v])
        .collect();

    // Every node must be matched
    graph.nodes().all(|n| matched_nodes.contains(&n))
}

/// Check if a matching is maximal (no more edges can be added).
///
/// A matching is maximal if no unmatched edge has both endpoints unmatched.
pub fn is_maximal_matching(graph: &Graph, matching: &Matching) -> bool {
    if !is_matching(graph, matching) {
        return false;
    }

    let matched_nodes: HashSet<NodeId> = matching.iter()
        .flat_map(|&(u, v)| [u, v])
        .collect();

    // Check that every unmatched edge has at least one matched endpoint
    for (u, v, _, _) in graph.edges() {
        let edge = if u < v { (u, v) } else { (v, u) };
        if !matching.contains(&edge) && !matched_nodes.contains(&u) && !matched_nodes.contains(&v) {
            return false; // This edge could be added to the matching
        }
    }

    true
}

// ─────────────────────────────────────────────────────────────────────────────
// Tests

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

    fn make_path_graph(n: usize) -> (Graph, Vec<NodeId>) {
        let mut g = Graph::new(GraphConfig::simple());
        let nodes: Vec<NodeId> = (0..n).map(|_| g.add_node()).collect();
        for i in 0..(n - 1) {
            g.add_edge(nodes[i], nodes[i + 1], None);
        }
        (g, nodes)
    }

    fn make_weighted_graph() -> (Graph, Vec<NodeId>) {
        let mut g = Graph::new(GraphConfig::simple());
        let nodes: Vec<NodeId> = (0..4).map(|_| g.add_node()).collect();
        g.add_edge(nodes[0], nodes[1], Some(10.0)); // High weight
        g.add_edge(nodes[1], nodes[2], Some(1.0));  // Low weight
        g.add_edge(nodes[2], nodes[3], Some(5.0));  // Medium weight
        g.add_edge(nodes[0], nodes[3], Some(3.0));  // Low weight
        (g, nodes)
    }

    #[test]
    fn test_max_weight_matching_valid() {
        let (g, _) = make_weighted_graph();
        let matching = max_weight_matching(&g, false);
        assert!(is_matching(&g, &matching));
    }

    #[test]
    fn test_max_cardinality_path_graph() {
        // Path of 4 nodes: max matching has 2 edges
        let (g, _) = make_path_graph(4);
        let matching = max_cardinality_matching(&g);
        assert_eq!(matching.len(), 2);
        assert!(is_matching(&g, &matching));
    }

    #[test]
    fn test_max_cardinality_odd_path() {
        // Path of 5 nodes: max matching has 2 edges
        let (g, _) = make_path_graph(5);
        let matching = max_cardinality_matching(&g);
        assert_eq!(matching.len(), 2);
    }

    #[test]
    fn test_is_maximal_matching() {
        let (g, nodes) = make_path_graph(4);
        // Matching {(0,1), (2,3)} is perfect for a path of 4
        let mut m: Matching = HashSet::new();
        let e1 = if nodes[0] < nodes[1] { (nodes[0], nodes[1]) } else { (nodes[1], nodes[0]) };
        let e2 = if nodes[2] < nodes[3] { (nodes[2], nodes[3]) } else { (nodes[3], nodes[2]) };
        m.insert(e1);
        m.insert(e2);

        assert!(is_matching(&g, &m));
        assert!(is_perfect_matching(&g, &m));
        assert!(is_maximal_matching(&g, &m));
    }

    #[test]
    fn test_empty_graph() {
        let g = Graph::new(GraphConfig::simple());
        let matching = max_weight_matching(&g, false);
        assert!(matching.is_empty());
    }

    #[test]
    fn test_single_edge() {
        let mut g = Graph::new(GraphConfig::simple());
        let a = g.add_node();
        let b = g.add_node();
        g.add_edge(a, b, Some(5.0));

        let matching = max_weight_matching(&g, false);
        assert_eq!(matching.len(), 1);
    }
}
