"""
maze_solver/strategies.py - паттерн Strategy.

Каждая стратегия реализует один и тот же интерфейс PathFindingStrategy
с методом find_path(maze, start, exit_), возвращающим:
    {'path': [Cell, ...], 'visited': int}

Стратегии не модифицируют сам лабиринт.
"""

from abc import ABC, abstractmethod
from collections import deque
import heapq


# ---------- интерфейс стратегии ----------

class PathFindingStrategy(ABC):
    name = "Strategy"

    @abstractmethod
    def find_path(self, maze, start, exit_):
        """Возвращает dict с ключами 'path' (list[Cell]) и 'visited' (int).
        Если пути нет - path = []."""


# ---------- общая утилита: восстановление пути ----------

def _reconstruct(parents, start, end):
    """Восстанавливает путь по словарю предшественников {(x,y): Cell|None}."""
    path = []
    cur = end
    while cur is not None:
        path.append(cur)
        cur = parents.get((cur.x, cur.y))
    path.reverse()
    if path and path[0] is start:
        return path
    return []


# ---------- BFS ----------

class BFSStrategy(PathFindingStrategy):
    """Поиск в ширину. Гарантирует кратчайший путь по числу шагов
    (когда веса всех клеток равны)."""
    name = "BFS"

    def find_path(self, maze, start, exit_):
        queue = deque([start])
        parents = {(start.x, start.y): None}
        visited = 1

        while queue:
            cell = queue.popleft()
            if cell is exit_:
                return {"path": _reconstruct(parents, start, exit_),
                        "visited": visited}
            for nb in maze.get_neighbors(cell):
                key = (nb.x, nb.y)
                if key not in parents:
                    parents[key] = cell
                    visited += 1
                    queue.append(nb)
        return {"path": [], "visited": visited}


# ---------- DFS ----------

class DFSStrategy(PathFindingStrategy):
    """Поиск в глубину. Не гарантирует кратчайший путь, но прост и быстр."""
    name = "DFS"

    def find_path(self, maze, start, exit_):
        stack = [start]
        parents = {(start.x, start.y): None}
        visited = 1

        while stack:
            cell = stack.pop()
            if cell is exit_:
                return {"path": _reconstruct(parents, start, exit_),
                        "visited": visited}
            for nb in maze.get_neighbors(cell):
                key = (nb.x, nb.y)
                if key not in parents:
                    parents[key] = cell
                    visited += 1
                    stack.append(nb)
        return {"path": [], "visited": visited}


# ---------- A* ----------

def _manhattan(a, b):
    return abs(a.x - b.x) + abs(a.y - b.y)


class AStarStrategy(PathFindingStrategy):
    """A*-поиск с манхэттенской эвристикой. Учитывает вес клеток (weight)."""
    name = "A*"

    def find_path(self, maze, start, exit_):
        # f = g + h; в куче храним (f, tie, cell)
        g_score = {(start.x, start.y): 0}
        parents = {(start.x, start.y): None}
        tie = 0
        heap = [(_manhattan(start, exit_), tie, start)]
        visited = 0
        closed = set()

        while heap:
            f, _, cell = heapq.heappop(heap)
            key = (cell.x, cell.y)
            if key in closed:
                continue
            closed.add(key)
            visited += 1

            if cell is exit_:
                return {"path": _reconstruct(parents, start, exit_),
                        "visited": visited}

            for nb in maze.get_neighbors(cell):
                nb_key = (nb.x, nb.y)
                tentative_g = g_score[key] + nb.weight
                if tentative_g < g_score.get(nb_key, float("inf")):
                    g_score[nb_key] = tentative_g
                    parents[nb_key] = cell
                    tie += 1
                    heapq.heappush(heap,
                                   (tentative_g + _manhattan(nb, exit_), tie, nb))
        return {"path": [], "visited": visited}


# ---------- Дейкстра ----------

class DijkstraStrategy(PathFindingStrategy):
    """Дейкстра - оптимальный путь с учётом веса клеток.
    На немодифицированном лабиринте (все веса = 1) совпадает с BFS."""
    name = "Dijkstra"

    def find_path(self, maze, start, exit_):
        dist = {(start.x, start.y): 0}
        parents = {(start.x, start.y): None}
        tie = 0
        heap = [(0, tie, start)]
        visited = 0
        closed = set()

        while heap:
            d, _, cell = heapq.heappop(heap)
            key = (cell.x, cell.y)
            if key in closed:
                continue
            closed.add(key)
            visited += 1

            if cell is exit_:
                return {"path": _reconstruct(parents, start, exit_),
                        "visited": visited}

            for nb in maze.get_neighbors(cell):
                nb_key = (nb.x, nb.y)
                nd = d + nb.weight
                if nd < dist.get(nb_key, float("inf")):
                    dist[nb_key] = nd
                    parents[nb_key] = cell
                    tie += 1
                    heapq.heappush(heap, (nd, tie, nb))
        return {"path": [], "visited": visited}


STRATEGIES = {
    "BFS": BFSStrategy,
    "DFS": DFSStrategy,
    "A*":  AStarStrategy,
    "Dijkstra": DijkstraStrategy,
}
