"""Movement commands for navigating the world."""

from mudlib.commands import CommandDefinition, register
from mudlib.entity import Entity
from mudlib.gmcp import send_map_data, send_room_info
from mudlib.player import Player
from mudlib.portal import Portal
from mudlib.zone import Zone
from mudlib.zones import get_zone

# Direction mappings: command -> (dx, dy)
DIRECTIONS: dict[str, tuple[int, int]] = {
    "n": (0, -1),
    "north": (0, -1),
    "s": (0, 1),
    "south": (0, 1),
    "e": (1, 0),
    "east": (1, 0),
    "w": (-1, 0),
    "west": (-1, 0),
    "ne": (1, -1),
    "northeast": (1, -1),
    "nw": (-1, -1),
    "northwest": (-1, -1),
    "se": (1, 1),
    "southeast": (1, 1),
    "sw": (-1, 1),
    "southwest": (-1, 1),
}

# Opposite directions for arrival messages
OPPOSITE_DIRECTIONS: dict[str, str] = {
    "north": "south",
    "south": "north",
    "east": "west",
    "west": "east",
    "northeast": "southwest",
    "southwest": "northeast",
    "northwest": "southeast",
    "southeast": "northwest",
}


def get_direction_name(dx: int, dy: int) -> str:
    """Get the full direction name from deltas."""
    direction_map = {
        (0, -1): "north",
        (0, 1): "south",
        (1, 0): "east",
        (-1, 0): "west",
        (1, -1): "northeast",
        (-1, -1): "northwest",
        (1, 1): "southeast",
        (-1, 1): "southwest",
    }
    return direction_map.get((dx, dy), "")


async def move_player(player: Player, dx: int, dy: int, direction_name: str) -> None:
    """Move a player in a given direction.

    Args:
        player: The player to move
        dx: X delta
        dy: Y delta
        direction_name: Full name of the direction for messages
    """
    zone = player.location
    assert isinstance(zone, Zone), "Player must be in a zone to move"
    target_x, target_y = zone.wrap(player.x + dx, player.y + dy)

    # Check if the target is passable (skip check in paint mode)
    if not player.paint_mode and not zone.is_passable(target_x, target_y):
        await player.send("You can't go that way.\r\n")
        return

    # Check boundary exit conditions
    current_boundary = None
    target_boundary = None

    for boundary in zone.boundaries:
        if boundary.contains(player.x, player.y):
            current_boundary = boundary
        if boundary.contains(target_x, target_y):
            target_boundary = boundary

    # If leaving a boundary with an exit check, evaluate it
    if (
        current_boundary is not None
        and current_boundary is not target_boundary
        and current_boundary.on_exit_check
        and current_boundary.on_exit_check.startswith("carrying:")
    ):
        check_value = current_boundary.on_exit_check[9:]  # strip "carrying:"
        # Check if player has any item matching name or tag
        from mudlib.thing import Thing

        has_item = False
        for obj in player._contents:
            if isinstance(obj, Thing) and (
                obj.name == check_value or check_value in obj.tags
            ):
                has_item = True
                break

        if has_item:
            # Check failed, block movement
            if current_boundary.on_exit_fail:
                await player.send(f"{current_boundary.on_exit_fail}\r\n")
            return

    # If painting, place the brush tile at the current position before moving
    if player.paint_mode and player.painting:
        zone.terrain[player.y][player.x] = player.paint_brush

    # Send departure message to players in the old area
    opposite = OPPOSITE_DIRECTIONS[direction_name]
    await send_nearby_message(
        player, player.x, player.y, f"{player.name} leaves {direction_name}.\r\n"
    )

    # Update position
    player.x = target_x
    player.y = target_y

    # Send boundary messages
    # If leaving a boundary (and check passed), send exit message
    if (
        current_boundary is not None
        and current_boundary is not target_boundary
        and current_boundary.on_exit_message
    ):
        await player.send(f"{current_boundary.on_exit_message}\r\n")

    # If entering a boundary, send enter message
    if (
        target_boundary is not None
        and target_boundary is not current_boundary
        and target_boundary.on_enter_message
    ):
        await player.send(f"{target_boundary.on_enter_message}\r\n")

    # Check for auto-trigger portals at new position
    portals_here = [
        obj for obj in zone.contents_at(target_x, target_y) if isinstance(obj, Portal)
    ]
    if portals_here:
        portal = portals_here[0]  # Take first portal
        target_zone = get_zone(portal.target_zone)
        if target_zone:
            await player.send(f"You enter {portal.name}.\r\n")
            await send_nearby_message(
                player, player.x, player.y, f"{player.name} enters {portal.name}.\r\n"
            )
            player.move_to(target_zone, x=portal.target_x, y=portal.target_y)
            await send_nearby_message(
                player, player.x, player.y, f"{player.name} arrives.\r\n"
            )
            from mudlib.commands.look import cmd_look

            await cmd_look(player, "")
            send_room_info(player)
            send_map_data(player)
            return  # Don't do normal arrival+look
        else:
            await player.send("The portal doesn't lead anywhere.\r\n")
            # Stay at portal tile but show normal look
            from mudlib.commands.look import cmd_look

            await cmd_look(player, "")
            return

    # Send arrival message to players in the new area
    await send_nearby_message(
        player, player.x, player.y, f"{player.name} arrives from the {opposite}.\r\n"
    )

    # Render new viewport to the moving player
    from mudlib.commands.look import cmd_look

    await cmd_look(player, "")
    send_room_info(player)
    send_map_data(player)


async def send_nearby_message(entity: Entity, x: int, y: int, message: str) -> None:
    """Send a message to all players near a location, excluding the entity.

    Sleeping players do not receive nearby messages (they are blind to room events).

    Args:
        entity: The entity who triggered the message (excluded from receiving it)
        x: X coordinate of the location
        y: Y coordinate of the location
        message: The message to send
    """
    # For now, use a simple viewport range (could be configurable)
    viewport_range = 10

    zone = entity.location
    assert isinstance(zone, Zone), "Entity must be in a zone to send nearby messages"
    for obj in zone.contents_near(x, y, viewport_range):
        if obj is not entity and isinstance(obj, Entity):
            # Skip sleeping players (they are blind to room events)
            if getattr(obj, "sleeping", False):
                continue
            await obj.send(message)


# Define individual movement command handlers
async def move_north(player: Player, args: str) -> None:
    await move_player(player, 0, -1, "north")


async def move_south(player: Player, args: str) -> None:
    await move_player(player, 0, 1, "south")


async def move_east(player: Player, args: str) -> None:
    await move_player(player, 1, 0, "east")


async def move_west(player: Player, args: str) -> None:
    await move_player(player, -1, 0, "west")


async def move_northeast(player: Player, args: str) -> None:
    await move_player(player, 1, -1, "northeast")


async def move_northwest(player: Player, args: str) -> None:
    await move_player(player, -1, -1, "northwest")


async def move_southeast(player: Player, args: str) -> None:
    await move_player(player, 1, 1, "southeast")


async def move_southwest(player: Player, args: str) -> None:
    await move_player(player, -1, 1, "southwest")


# Register all movement commands with their aliases
register(CommandDefinition("north", move_north, aliases=["n"]))
register(CommandDefinition("south", move_south, aliases=["s"]))
register(CommandDefinition("east", move_east, aliases=["e"]))
register(CommandDefinition("west", move_west, aliases=["w"]))
register(CommandDefinition("northeast", move_northeast, aliases=["ne"]))
register(CommandDefinition("northwest", move_northwest, aliases=["nw"]))
register(CommandDefinition("southeast", move_southeast, aliases=["se"]))
register(CommandDefinition("southwest", move_southwest, aliases=["sw"]))
