"""Combat command handlers."""

import asyncio
from collections import defaultdict
from pathlib import Path

from mudlib.combat.encounter import CombatState
from mudlib.combat.engine import get_encounter, start_encounter
from mudlib.combat.moves import CombatMove, load_moves
from mudlib.combat.stamina import check_stamina_cues
from mudlib.commands import CommandDefinition, register
from mudlib.player import Player
from mudlib.render.colors import colorize
from mudlib.render.pov import render_pov

# Combat moves will be injected after loading
combat_moves: dict[str, CombatMove] = {}
combat_content_dir: Path | None = None


async def do_attack(player: Player, target_args: str, move: CombatMove) -> None:
    """Core attack logic with a resolved move.

    Args:
        player: The attacking player
        target_args: Remaining args after move resolution (just the target name)
        move: The resolved combat move
    """
    # Check unlock gating
    base_command = move.command or move.name
    if move.unlock_condition is not None and base_command not in player.unlocked_moves:
        await player.send("You haven't learned that yet.\r\n")
        return

    # Check safe zone
    if getattr(player.location, "safe", False):
        await player.send("You can't fight here.\r\n")
        return

    encounter = get_encounter(player)

    # Parse target from args
    target = None
    target_name = target_args.strip()
    if encounter is None and target_name:
        from mudlib.targeting import find_entity_on_tile

        target = find_entity_on_tile(target_name, player)

        # If no target found on same z-axis, check if one exists on different z-axis
        if target is None:
            any_z_target = find_entity_on_tile(target_name, player, z_filter=False)
            if any_z_target is not None:
                await player.send("You can't reach them from here!\r\n")
                return

    # Check stamina
    if player.stamina < move.stamina_cost:
        await player.send("You don't have enough stamina for that move.\r\n")
        return

    if encounter is None:
        # Not in combat - need a target
        if target is None:
            await player.send("You need a target to start combat.\r\n")
            return

        # Start new encounter
        try:
            encounter = start_encounter(player, target)
            player.mode_stack.append("combat")
            await player.send(f"You engage {target.name} in combat!\r\n")

            # Send telegraph to defender if they can receive messages
            if hasattr(target, "send") and move.telegraph:
                telegraph = render_pov(move.telegraph, target, player, target)
                telegraph_color = move.telegraph_color
                if telegraph_color:
                    color_depth = getattr(target, "color_depth", None)
                    telegraph = colorize(
                        f"{{{telegraph_color}}}{telegraph}{{/}}", color_depth
                    )
                await target.send(f"{telegraph}\r\n")

        except ValueError as e:
            await player.send(f"Cannot start combat: {e}\r\n")
            return
    else:
        # Already in combat - send telegraph to defender
        if encounter.attacker is player:
            defender = encounter.defender
        else:
            defender = encounter.attacker
        if hasattr(defender, "send") and move.telegraph:
            telegraph = render_pov(move.telegraph, defender, player, defender)
            telegraph_color = move.telegraph_color
            if telegraph_color:
                color_depth = getattr(defender, "color_depth", None)
                telegraph = colorize(
                    f"{{{telegraph_color}}}{telegraph}{{/}}", color_depth
                )
            await defender.send(f"{telegraph}\r\n")

    # Detect switch before attack() modifies state
    switching = encounter.state in (
        CombatState.TELEGRAPH,
        CombatState.WINDOW,
    )

    # Execute the attack (deducts stamina)
    encounter.attack(move)

    # Send vitals update immediately after stamina deduction
    from mudlib.gmcp import send_char_vitals

    send_char_vitals(player)

    # Check stamina cues after attack cost
    await check_stamina_cues(player)

    if switching:
        await player.send(f"You switch to {move.name}!\r\n")
    else:
        await player.send(f"You use {move.name}!\r\n")


async def do_defend(player: Player, _args: str, move: CombatMove) -> None:
    """Core defense logic with a resolved move.

    Works both in and outside combat. Applies a recovery lock
    (based on timing_window_ms) so defenses have commitment.

    Args:
        player: The defending player
        _args: Unused (defense moves don't take a target)
        move: The resolved combat move
    """
    # Check unlock gating
    base_command = move.command or move.name
    if move.unlock_condition is not None and base_command not in player.unlocked_moves:
        await player.send("You haven't learned that yet.\r\n")
        return

    # Check stamina
    if player.stamina < move.stamina_cost:
        await player.send("You don't have enough stamina for that move.\r\n")
        return

    player.stamina -= move.stamina_cost

    # Send vitals update immediately after stamina deduction
    from mudlib.gmcp import send_char_vitals

    send_char_vitals(player)

    # Check stamina cues after defense cost
    await check_stamina_cues(player)

    # If in combat, queue the defense on the encounter
    encounter = get_encounter(player)
    if encounter is not None:
        encounter.defend(move)

    # Broadcast to nearby players
    from mudlib.commands.movement import send_nearby_message

    await send_nearby_message(
        player,
        player.x,
        player.y,
        f"{player.name} {move.command}s!\r\n",
    )

    # Commitment: block for the timing window (inputs queue naturally)
    await asyncio.sleep(move.timing_window_ms / 1000.0)

    if encounter is not None:
        await player.send(f"You {move.name}!\r\n")
    else:
        await player.send(f"You {move.command} the air!\r\n")


def make_direct_handler(move: CombatMove, handler_fn):
    """Create a handler bound to a specific move.

    Used for simple moves (roundhouse) and variant aliases (pl, pr).
    Args are just the target name.
    """

    async def handler(player: Player, args: str) -> None:
        await handler_fn(player, args, move)

    return handler


def make_variant_handler(
    base_name: str, variant_moves: dict[str, CombatMove], handler_fn
):
    """Create a handler for a move with directional variants.

    Used for base commands like "punch" where the first arg is the direction.
    """

    async def handler(player: Player, args: str) -> None:
        parts = args.strip().split(maxsplit=1)
        if not parts:
            variants = "/".join(variant_moves.keys())
            await player.send(f"{base_name.capitalize()} which way? ({variants})\r\n")
            return

        variant_key = parts[0].lower()

        # Try exact match first
        move = variant_moves.get(variant_key)
        if move is None:
            # Fall back to prefix matching
            matches = [k for k in variant_moves if k.startswith(variant_key)]
            if len(matches) == 1:
                move = variant_moves[matches[0]]
            elif len(matches) > 1:
                variants = "/".join(sorted(matches))
                await player.send(
                    f"Ambiguous {base_name} direction: {variant_key}. ({variants})\r\n"
                )
                return
            else:
                variants = "/".join(variant_moves.keys())
                await player.send(
                    f"Unknown {base_name} direction: {variant_key}. Try: {variants}\r\n"
                )
                return

        target_args = parts[1] if len(parts) > 1 else ""
        await handler_fn(player, target_args, move)

    return handler


def register_moves(moves: list[CombatMove]) -> None:
    """Register a list of combat moves as commands.

    Args:
        moves: List of combat moves to register
    """
    # Group variant moves by their base command
    variant_groups: dict[str, dict[str, CombatMove]] = defaultdict(dict)
    simple_moves: list[CombatMove] = []
    registered_names: set[str] = set()

    for move in moves:
        if move.name in registered_names:
            continue
        registered_names.add(move.name)

        if move.variant:
            variant_groups[move.command][move.variant] = move
        else:
            simple_moves.append(move)

    # Register simple moves (roundhouse, sweep, duck, jump)
    for move in simple_moves:
        handler_fn = do_attack if move.move_type == "attack" else do_defend
        mode = "*"
        action = "Attack" if move.move_type == "attack" else "Defend"
        register(
            CommandDefinition(
                name=move.name,
                handler=make_direct_handler(move, handler_fn),
                aliases=[],
                mode=mode,
                help=f"{action} with {move.name}",
            )
        )

    # Register variant moves (punch, dodge, parry)
    for base_name, variants in variant_groups.items():
        # Determine type from first variant
        first_variant = next(iter(variants.values()))
        handler_fn = do_attack if first_variant.move_type == "attack" else do_defend
        mode = "*"

        # Register base command with variant handler (e.g. "punch")
        action = "Attack" if first_variant.move_type == "attack" else "Defend"
        register(
            CommandDefinition(
                name=base_name,
                handler=make_variant_handler(base_name, variants, handler_fn),
                aliases=[],
                mode=mode,
                help=f"{action} with {base_name}",
            )
        )


def register_combat_commands(content_dir: Path) -> None:
    """Load and register all combat moves as commands.

    Args:
        content_dir: Path to directory containing combat move TOML files
    """
    global combat_moves, combat_content_dir

    # Save content directory for use by edit command
    combat_content_dir = content_dir

    # Load all moves from content directory
    combat_moves = load_moves(content_dir)

    # Register all loaded moves
    register_moves(list(combat_moves.values()))
