from abc import ABC, abstractmethod
from typing import List, Tuple
from collections import deque
import heapq
from maze import Maze, Cell


class PathFindingStrategy(ABC):
    """Интерфейс стратегии поиска пути (Strategy pattern)."""
    
    @abstractmethod
    def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]:
        """
        Возвращает:
            - путь (список клеток от старта до выхода)
            - количество посещённых клеток
        Если пути нет — возвращает ([], 0).
        """
        pass
    
    @abstractmethod
    def get_name(self) -> str:
        """Название алгоритма для отчёта."""
        pass


class BFSStrategy(PathFindingStrategy):
    """BFS — поиск в ширину. Гарантирует кратчайший путь по числу шагов."""
    
    def get_name(self) -> str:
        return "BFS"
    
    def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]:
        if not start or not exit:
            return [], 0
        
        # Очередь: (текущая_клетка, путь_до_неё)
        queue = deque([(start, [start])])
        visited = {start}
        visited_count = 1
        
        while queue:
            current, path = queue.popleft()
            
            if current == exit:
                return path, visited_count
            
            # Проверяем всех соседей
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    visited_count += 1
                    queue.append((neighbor, path + [neighbor]))
        
        return [], visited_count


class DFSStrategy(PathFindingStrategy):
    """DFS — поиск в глубину. Быстрый, но путь не обязательно кратчайший."""
    
    def get_name(self) -> str:
        return "DFS"
    
    def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]:
        if not start or not exit:
            return [], 0
        
        # Стек: (текущая_клетка, путь_до_неё)
        stack = [(start, [start])]
        visited = {start}
        visited_count = 1
        
        while stack:
            current, path = stack.pop()
            
            if current == exit:
                return path, visited_count
            
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    visited_count += 1
                    stack.append((neighbor, path + [neighbor]))
        
        return [], visited_count


class AStarStrategy(PathFindingStrategy):
    """
    A* — поиск с эвристикой.
    Использует приоритетную очередь (кучу) и манхэттенское расстояние.
    """
    
    def get_name(self) -> str:
        return "A*"
    
    def _heuristic(self, cell: Cell, exit: Cell) -> int:
        """Манхэттенское расстояние: |x1-x2| + |y1-y2|."""
        if not cell or not exit:
            return 0
        return abs(cell.x - exit.x) + abs(cell.y - exit.y)
    
    def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]:
        if not start or not exit:
            return [], 0
        
        # Куча: (f_score, счётчик, клетка, путь, g_score)
        # f = g + h, где g — пройденный путь, h — эвристика
        counter = 0
        open_set = [(self._heuristic(start, exit), counter, start, [start], 0)]
        visited = set()
        visited_count = 0
        g_scores = {start: 0}
        
        while open_set:
            _, _, current, path, g_score = heapq.heappop(open_set)
            
            if current in visited:
                continue
            
            visited.add(current)
            visited_count += 1
            
            if current == exit:
                return path, visited_count
            
            for neighbor in maze.get_neighbors(current):
                if neighbor in visited:
                    continue
                
                tentative_g = g_score + 1
                
                if neighbor not in g_scores or tentative_g < g_scores[neighbor]:
                    g_scores[neighbor] = tentative_g
                    f_score = tentative_g + self._heuristic(neighbor, exit)
                    counter += 1
                    heapq.heappush(open_set, (f_score, counter, neighbor, path + [neighbor], tentative_g))
        
        return [], visited_count


class DijkstraStrategy(PathFindingStrategy):
    """
    Дейкстра — для взвешенных графов.
    В базовом варианте (вес=1) работает как BFS, но с приоритетной очередью.
    """
    
    def get_name(self) -> str:
        return "Dijkstra"
    
    def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]:
        if not start or not exit:
            return [], 0
        
        # Куча: (расстояние, счётчик, клетка, путь)
        counter = 0
        pq = [(0, counter, start, [start])]
        distances = {start: 0}
        visited = set()
        visited_count = 0
        
        while pq:
            dist, _, current, path = heapq.heappop(pq)
            
            if current in visited:
                continue
            
            visited.add(current)
            visited_count += 1
            
            if current == exit:
                return path, visited_count
            
            for neighbor in maze.get_neighbors(current):
                weight = neighbor.weight
                new_dist = dist + weight
                
                if neighbor not in distances or new_dist < distances[neighbor]:
                    distances[neighbor] = new_dist
                    counter += 1
                    heapq.heappush(pq, (new_dist, counter, neighbor, path + [neighbor]))
        
        return [], visited_count