from typing import Optional

from source.settings import cell_mapping


class Cell:
    """Представляет одну клетку поля лабиринта.

    Каждая клетка хранит свои координаты и один из четырёх возможных
    типов: стена, старт, выход или пустая клетка. Типы взаимоисключают
    друг друга: установка одного автоматически сбрасывает остальные.
    """

    def __init__(
        self,
        x: int,
        y: int,
        is_wall: bool = False,
        is_start: bool = False,
        is_exit: bool = False,
    ):
        """Инициализирует клетку с заданными координатами и типом.

        Args:
            x: Координата клетки по оси X.
            y: Координата клетки по оси Y.
            is_wall: Если True — клетка является стеной.
            is_start: Если True — клетка является стартом.
            is_exit: Если True — клетка является выходом.
        """
        self.x = x
        self.y = y
        self._is_wall = is_wall
        self._is_start = is_start
        self._is_exit = is_exit

    def is_possible(self) -> bool:
        """Проверяет, можно ли переместиться в эту клетку.

        Returns:
            True, если клетка не является стеной, иначе False.
        """
        return not self._is_wall

    @property
    def is_wall(self) -> bool:
        """True, если клетка является стеной."""
        return self._is_wall

    @property
    def is_start(self) -> bool:
        """True, если клетка является стартовой позицией."""
        return self._is_start

    @property
    def is_exit(self) -> bool:
        """True, если клетка является выходом из лабиринта."""
        return self._is_exit

    def _clear_flags(self) -> None:
        """Сбрасывает все флаги типа клетки в False."""
        self._is_wall = False
        self._is_start = False
        self._is_exit = False

    @is_wall.setter
    def is_wall(self, value: bool) -> None:
        """Устанавливает флаг стены, сбрасывая остальные типы при value=True.

        Args:
            value: Новое значение флага стены.
        """
        if value:
            self._clear_flags()
        self._is_wall = value

    @is_start.setter
    def is_start(self, value: bool) -> None:
        """Устанавливает флаг старта, сбрасывая остальные типы при value=True.

        Args:
            value: Новое значение флага старта.
        """
        if value:
            self._clear_flags()
        self._is_start = value

    @is_exit.setter
    def is_exit(self, value: bool) -> None:
        """Устанавливает флаг выхода, сбрасывая остальные типы при value=True.

        Args:
            value: Новое значение флага выхода.
        """
        if value:
            self._clear_flags()
        self._is_exit = value

    def _get_type_cell(self) -> str:
        """Возвращает символ клетки согласно cell_mapping.

        Returns:
            Строковый символ, соответствующий текущему типу клетки.
        """
        if self._is_wall:
            return cell_mapping["wall"]
        if self._is_start:
            return cell_mapping["start"]
        if self._is_exit:
            return cell_mapping["exit"]
        return cell_mapping["empty"]

    def __str__(self) -> str:
        return self._get_type_cell()

    def __repr__(self) -> str:
        return f"Cell(x={self.x}, y={self.y}, '{self._get_type_cell()}')"


class Maze:
    """Представляет двумерный лабиринт из клеток Cell.

    Лабиринт хранится как список списков клеток. Доступ к отдельным
    клеткам и их изменение возможны через индексацию вида maze[row, col].
    """

    def __init__(self, size: tuple[int, int] = (10, 10)):
        """Создаёт пустой лабиринт заданного размера.

        Args:
            size: Кортеж (width, height) — ширина и высота лабиринта в клетках.
        """
        self._width, self._height = size
        self._map: list[list[Cell]] = [
            [Cell(x, y) for x in range(self._width)] for y in range(self._height)
        ]

    def _check_point_in_map(self, x: int, y: int) -> bool:
        """Проверяет, находится ли точка в границах лабиринта.

        Args:
            x: Координата по оси X.
            y: Координата по оси Y.

        Returns:
            True, если точка (x, y) находится внутри лабиринта.
        """
        return 0 <= x < self._width and 0 <= y < self._height

    def get_cell(self, x: int, y: int) -> Optional[Cell]:
        """Возвращает клетку по координатам или None, если координаты вне границ.

        Args:
            x: Координата по оси X.
            y: Координата по оси Y.

        Returns:
            Объект Cell, если координаты корректны, иначе None.
        """
        if not self._check_point_in_map(x, y):
            return None
        return self._map[y][x]

    def get_neighbors(self, x: int, y: int) -> Optional[list[Cell]]:
        """Возвращает список проходимых соседей клетки (вверх, вправо, вниз, влево).

        Args:
            x: Координата клетки по оси X.
            y: Координата клетки по оси Y.

        Returns:
            Список проходимых соседних клеток, или None если (x, y) вне границ.
        """
        if not self._check_point_in_map(x, y):
            return None

        deltas = ((0, 1), (1, 0), (0, -1), (-1, 0))
        neighbors = []

        for dx, dy in deltas:
            cell = self.get_cell(x + dx, y + dy)
            if cell is not None and cell.is_possible():
                neighbors.append(cell)

        return neighbors

    @property
    def start(self) -> Optional[Cell]:
        for y in range(self._height):
            for x in range(self._width):
                if self[y, x].is_start:
                    return self[y, x]
        return None

    @property
    def exit(self) -> Optional[Cell]:
        for y in range(self._height):
            for x in range(self._width):
                if self[y, x].is_exit:
                    return self[y, x]
        return None

    @property
    def shape(self) -> tuple[int, int]:
        return self._height, self._width

    def __getitem__(self, index: tuple[int, int]) -> Cell:
        """Возвращает клетку по индексу [row, col].

        Args:
            index: Кортеж (row, col) — строка и столбец.

        Returns:
            Объект Cell в позиции (row, col).

        Raises:
            IndexError: Если индекс выходит за пределы лабиринта.
        """
        row, col = index
        if not self._check_point_in_map(col, row):
            raise IndexError(f"Индекс ({row}, {col}) выходит за пределы лабиринта")
        return self._map[row][col]

    def __setitem__(self, index: tuple[int, int], value: str) -> None:
        """Устанавливает тип клетки по индексу [row, col] через символ из cell_mapping.

        Args:
            index: Кортеж (row, col) — строка и столбец.
            value: Символ типа клетки согласно cell_mapping.

        Raises:
            IndexError: Если индекс выходит за пределы лабиринта.
            ValueError: Если символ не найден в cell_mapping.
        """
        row, col = index
        if not self._check_point_in_map(col, row):
            raise IndexError(f"Индекс ({row}, {col}) выходит за пределы лабиринта")

        cell = self._map[row][col]
        cell_type = next(
            (t for t, s in cell_mapping.items() if s == value),
            None,
        )

        if cell_type is None:
            raise ValueError(f"Символ '{value}' не соответствует ни одному типу клетки")

        if cell_type == "empty":
            cell._clear_flags()
        else:
            setattr(cell, f"is_{cell_type}", True)

    def __str__(self) -> str:
        return "\n".join(
            "".join(str(self._map[y][x]) for x in range(self._width))
            for y in range(self._height)
        )
