//! Path finding algorithms for graphs.

use super::edge::Edge;
use super::node::NodeId;
use super::store::{Direction, GraphStore};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};

/// Result of a path finding operation.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PathResult {
    /// The path as a list of node IDs.
    pub nodes: Vec<NodeId>,
    /// The edges traversed.
    pub edges: Vec<Edge>,
    /// Total path length (number of hops or weighted distance).
    pub length: f64,
    /// Whether a path was found.
    pub found: bool,
}

impl PathResult {
    /// Creates an empty (not found) result.
    pub fn not_found() -> Self {
        Self {
            nodes: Vec::new(),
            edges: Vec::new(),
            length: f64::INFINITY,
            found: false,
        }
    }

    /// Creates a found result.
    pub fn found(nodes: Vec<NodeId>, edges: Vec<Edge>, length: f64) -> Self {
        Self {
            nodes,
            edges,
            length,
            found: true,
        }
    }
}

/// State for Dijkstra's algorithm priority queue.
#[derive(Clone)]
struct DijkstraState {
    node: NodeId,
    distance: f64,
}

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

impl Eq for DijkstraState {}

impl Ord for DijkstraState {
    fn cmp(&self, other: &Self) -> Ordering {
        // Reverse ordering for min-heap
        other.distance.partial_cmp(&self.distance).unwrap_or(Ordering::Equal)
    }
}

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

/// Path finder for graph shortest path queries.
pub struct PathFinder<'a> {
    store: &'a GraphStore,
}

impl<'a> PathFinder<'a> {
    /// Creates a new path finder.
    pub fn new(store: &'a GraphStore) -> Self {
        Self { store }
    }

    /// Finds the shortest path using BFS (unweighted).
    pub fn shortest_path_bfs(&self, from: &NodeId, to: &NodeId) -> PathResult {
        if from == to {
            return PathResult::found(vec![*from], Vec::new(), 0.0);
        }

        let mut visited = HashSet::new();
        let mut queue: VecDeque<(NodeId, Vec<NodeId>, Vec<Edge>)> = VecDeque::new();

        visited.insert(*from);
        queue.push_back((*from, vec![*from], Vec::new()));

        while let Some((current, path, edges)) = queue.pop_front() {
            let neighbor_edges = self.store.edges_of(&current, Direction::Both);

            for edge in neighbor_edges {
                let neighbor = if edge.source == current {
                    edge.target
                } else if !edge.directed || edge.target == current {
                    edge.source
                } else {
                    continue;
                };

                if &neighbor == to {
                    let mut final_path = path.clone();
                    final_path.push(neighbor);
                    let path_len = final_path.len();
                    let mut final_edges = edges.clone();
                    final_edges.push(edge);
                    return PathResult::found(final_path, final_edges, path_len as f64 - 1.0);
                }

                if !visited.contains(&neighbor) {
                    visited.insert(neighbor);
                    let mut new_path = path.clone();
                    new_path.push(neighbor);
                    let mut new_edges = edges.clone();
                    new_edges.push(edge);
                    queue.push_back((neighbor, new_path, new_edges));
                }
            }
        }

        PathResult::not_found()
    }

    /// Finds the shortest path using Dijkstra's algorithm (weighted).
    pub fn shortest_path_dijkstra(&self, from: &NodeId, to: &NodeId) -> PathResult {
        if from == to {
            return PathResult::found(vec![*from], Vec::new(), 0.0);
        }

        let mut distances: HashMap<NodeId, f64> = HashMap::new();
        let mut previous: HashMap<NodeId, (NodeId, Edge)> = HashMap::new();
        let mut heap = BinaryHeap::new();
        let mut visited = HashSet::new();

        distances.insert(*from, 0.0);
        heap.push(DijkstraState { node: *from, distance: 0.0 });

        while let Some(DijkstraState { node: current, distance: dist }) = heap.pop() {
            if &current == to {
                // Reconstruct path
                let mut path = vec![current];
                let mut edges = Vec::new();
                let mut curr = current;

                while let Some((prev, edge)) = previous.get(&curr) {
                    path.push(*prev);
                    edges.push(edge.clone());
                    curr = *prev;
                }

                path.reverse();
                edges.reverse();
                return PathResult::found(path, edges, dist);
            }

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

            let neighbor_edges = self.store.edges_of(&current, Direction::Both);

            for edge in neighbor_edges {
                let neighbor = if edge.source == current {
                    edge.target
                } else if !edge.directed || edge.target == current {
                    edge.source
                } else {
                    continue;
                };

                if visited.contains(&neighbor) {
                    continue;
                }

                let new_dist = dist + edge.weight;
                let is_shorter = distances.get(&neighbor).map(|&d| new_dist < d).unwrap_or(true);

                if is_shorter {
                    distances.insert(neighbor, new_dist);
                    previous.insert(neighbor, (current, edge.clone()));
                    heap.push(DijkstraState { node: neighbor, distance: new_dist });
                }
            }
        }

        PathResult::not_found()
    }

    /// Finds all paths between two nodes up to a maximum length.
    pub fn all_paths(&self, from: &NodeId, to: &NodeId, max_length: usize) -> Vec<PathResult> {
        let mut results = Vec::new();
        let mut current_path = vec![*from];
        let mut current_edges = Vec::new();
        let mut visited = HashSet::new();
        visited.insert(*from);

        self.find_all_paths_dfs(
            from,
            to,
            max_length,
            &mut current_path,
            &mut current_edges,
            &mut visited,
            &mut results,
        );

        results
    }

    fn find_all_paths_dfs(
        &self,
        current: &NodeId,
        target: &NodeId,
        max_length: usize,
        path: &mut Vec<NodeId>,
        edges: &mut Vec<Edge>,
        visited: &mut HashSet<NodeId>,
        results: &mut Vec<PathResult>,
    ) {
        if current == target {
            let total_weight: f64 = edges.iter().map(|e| e.weight).sum();
            results.push(PathResult::found(path.clone(), edges.clone(), total_weight));
            return;
        }

        if path.len() > max_length {
            return;
        }

        let neighbor_edges = self.store.edges_of(current, Direction::Both);

        for edge in neighbor_edges {
            let neighbor = if edge.source == *current {
                edge.target
            } else if !edge.directed || edge.target == *current {
                edge.source
            } else {
                continue;
            };

            if !visited.contains(&neighbor) {
                visited.insert(neighbor);
                path.push(neighbor);
                edges.push(edge.clone());

                self.find_all_paths_dfs(&neighbor, target, max_length, path, edges, visited, results);

                path.pop();
                edges.pop();
                visited.remove(&neighbor);
            }
        }
    }

    /// Finds the shortest path considering only specific edge types.
    pub fn shortest_path_by_type(&self, from: &NodeId, to: &NodeId, edge_types: &[String]) -> PathResult {
        if from == to {
            return PathResult::found(vec![*from], Vec::new(), 0.0);
        }

        let mut visited = HashSet::new();
        let mut queue: VecDeque<(NodeId, Vec<NodeId>, Vec<Edge>)> = VecDeque::new();

        visited.insert(*from);
        queue.push_back((*from, vec![*from], Vec::new()));

        while let Some((current, path, edges)) = queue.pop_front() {
            let neighbor_edges = self.store.edges_of(&current, Direction::Both);

            for edge in neighbor_edges {
                // Filter by edge type
                if !edge_types.is_empty() && !edge_types.contains(&edge.edge_type) {
                    continue;
                }

                let neighbor = if edge.source == current {
                    edge.target
                } else if !edge.directed || edge.target == current {
                    edge.source
                } else {
                    continue;
                };

                if &neighbor == to {
                    let mut final_path = path.clone();
                    final_path.push(neighbor);
                    let path_len = final_path.len();
                    let mut final_edges = edges.clone();
                    final_edges.push(edge);
                    return PathResult::found(final_path, final_edges, path_len as f64 - 1.0);
                }

                if !visited.contains(&neighbor) {
                    visited.insert(neighbor);
                    let mut new_path = path.clone();
                    new_path.push(neighbor);
                    let mut new_edges = edges.clone();
                    new_edges.push(edge);
                    queue.push_back((neighbor, new_path, new_edges));
                }
            }
        }

        PathResult::not_found()
    }

    /// Checks if a path exists between two nodes.
    pub fn path_exists(&self, from: &NodeId, to: &NodeId) -> bool {
        if from == to {
            return true;
        }

        let mut visited = HashSet::new();
        let mut queue = VecDeque::new();

        visited.insert(*from);
        queue.push_back(*from);

        while let Some(current) = queue.pop_front() {
            let neighbor_edges = self.store.edges_of(&current, Direction::Both);

            for edge in neighbor_edges {
                let neighbor = if edge.source == current {
                    edge.target
                } else if !edge.directed || edge.target == current {
                    edge.source
                } else {
                    continue;
                };

                if &neighbor == to {
                    return true;
                }

                if !visited.contains(&neighbor) {
                    visited.insert(neighbor);
                    queue.push_back(neighbor);
                }
            }
        }

        false
    }

    /// Finds the distance (number of hops) between two nodes.
    pub fn distance(&self, from: &NodeId, to: &NodeId) -> Option<usize> {
        let result = self.shortest_path_bfs(from, to);
        if result.found {
            Some(result.nodes.len() - 1)
        } else {
            None
        }
    }
}

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

    fn setup_graph() -> GraphStore {
        let store = GraphStore::new();

        // Create nodes A -> B -> C -> D
        //              \         /
        //               \-> E ->/
        let a = store.create_node(vec![], serde_json::json!({"name": "A"}));
        let b = store.create_node(vec![], serde_json::json!({"name": "B"}));
        let c = store.create_node(vec![], serde_json::json!({"name": "C"}));
        let d = store.create_node(vec![], serde_json::json!({"name": "D"}));
        let e = store.create_node(vec![], serde_json::json!({"name": "E"}));

        store.create_edge(a, b, "LINK", serde_json::json!({})).unwrap();
        store.create_edge(b, c, "LINK", serde_json::json!({})).unwrap();
        store.create_edge(c, d, "LINK", serde_json::json!({})).unwrap();
        store.create_edge(a, e, "LINK", serde_json::json!({})).unwrap();
        store.create_edge(e, d, "LINK", serde_json::json!({})).unwrap();

        store
    }

    #[test]
    fn test_shortest_path_bfs() {
        let store = setup_graph();
        let finder = PathFinder::new(&store);

        let nodes: Vec<_> = store.find_nodes(None, &serde_json::json!({}));
        let a = nodes.iter().find(|n| n.get_property("name") == Some(&serde_json::json!("A"))).unwrap();
        let d = nodes.iter().find(|n| n.get_property("name") == Some(&serde_json::json!("D"))).unwrap();

        let result = finder.shortest_path_bfs(&a.id, &d.id);

        assert!(result.found);
        assert_eq!(result.length, 2.0); // A -> E -> D (shortest)
    }

    #[test]
    fn test_shortest_path_dijkstra() {
        let store = GraphStore::new();

        let a = store.create_node(vec![], serde_json::json!({"name": "A"}));
        let b = store.create_node(vec![], serde_json::json!({"name": "B"}));
        let c = store.create_node(vec![], serde_json::json!({"name": "C"}));

        // A --(1.0)--> B --(1.0)--> C
        // A --(3.0)--> C
        let mut edge1 = super::super::edge::Edge::new(a, b, "LINK", serde_json::json!({}));
        edge1.weight = 1.0;
        store.create_edge(a, b, "LINK", serde_json::json!({})).unwrap();

        let mut edge2 = super::super::edge::Edge::new(b, c, "LINK", serde_json::json!({}));
        edge2.weight = 1.0;
        store.create_edge(b, c, "LINK", serde_json::json!({})).unwrap();

        store.create_edge(a, c, "DIRECT", serde_json::json!({})).unwrap();

        let finder = PathFinder::new(&store);
        let result = finder.shortest_path_dijkstra(&a, &c);

        assert!(result.found);
        // Both paths have same weight (1.0 each), either is valid
        assert!(result.nodes.len() <= 3);
    }

    #[test]
    fn test_path_not_found() {
        let store = GraphStore::new();

        let a = store.create_node(vec![], serde_json::json!({}));
        let b = store.create_node(vec![], serde_json::json!({}));
        // No edge between a and b

        let finder = PathFinder::new(&store);
        let result = finder.shortest_path_bfs(&a, &b);

        assert!(!result.found);
    }

    #[test]
    fn test_all_paths() {
        let store = setup_graph();
        let finder = PathFinder::new(&store);

        let nodes: Vec<_> = store.find_nodes(None, &serde_json::json!({}));
        let a = nodes.iter().find(|n| n.get_property("name") == Some(&serde_json::json!("A"))).unwrap();
        let d = nodes.iter().find(|n| n.get_property("name") == Some(&serde_json::json!("D"))).unwrap();

        let paths = finder.all_paths(&a.id, &d.id, 5);

        assert!(paths.len() >= 2); // At least A->B->C->D and A->E->D
    }

    #[test]
    fn test_path_exists() {
        let store = setup_graph();
        let finder = PathFinder::new(&store);

        let nodes: Vec<_> = store.find_nodes(None, &serde_json::json!({}));
        let a = nodes.iter().find(|n| n.get_property("name") == Some(&serde_json::json!("A"))).unwrap();
        let d = nodes.iter().find(|n| n.get_property("name") == Some(&serde_json::json!("D"))).unwrap();

        assert!(finder.path_exists(&a.id, &d.id));
    }

    #[test]
    fn test_distance() {
        let store = setup_graph();
        let finder = PathFinder::new(&store);

        let nodes: Vec<_> = store.find_nodes(None, &serde_json::json!({}));
        let a = nodes.iter().find(|n| n.get_property("name") == Some(&serde_json::json!("A"))).unwrap();
        let d = nodes.iter().find(|n| n.get_property("name") == Some(&serde_json::json!("D"))).unwrap();
        let b = nodes.iter().find(|n| n.get_property("name") == Some(&serde_json::json!("B"))).unwrap();

        assert_eq!(finder.distance(&a.id, &b.id), Some(1));
        assert_eq!(finder.distance(&a.id, &d.id), Some(2)); // A -> E -> D
    }
}
