"""Zone — a spatial area with a terrain grid."""

from __future__ import annotations

import random
from dataclasses import dataclass, field

from mudlib.object import Object


@dataclass(eq=False)
class BoundaryRegion:
    """A rectangular region within a zone that can trigger effects.

    Used for anti-theft detection, level gates, messages, etc.
    """

    name: str
    x_min: int
    x_max: int
    y_min: int
    y_max: int
    on_enter_message: str | None = None
    on_exit_message: str | None = None
    on_exit_check: str | None = None
    on_exit_fail: str | None = None

    def contains(self, x: int, y: int) -> bool:
        """Check if a position is inside this boundary region."""
        return self.x_min <= x <= self.x_max and self.y_min <= y <= self.y_max


@dataclass(eq=False)
class SpawnRule:
    """Configuration for spawning mobs in a zone.

    Defines what mob should spawn, how many can exist at once,
    and how long to wait before respawning after one dies.
    """

    mob: str
    max_count: int = 1
    respawn_seconds: int = 300
    home_region: dict | None = None


@dataclass(eq=False)
class Zone(Object):
    """A spatial area with a grid of terrain tiles.

    Zones are top-level containers (location=None). Everything in the world
    lives inside a zone — players, mobs, items, fixtures. The overworld is
    a zone. A tavern interior is a zone. A pocket dimension is a zone.
    """

    description: str = ""
    width: int = 0
    height: int = 0
    toroidal: bool = True
    terrain: list[list[str]] = field(default_factory=list)
    impassable: set[str] = field(default_factory=lambda: {"^", "~"})
    spawn_x: int = 0
    spawn_y: int = 0
    ambient_messages: list[str] = field(default_factory=list)
    ambient_interval: int = 120
    spawn_rules: list[SpawnRule] = field(default_factory=list)
    safe: bool = False
    boundaries: list[BoundaryRegion] = field(default_factory=list)

    def can_accept(self, obj: Object) -> bool:
        """Zones accept everything."""
        return True

    def wrap(self, x: int, y: int) -> tuple[int, int]:
        """Wrap or clamp coordinates depending on zone type."""
        if self.toroidal:
            return x % self.width, y % self.height
        else:
            return (
                max(0, min(x, self.width - 1)),
                max(0, min(y, self.height - 1)),
            )

    def get_tile(self, x: int, y: int) -> str:
        """Return terrain character at position (wrapping/clamping)."""
        wx, wy = self.wrap(x, y)
        return self.terrain[wy][wx]

    def is_passable(self, x: int, y: int) -> bool:
        """Check if position is passable (wrapping/clamping)."""
        return self.get_tile(x, y) not in self.impassable

    def get_viewport(
        self, cx: int, cy: int, width: int, height: int
    ) -> list[list[str]]:
        """Return 2D slice of terrain centered on (cx, cy)."""
        viewport = []
        half_w = width // 2
        half_h = height // 2
        start_x = cx - half_w
        start_y = cy - half_h

        for dy in range(height):
            row = []
            for dx in range(width):
                wx, wy = self.wrap(start_x + dx, start_y + dy)
                row.append(self.terrain[wy][wx])
            viewport.append(row)

        return viewport

    def contents_at(self, x: int, y: int) -> list[Object]:
        """Return all contents at the given coordinates."""
        return [obj for obj in self._contents if obj.x == x and obj.y == y]

    def contents_near(self, x: int, y: int, range_: int) -> list[Object]:
        """Return all contents within range_ tiles of (x, y).

        Uses wrapping-aware Manhattan distance for toroidal zones.
        Includes objects at exactly (x, y).

        Args:
            x: X coordinate of center point
            y: Y coordinate of center point
            range_: Maximum Manhattan distance (inclusive)

        Returns:
            List of all objects within range
        """
        nearby = []
        for obj in self._contents:
            # Skip objects without coordinates
            if obj.x is None or obj.y is None:
                continue

            dx_dist = abs(obj.x - x)
            dy_dist = abs(obj.y - y)

            if self.toroidal:
                # Use wrapping-aware distance
                dx_dist = min(dx_dist, self.width - dx_dist)
                dy_dist = min(dy_dist, self.height - dy_dist)

            manhattan_dist = dx_dist + dy_dist
            if manhattan_dist <= range_:
                nearby.append(obj)

        return nearby


def get_ambient_message(zone: Zone) -> str | None:
    """Return a random ambient message from the zone's list.

    Args:
        zone: The zone to get an ambient message from

    Returns:
        A random message from the zone's ambient_messages list,
        or None if the list is empty
    """
    if not zone.ambient_messages:
        return None
    return random.choice(zone.ambient_messages)
