"""Combat encounter and state machine."""

import time
from dataclasses import dataclass
from enum import Enum

from mudlib.combat.moves import CombatMove
from mudlib.entity import Entity


class CombatState(Enum):
    """States of the combat state machine."""

    IDLE = "idle"
    PENDING = "pending"
    RESOLVE = "resolve"


# Seconds since last landed damage before combat fizzles out
IDLE_TIMEOUT = 30.0


@dataclass
class ResolveResult:
    """Result of resolving a combat exchange."""

    resolve_template: str  # POV template for resolve message
    damage: float
    countered: bool
    combat_ended: bool


@dataclass
class CombatEncounter:
    """Represents an active combat encounter between two entities."""

    attacker: Entity
    defender: Entity
    state: CombatState = CombatState.IDLE
    current_move: CombatMove | None = None
    move_started_at: float = 0.0
    pending_defense: CombatMove | None = None
    defense_activated_at: float | None = None
    defense_recovery_until: float | None = None
    queued_defense: CombatMove | None = None
    # Monotonic timestamp of most recent landed damage in this encounter.
    last_action_at: float = 0.0

    def attack(self, move: CombatMove) -> None:
        """Initiate or switch an attack move.

        If called during PENDING, switches to the new move and restarts
        the timer. Refunds old move's stamina cost.

        Args:
            move: The attack move to execute
        """
        now = time.monotonic()

        if self.state == CombatState.PENDING and self.current_move:
            # Switching — refund old cost
            self.attacker.stamina = min(
                self.attacker.stamina + self.current_move.stamina_cost,
                self.attacker.max_stamina,
            )

        # Always restart timer
        self.move_started_at = now
        self.current_move = move
        self.attacker.stamina -= move.stamina_cost
        if self.state == CombatState.IDLE:
            self.state = CombatState.PENDING

    def defend(self, move: CombatMove) -> None:
        """Queue or activate a defense move.

        If in recovery, queues the defense. Otherwise activates immediately.
        Stamina cost is handled by the command layer (do_defend).

        Args:
            move: The defense move to attempt
        """
        now = time.monotonic()

        # Check if in recovery
        if (
            self.defense_recovery_until is not None
            and now < self.defense_recovery_until
        ):
            # Queue for later activation
            self.queued_defense = move
        else:
            # Activate immediately
            self.pending_defense = move
            self.defense_activated_at = now
            self.queued_defense = None

    def tick(self, now: float) -> None:
        """Advance the state machine based on current time.

        Args:
            now: Current time from monotonic clock
        """
        # Check if queued defense should activate
        if (
            self.queued_defense is not None
            and self.defense_recovery_until is not None
            and now >= self.defense_recovery_until
        ):
            # Activate queued defense
            self.pending_defense = self.queued_defense
            self.defense_activated_at = now
            self.queued_defense = None
            self.defense_recovery_until = None

        # Check PENDING -> RESOLVE transition
        if self.state == CombatState.PENDING:
            if self.current_move is None:
                return

            elapsed = now - self.move_started_at
            hit_time_seconds = self.current_move.hit_time_ms / 1000.0

            if elapsed >= hit_time_seconds:
                self.state = CombatState.RESOLVE
                # Don't expire defense here - resolve() will handle it after checking

        # Only expire defense if NOT in RESOLVE state
        # (resolve() will clear defense after checking for counters)
        if (
            self.state != CombatState.RESOLVE
            and self.defense_activated_at is not None
            and self.pending_defense is not None
        ):
            active_duration = now - self.defense_activated_at
            active_seconds = self.pending_defense.active_ms / 1000.0
            if active_duration >= active_seconds:
                # Defense window expired, enter recovery
                recovery_seconds = self.pending_defense.recovery_ms / 1000.0
                self.defense_recovery_until = now + recovery_seconds
                self.defense_activated_at = None
                self.pending_defense = None

    def resolve(self) -> ResolveResult:
        """Resolve the combat exchange and return result.

        Returns:
            ResolveResult with template for both participants
        """
        if self.current_move is None:
            return ResolveResult(
                resolve_template="No active move to resolve.",
                damage=0.0,
                countered=False,
                combat_ended=False,
            )

        # Check for altitude mismatch (flying dodge)
        attacker_flying = getattr(self.attacker, "flying", False)
        defender_flying = getattr(self.defender, "flying", False)
        if attacker_flying != defender_flying:
            # Altitude mismatch - attack misses
            template = "{attacker} miss{es} — {defender} {are|is} out of reach!"
            # Reset to IDLE
            self.state = CombatState.IDLE
            self.current_move = None
            self.pending_defense = None
            return ResolveResult(
                resolve_template=template,
                damage=0.0,
                countered=True,
                combat_ended=False,
            )

        # Check if defense counters attack
        # Defense must be active (in active window) to succeed
        defense_is_active = False
        if self.defense_activated_at is not None and self.pending_defense is not None:
            active_duration = time.monotonic() - self.defense_activated_at
            active_window = self.pending_defense.active_ms / 1000.0
            defense_is_active = active_duration < active_window

        defense_succeeds = (
            defense_is_active
            and self.pending_defense is not None
            and self.pending_defense.name in self.current_move.countered_by
        )
        if defense_succeeds:
            # Successful counter - no damage
            damage = 0.0
            template = (
                self.current_move.resolve_miss
                if self.current_move.resolve_miss
                else "{defender} counter{s} {attacker}'s attack!"
            )
            countered = True
        elif self.pending_defense:
            # Wrong defense - normal damage
            damage = self.attacker.pl * self.current_move.damage_pct
            self.defender.pl = max(0.0, self.defender.pl - damage)
            template = (
                self.current_move.resolve_hit
                if self.current_move.resolve_hit
                else "{attacker}'s attack hit{s} {defender} for damage!"
            )
            countered = False
        else:
            # No defense - increased damage
            damage = self.attacker.pl * self.current_move.damage_pct * 1.5
            self.defender.pl = max(0.0, self.defender.pl - damage)
            template = (
                self.current_move.resolve_hit
                if self.current_move.resolve_hit
                else "{attacker}'s attack hit{s} {defender} for damage!"
            )
            countered = False

        if damage > 0:
            self.last_action_at = time.monotonic()
        combat_ended = False

        # Reset to IDLE and clear defense state
        # Note: defense_recovery_until persists across attacks
        self.state = CombatState.IDLE
        self.current_move = None
        self.pending_defense = None
        self.defense_activated_at = None

        return ResolveResult(
            resolve_template=template,
            damage=damage,
            countered=countered,
            combat_ended=combat_ended,
        )
