"""Mob AI — game loop processor for mob combat decisions."""

import random
import time

from mudlib.combat.encounter import CombatState
from mudlib.combat.engine import get_encounter
from mudlib.combat.moves import CombatMove
from mudlib.commands.movement import (
    OPPOSITE_DIRECTIONS,
    get_direction_name,
    send_nearby_message,
)
from mudlib.mobs import mobs
from mudlib.npc_behavior import (
    get_flee_direction,
    get_patrol_direction,
    process_behavior,
)
from mudlib.render.colors import colorize
from mudlib.render.pov import render_pov
from mudlib.zone import Zone

# Seconds between mob actions (gives player time to read and react)
MOB_ACTION_COOLDOWN = 1.0

# Seconds between mob pathfinding movements
MOB_MOVEMENT_COOLDOWN = 3.0


async def process_mobs(combat_moves: dict[str, CombatMove]) -> None:
    """Called once per game loop tick. Handles mob combat decisions."""
    now = time.monotonic()

    for mob in mobs[:]:  # copy list in case of modification
        if not mob.alive:
            continue

        encounter = get_encounter(mob)
        if encounter is None:
            continue

        if now < mob.next_action_at:
            continue

        # Determine if mob is attacker or defender in this encounter
        mob_is_defender = encounter.defender is mob

        # Defense AI: react during TELEGRAPH or WINDOW when mob is defender
        if mob_is_defender and encounter.state in (
            CombatState.TELEGRAPH,
            CombatState.WINDOW,
        ):
            _try_defend(mob, encounter, combat_moves, now)
            continue

        # Attack AI: act when encounter is IDLE
        if encounter.state == CombatState.IDLE:
            await _try_attack(mob, encounter, combat_moves, now)


async def _try_attack(mob, encounter, combat_moves, now):
    """Attempt to pick and execute an attack move."""
    # Filter to affordable attack moves from mob's move list
    affordable = []
    for move_name in mob.moves:
        move = combat_moves.get(move_name)
        if (
            move is not None
            and move.move_type == "attack"
            and mob.stamina >= move.stamina_cost
        ):
            affordable.append(move)

    if not affordable:
        return

    move = random.choice(affordable)

    # Swap roles if mob is currently the defender
    if encounter.defender is mob:
        encounter.attacker, encounter.defender = (
            encounter.defender,
            encounter.attacker,
        )

    encounter.attack(move)
    mob.next_action_at = now + MOB_ACTION_COOLDOWN

    # Send telegraph to the player (the defender after role swap)
    if move.telegraph:
        defender = encounter.defender
        telegraph_msg = render_pov(move.telegraph, defender, mob, defender)
        telegraph_color = move.telegraph_color
        if telegraph_color:
            color_depth = getattr(defender, "color_depth", None)
            telegraph_msg = colorize(
                f"{{{telegraph_color}}}{telegraph_msg}{{/}}", color_depth
            )
        await defender.send(f"{telegraph_msg}\r\n")


def _try_defend(mob, encounter, combat_moves, now):
    """Attempt to pick and queue a defense move."""
    # Don't double-defend
    if encounter.pending_defense is not None:
        return

    # Filter to affordable defense moves from mob's move list
    affordable_defenses = []
    for move_name in mob.moves:
        move = combat_moves.get(move_name)
        if (
            move is not None
            and move.move_type == "defense"
            and mob.stamina >= move.stamina_cost
        ):
            affordable_defenses.append(move)

    if not affordable_defenses:
        return

    # 40% chance to pick a correct counter, 60% random
    chosen = None
    current_move = encounter.current_move
    if current_move and random.random() < 0.4:
        # Try to find a correct counter
        counters = []
        for defense in affordable_defenses:
            if defense.name in current_move.countered_by:
                counters.append(defense)
        if counters:
            chosen = random.choice(counters)

    if chosen is None:
        chosen = random.choice(affordable_defenses)

    encounter.defend(chosen)
    mob.stamina -= chosen.stamina_cost


async def process_mob_movement() -> None:
    """Move mobs back toward their home regions if they've strayed.

    Called once per game loop tick. For each mob:
    - Skip if in combat/encounter
    - Skip if behavior state blocks movement (converse, working)
    - If patrol state, use waypoint navigation instead of home region
    - If flee state, move away from threat
    - If idle state (default), use home region wander logic
    - Skip if no home region set (for idle state)
    - Skip if already inside home region (for idle state)
    - Skip if not enough time has passed since last move (throttle)
    - Calculate direction (behavior-dependent or toward home region center)
    - Move one tile in that direction (prefer axis with larger distance)
    - Check passability before moving
    - Broadcast movement to nearby players
    """
    now = time.monotonic()

    for mob in mobs[:]:  # copy list in case of modification
        if not mob.alive:
            continue

        # Skip if mob is in combat
        encounter = get_encounter(mob)
        if encounter is not None:
            continue

        # Skip if behavior state blocks movement
        if mob.behavior_state in ("converse", "working"):
            continue

        # Skip if not enough time has passed (throttle)
        if now < mob.next_action_at:
            continue

        zone = mob.location
        assert isinstance(zone, Zone), "Mob must be in a zone to move"

        # Determine movement direction based on behavior state
        move_x = 0
        move_y = 0
        direction_name = None

        if mob.behavior_state == "patrol":
            # Use patrol waypoint navigation
            direction_name = get_patrol_direction(mob, zone)
            if direction_name is None:
                # Already at waypoint, process behavior to advance waypoint
                await process_behavior(mob, zone)
                continue

            # Convert direction name to movement delta
            if direction_name == "north":
                move_y = -1
            elif direction_name == "south":
                move_y = 1
            elif direction_name == "east":
                move_x = 1
            elif direction_name == "west":
                move_x = -1

        elif mob.behavior_state == "flee":
            # Use flee direction calculation
            direction_name = get_flee_direction(mob, zone)
            if direction_name is None:
                continue

            # Convert direction name to movement delta
            if direction_name == "north":
                move_y = -1
            elif direction_name == "south":
                move_y = 1
            elif direction_name == "east":
                move_x = 1
            elif direction_name == "west":
                move_x = -1

        else:
            # Default behavior (idle): move toward home region
            # Skip if no home region set
            if (
                mob.home_x_min is None
                or mob.home_x_max is None
                or mob.home_y_min is None
                or mob.home_y_max is None
            ):
                continue

            # Skip if already inside home region
            if (
                mob.home_x_min <= mob.x <= mob.home_x_max
                and mob.home_y_min <= mob.y <= mob.home_y_max
            ):
                continue

            # Calculate center of home region
            center_x = (mob.home_x_min + mob.home_x_max) // 2
            center_y = (mob.home_y_min + mob.home_y_max) // 2

            # Calculate direction toward center
            dx = center_x - mob.x
            dy = center_y - mob.y

            # Move one tile (prefer axis with larger distance, ties prefer x)
            if abs(dx) >= abs(dy):
                move_x = 1 if dx > 0 else -1 if dx < 0 else 0
            else:
                move_y = 1 if dy > 0 else -1 if dy < 0 else 0

            direction_name = get_direction_name(move_x, move_y)

        # Check if we have a valid movement
        if move_x == 0 and move_y == 0:
            continue

        # Calculate target position
        target_x = mob.x + move_x
        target_y = mob.y + move_y

        # Handle toroidal wrapping if needed
        if zone.toroidal:
            target_x, target_y = zone.wrap(target_x, target_y)

        if not zone.is_passable(target_x, target_y):
            # Can't move that way, skip this tick
            continue

        # Send departure message
        if direction_name:
            await send_nearby_message(
                mob, mob.x, mob.y, f"{mob.name} wanders {direction_name}.\r\n"
            )

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

        # Send arrival message (use opposite direction)
        if direction_name:
            opposite = OPPOSITE_DIRECTIONS.get(direction_name, "")
            if opposite:
                await send_nearby_message(
                    mob,
                    mob.x,
                    mob.y,
                    f"{mob.name} wanders in from the {opposite}.\r\n",
                )

        # Set next move time
        mob.next_action_at = now + MOB_MOVEMENT_COOLDOWN
