from __future__ import annotations

from collections import deque
from collections.abc import Callable

from .enums import Action, CellType
from .models import GameState, Position

_DEFAULT_BOMB_RANGE = 2


class GameHelpers:
    def __init__(self, state: GameState):
        self.state = state

    def is_walkable(self, pos: Position) -> bool:
        if (
            pos.x < 0
            or pos.x >= self.state.field.width
            or pos.y < 0
            or pos.y >= self.state.field.height
        ):
            return False

        cell = self.state.field.cell_at(pos)
        if cell in (CellType.WALL, CellType.BOX):
            return False

        for bomb in self.state.bombs:
            if bomb.pos == pos:
                return False

        return True

    def get_adjacent_walkable_positions(self, pos: Position) -> list[Position]:
        candidates = [
            Position(pos.x, pos.y - 1),
            Position(pos.x + 1, pos.y),
            Position(pos.x, pos.y + 1),
            Position(pos.x - 1, pos.y),
        ]

        return [next_pos for next_pos in candidates if self.is_walkable(next_pos)]

    def get_next_action_towards(self, start: Position, target: Position) -> Action:
        if start == target:
            return Action.DO_NOTHING

        prev = self._bfs(start, lambda pos: pos == target, allow_unsafe_start=False)
        if prev is None:
            return Action.DO_NOTHING

        path = _rebuild_path(start, target, prev)
        if len(path) < 2:
            return Action.DO_NOTHING

        return _action_from_step(path[0], path[1])

    def is_safe(self, pos: Position) -> bool:
        if (
            pos.x < 0
            or pos.x >= self.state.field.width
            or pos.y < 0
            or pos.y >= self.state.field.height
        ):
            return False

        for bomb in self.state.bombs:
            if bomb.pos == pos:
                return False

        danger = self._compute_danger_positions()
        return not danger.get(pos, False)

    def get_nearest_safe_position(self, start: Position) -> Position:
        if self.is_walkable(start) and self.is_safe(start):
            return start

        prev = self._bfs(
            start,
            lambda pos: self.is_walkable(pos) and self.is_safe(pos),
            allow_unsafe_start=True,
        )
        if prev is None:
            return start

        queue = deque([start])
        visited = {start}

        while queue:
            cur = queue.popleft()
            if self.is_walkable(cur) and self.is_safe(cur):
                return cur

            for next_pos in self.get_adjacent_walkable_positions(cur):
                if next_pos not in visited:
                    visited.add(next_pos)
                    queue.append(next_pos)

        return start

    def find_nearest_box(self, start: Position) -> tuple[Position, bool]:
        queue = deque([start])
        visited = {start}

        while queue:
            cur = queue.popleft()
            if self.state.field.cell_at(cur) == CellType.BOX:
                return cur, True

            for next_pos in [
                Position(cur.x, cur.y - 1),
                Position(cur.x + 1, cur.y),
                Position(cur.x, cur.y + 1),
                Position(cur.x - 1, cur.y),
            ]:
                if (
                    next_pos.x < 0
                    or next_pos.x >= self.state.field.width
                    or next_pos.y < 0
                    or next_pos.y >= self.state.field.height
                ):
                    continue
                if next_pos in visited:
                    continue

                cell = self.state.field.cell_at(next_pos)
                if cell == CellType.WALL:
                    continue

                visited.add(next_pos)
                queue.append(next_pos)

        return Position(0, 0), False

    def _bfs(
        self,
        start: Position,
        goal: Callable[[Position], bool],
        allow_unsafe_start: bool,
    ) -> dict[Position, Position] | None:
        if not allow_unsafe_start and not self.is_walkable(start):
            return None

        queue = deque([start])
        visited = {start}
        prev: dict[Position, Position] = {}

        while queue:
            cur = queue.popleft()
            if cur != start and goal(cur):
                return prev

            for next_pos in self.get_adjacent_walkable_positions(cur):
                if next_pos in visited:
                    continue
                visited.add(next_pos)
                prev[next_pos] = cur
                queue.append(next_pos)

        return None

    def _compute_danger_positions(self) -> dict[Position, bool]:
        danger: dict[Position, bool] = {}

        bomb_index = {bomb.pos: idx for idx, bomb in enumerate(self.state.bombs)}
        triggered = [False] * len(self.state.bombs)
        queue = deque[int]()

        for e in self.state.explosions:
            danger[e] = True
            idx = bomb_index.get(e)
            if idx is not None and not triggered[idx]:
                triggered[idx] = True
                queue.append(idx)

        for i, bomb in enumerate(self.state.bombs):
            if bomb.fuse <= 1 and not triggered[i]:
                triggered[i] = True
                queue.append(i)

        while queue:
            idx = queue.popleft()
            blast = self._blast_cells(self.state.bombs[idx].pos)
            for cell in blast:
                danger[cell] = True
                hit_idx = bomb_index.get(cell)
                if hit_idx is not None and not triggered[hit_idx]:
                    triggered[hit_idx] = True
                    queue.append(hit_idx)

        return danger

    def _blast_cells(self, origin: Position) -> list[Position]:
        cells = [origin]
        directions = [Position(0, -1), Position(1, 0), Position(0, 1), Position(-1, 0)]

        for direction in directions:
            for step in range(1, _DEFAULT_BOMB_RANGE + 1):
                pos = Position(
                    origin.x + direction.x * step, origin.y + direction.y * step
                )
                if (
                    pos.x < 0
                    or pos.x >= self.state.field.width
                    or pos.y < 0
                    or pos.y >= self.state.field.height
                ):
                    break

                cell = self.state.field.cell_at(pos)
                if cell == CellType.WALL:
                    break

                cells.append(pos)
                if cell == CellType.BOX:
                    break

        return cells


def new_game_helpers(state: GameState) -> GameHelpers:
    return GameHelpers(state)


def _rebuild_path(
    start: Position, target: Position, prev: dict[Position, Position]
) -> list[Position]:
    path = [target]
    cur = target

    while cur != start:
        parent = prev.get(cur)
        if parent is None:
            return []
        cur = parent
        path.append(cur)

    path.reverse()
    return path


def _action_from_step(from_pos: Position, to_pos: Position) -> Action:
    if to_pos.x == from_pos.x and to_pos.y == from_pos.y - 1:
        return Action.MOVE_UP
    if to_pos.x == from_pos.x + 1 and to_pos.y == from_pos.y:
        return Action.MOVE_RIGHT
    if to_pos.x == from_pos.x and to_pos.y == from_pos.y + 1:
        return Action.MOVE_DOWN
    if to_pos.x == from_pos.x - 1 and to_pos.y == from_pos.y:
        return Action.MOVE_LEFT

    return Action.DO_NOTHING
