"""
autobee.graph — BFS, DFS, and Dijkstra's shortest path.
Graphs are represented as adjacency dicts.

Unweighted:  {"A": ["B", "C"], "B": ["D"], ...}
Weighted:    {"A": [("B", 1), ("C", 4)], ...}
"""

from collections import deque
import heapq


def bfs(graph: dict, start) -> dict:
    """
    Breadth-First Search — explores nodes level by level.

    Args:
        graph: Adjacency dict {node: [neighbors]}.
        start: Starting node.

    Returns:
        dict with keys: result (traversal order), steps, algo.

    Example:
        >>> from autobee import bfs
        >>> g = {"A": ["B", "C"], "B": ["D"], "C": [], "D": []}
        >>> bfs(g, "A")["result"]
        ['A', 'B', 'C', 'D']
    """
    visited = set()
    order = []
    steps = []
    queue = deque([start])
    visited.add(start)

    while queue:
        node = queue.popleft()
        order.append(node)
        steps.append({
            "node": node,
            "visited": list(visited),
            "queue": list(queue),
        })
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

    return {
        "result": order,
        "steps": steps,
        "algo": "BFS",
        "complexity": {"time": "O(V + E)", "space": "O(V)"},
    }


def dfs(graph: dict, start) -> dict:
    """
    Depth-First Search — explores as far as possible before backtracking.

    Args:
        graph: Adjacency dict {node: [neighbors]}.
        start: Starting node.

    Returns:
        dict with keys: result (traversal order), steps, algo.

    Example:
        >>> from autobee import dfs
        >>> g = {"A": ["B", "C"], "B": ["D"], "C": [], "D": []}
        >>> dfs(g, "A")["result"]
        ['A', 'B', 'D', 'C']
    """
    visited = set()
    order = []
    steps = []

    def _dfs(node):
        visited.add(node)
        order.append(node)
        steps.append({"node": node, "visited": list(visited)})
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                _dfs(neighbor)

    _dfs(start)
    return {
        "result": order,
        "steps": steps,
        "algo": "DFS",
        "complexity": {"time": "O(V + E)", "space": "O(V)"},
    }


def dijkstra(graph: dict, start) -> dict:
    """
    Dijkstra's Algorithm — finds shortest paths from start to all nodes.

    Args:
        graph: Weighted adjacency dict {node: [(neighbor, weight), ...]}.
        start: Starting node.

    Returns:
        dict with keys: result (distances dict), steps, algo.

    Example:
        >>> from autobee import dijkstra
        >>> g = {"A": [("B", 1), ("C", 4)], "B": [("C", 2), ("D", 5)], "C": [("D", 1)], "D": []}
        >>> dijkstra(g, "A")["result"]
        {'A': 0, 'B': 1, 'C': 3, 'D': 4}
    """
    dist = {node: float("inf") for node in graph}
    dist[start] = 0
    pq = [(0, start)]
    steps = []

    while pq:
        d, u = heapq.heappop(pq)
        steps.append({"node": u, "distance": d, "distances": dict(dist)})
        if d > dist[u]:
            continue
        for v, w in graph.get(u, []):
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(pq, (dist[v], v))

    return {
        "result": dist,
        "steps": steps,
        "algo": "Dijkstra",
        "complexity": {"time": "O((V + E) log V)", "space": "O(V)"},
    }
