"""
Damage Calculator Service

A comprehensive, formula-driven damage calculation system for Code of Conquest.
Handles physical, magical, and elemental damage with LUK stat integration
for variance, critical hits, and accuracy.

Formulas:
    Physical: (Weapon_Base + STR * 0.75) * Variance * Crit_Mult - DEF
    Magical:  (Ability_Base + INT * 0.75) * Variance * Crit_Mult - RES
    Elemental: Split between physical and magical components

LUK Integration:
    - Miss reduction: 10% base - (LUK * 0.5%), hard cap at 5% miss
    - Crit bonus: Base 5% + (LUK * 0.5%), max 25%
    - Lucky variance: 5% + (LUK * 0.25%) chance for higher damage roll
"""

import random
from dataclasses import dataclass, field
from typing import Dict, Any, Optional, List

from app.models.stats import Stats
from app.models.enums import DamageType


class CombatConstants:
    """
    Combat system tuning constants.

    These values control the balance of combat mechanics and can be
    adjusted for game balance without modifying formula logic.
    """

    # Stat Scaling
    # How much primary stats (STR/INT) contribute to damage
    # 0.75 means STR 14 adds +10.5 damage
    STAT_SCALING_FACTOR: float = 0.75

    # Hit/Miss System
    BASE_MISS_CHANCE: float = 0.10      # 10% base miss rate
    LUK_MISS_REDUCTION: float = 0.005   # 0.5% per LUK point
    DEX_EVASION_BONUS: float = 0.0025   # 0.25% per DEX above 10
    MIN_MISS_CHANCE: float = 0.05       # Hard cap: 5% minimum miss

    # Critical Hits
    DEFAULT_CRIT_CHANCE: float = 0.05   # 5% base crit
    LUK_CRIT_BONUS: float = 0.005       # 0.5% per LUK point
    MAX_CRIT_CHANCE: float = 0.25       # 25% cap (before skills)
    DEFAULT_CRIT_MULTIPLIER: float = 2.0

    # Damage Variance
    BASE_VARIANCE_MIN: float = 0.95     # Minimum variance roll
    BASE_VARIANCE_MAX: float = 1.05     # Maximum variance roll
    LUCKY_VARIANCE_MIN: float = 1.00    # Lucky roll minimum
    LUCKY_VARIANCE_MAX: float = 1.10    # Lucky roll maximum (10% bonus)
    BASE_LUCKY_CHANCE: float = 0.05     # 5% base lucky roll chance
    LUK_LUCKY_BONUS: float = 0.0025     # 0.25% per LUK point

    # Defense Mitigation
    # Ensures high-DEF targets still take meaningful damage
    MIN_DAMAGE_RATIO: float = 0.20      # 20% of raw always goes through
    MIN_DAMAGE: int = 1                  # Absolute minimum damage


@dataclass
class DamageResult:
    """
    Result of a damage calculation.

    Contains the calculated damage values, whether the attack was a crit or miss,
    and a human-readable message for the combat log.

    Attributes:
        total_damage: Final damage after all calculations
        physical_damage: Physical component (for split damage)
        elemental_damage: Elemental component (for split damage)
        damage_type: Primary damage type (physical, fire, etc.)
        is_critical: Whether the attack was a critical hit
        is_miss: Whether the attack missed entirely
        variance_roll: The variance multiplier that was applied
        raw_damage: Damage before defense mitigation
        message: Human-readable description for combat log
    """

    total_damage: int = 0
    physical_damage: int = 0
    elemental_damage: int = 0
    damage_type: DamageType = DamageType.PHYSICAL
    elemental_type: Optional[DamageType] = None
    is_critical: bool = False
    is_miss: bool = False
    variance_roll: float = 1.0
    raw_damage: int = 0
    message: str = ""

    def to_dict(self) -> Dict[str, Any]:
        """Serialize damage result to dictionary."""
        return {
            "total_damage": self.total_damage,
            "physical_damage": self.physical_damage,
            "elemental_damage": self.elemental_damage,
            "damage_type": self.damage_type.value if self.damage_type else "physical",
            "elemental_type": self.elemental_type.value if self.elemental_type else None,
            "is_critical": self.is_critical,
            "is_miss": self.is_miss,
            "variance_roll": round(self.variance_roll, 3),
            "raw_damage": self.raw_damage,
            "message": self.message,
        }


class DamageCalculator:
    """
    Formula-driven damage calculator for combat.

    This class provides static methods for calculating all types of damage
    in the combat system, including hit/miss chances, critical hits,
    damage variance, and defense mitigation.

    All formulas integrate the LUK stat for meaningful randomness while
    maintaining a hard cap on miss chance to prevent frustration.
    """

    @staticmethod
    def calculate_hit_chance(
        attacker_luck: int,
        defender_dexterity: int,
        skill_bonus: float = 0.0
    ) -> float:
        """
        Calculate hit probability for an attack.

        Formula:
            miss_chance = max(0.05, 0.10 - (LUK * 0.005) + ((DEX - 10) * 0.0025))
            hit_chance = 1.0 - miss_chance

        Args:
            attacker_luck: Attacker's LUK stat
            defender_dexterity: Defender's DEX stat
            skill_bonus: Additional hit chance from skills (0.0 to 1.0)

        Returns:
            Hit probability as a float between 0.0 and 1.0

        Examples:
            LUK 8, DEX 10: miss = 10% - 4% + 0% = 6%
            LUK 12, DEX 10: miss = 10% - 6% + 0% = 4% -> capped at 5%
            LUK 8, DEX 15: miss = 10% - 4% + 1.25% = 7.25%
        """
        # Base miss rate
        base_miss = CombatConstants.BASE_MISS_CHANCE

        # LUK reduces miss chance
        luk_reduction = attacker_luck * CombatConstants.LUK_MISS_REDUCTION

        # High DEX increases evasion (only DEX above 10 counts)
        dex_above_base = max(0, defender_dexterity - 10)
        dex_evasion = dex_above_base * CombatConstants.DEX_EVASION_BONUS

        # Calculate final miss chance with hard cap
        miss_chance = base_miss - luk_reduction + dex_evasion - skill_bonus
        miss_chance = max(CombatConstants.MIN_MISS_CHANCE, miss_chance)

        return 1.0 - miss_chance

    @staticmethod
    def calculate_crit_chance(
        attacker_luck: int,
        weapon_crit_chance: float = CombatConstants.DEFAULT_CRIT_CHANCE,
        skill_bonus: float = 0.0
    ) -> float:
        """
        Calculate critical hit probability.

        Formula:
            crit_chance = min(0.25, weapon_crit + (LUK * 0.005) + skill_bonus)

        Args:
            attacker_luck: Attacker's LUK stat
            weapon_crit_chance: Base crit chance from weapon (default 5%)
            skill_bonus: Additional crit chance from skills

        Returns:
            Crit probability as a float (capped at 25%)

        Examples:
            LUK 8, weapon 5%: crit = 5% + 4% = 9%
            LUK 12, weapon 5%: crit = 5% + 6% = 11%
            LUK 12, weapon 10%: crit = 10% + 6% = 16%
        """
        # LUK bonus to crit
        luk_bonus = attacker_luck * CombatConstants.LUK_CRIT_BONUS

        # Total crit chance with cap
        total_crit = weapon_crit_chance + luk_bonus + skill_bonus

        return min(CombatConstants.MAX_CRIT_CHANCE, total_crit)

    @staticmethod
    def calculate_variance(attacker_luck: int) -> float:
        """
        Calculate damage variance multiplier with LUK bonus.

        Hybrid variance system:
            - Base roll: 95% to 105% of damage
            - LUK grants chance for "lucky roll": 100% to 110% instead

        Args:
            attacker_luck: Attacker's LUK stat

        Returns:
            Variance multiplier (typically 0.95 to 1.10)

        Examples:
            LUK 8: 7% chance for lucky roll (100-110%)
            LUK 12: 8% chance for lucky roll
        """
        # Calculate lucky roll chance
        lucky_chance = (
            CombatConstants.BASE_LUCKY_CHANCE +
            (attacker_luck * CombatConstants.LUK_LUCKY_BONUS)
        )

        # Roll for lucky variance
        if random.random() < lucky_chance:
            # Lucky roll: higher damage range
            return random.uniform(
                CombatConstants.LUCKY_VARIANCE_MIN,
                CombatConstants.LUCKY_VARIANCE_MAX
            )
        else:
            # Normal roll
            return random.uniform(
                CombatConstants.BASE_VARIANCE_MIN,
                CombatConstants.BASE_VARIANCE_MAX
            )

    @staticmethod
    def apply_defense(
        raw_damage: int,
        defense: int,
        min_damage_ratio: float = CombatConstants.MIN_DAMAGE_RATIO
    ) -> int:
        """
        Apply defense mitigation with minimum damage guarantee.

        Ensures at least 20% of raw damage always goes through,
        preventing high-DEF tanks from becoming unkillable.
        Absolute minimum is always 1 damage.

        Args:
            raw_damage: Damage before defense
            defense: Target's defense value
            min_damage_ratio: Minimum % of raw damage that goes through

        Returns:
            Final damage after mitigation (minimum 1)

        Examples:
            raw=20, def=5: 20 - 5 = 15 damage
            raw=20, def=18: max(4, 2) = 4 damage (20% minimum)
            raw=10, def=100: max(2, -90) = 2 damage (20% minimum)
        """
        # Calculate mitigated damage
        mitigated = raw_damage - defense

        # Minimum damage is 20% of raw, or 1, whichever is higher
        min_damage = max(CombatConstants.MIN_DAMAGE, int(raw_damage * min_damage_ratio))

        return max(min_damage, mitigated)

    @classmethod
    def calculate_physical_damage(
        cls,
        attacker_stats: Stats,
        defender_stats: Stats,
        weapon_damage: int = 0,
        weapon_crit_chance: float = CombatConstants.DEFAULT_CRIT_CHANCE,
        weapon_crit_multiplier: float = CombatConstants.DEFAULT_CRIT_MULTIPLIER,
        ability_base_power: int = 0,
        skill_hit_bonus: float = 0.0,
        skill_crit_bonus: float = 0.0,
    ) -> DamageResult:
        """
        Calculate physical damage for a melee/ranged attack.

        Formula:
            Base = Weapon_Base + Ability_Power + (STR * 0.75)
            Damage = Base * Variance * Crit_Mult - DEF

        Args:
            attacker_stats: Attacker's Stats (STR, LUK used)
            defender_stats: Defender's Stats (DEX, CON used)
            weapon_damage: Base damage from equipped weapon
            weapon_crit_chance: Crit chance from weapon (default 5%)
            weapon_crit_multiplier: Crit damage multiplier (default 2.0x)
            ability_base_power: Additional base power from ability
            skill_hit_bonus: Hit chance bonus from skills
            skill_crit_bonus: Crit chance bonus from skills

        Returns:
            DamageResult with calculated damage and metadata
        """
        result = DamageResult(damage_type=DamageType.PHYSICAL)

        # Step 1: Check for miss
        hit_chance = cls.calculate_hit_chance(
            attacker_stats.luck,
            defender_stats.dexterity,
            skill_hit_bonus
        )

        if random.random() > hit_chance:
            result.is_miss = True
            result.message = "Attack missed!"
            return result

        # Step 2: Calculate base damage
        # Formula: weapon + ability + (STR * scaling_factor)
        str_bonus = attacker_stats.strength * CombatConstants.STAT_SCALING_FACTOR
        base_damage = weapon_damage + ability_base_power + str_bonus

        # Step 3: Apply variance
        variance = cls.calculate_variance(attacker_stats.luck)
        result.variance_roll = variance
        damage = base_damage * variance

        # Step 4: Check for critical hit
        crit_chance = cls.calculate_crit_chance(
            attacker_stats.luck,
            weapon_crit_chance,
            skill_crit_bonus
        )

        if random.random() < crit_chance:
            result.is_critical = True
            damage *= weapon_crit_multiplier

        # Store raw damage before defense
        result.raw_damage = int(damage)

        # Step 5: Apply defense mitigation
        final_damage = cls.apply_defense(int(damage), defender_stats.defense)

        result.total_damage = final_damage
        result.physical_damage = final_damage

        # Build message
        crit_text = " CRITICAL HIT!" if result.is_critical else ""
        result.message = f"Dealt {final_damage} physical damage.{crit_text}"

        return result

    @classmethod
    def calculate_magical_damage(
        cls,
        attacker_stats: Stats,
        defender_stats: Stats,
        ability_base_power: int,
        damage_type: DamageType = DamageType.FIRE,
        weapon_crit_chance: float = CombatConstants.DEFAULT_CRIT_CHANCE,
        weapon_crit_multiplier: float = CombatConstants.DEFAULT_CRIT_MULTIPLIER,
        skill_hit_bonus: float = 0.0,
        skill_crit_bonus: float = 0.0,
    ) -> DamageResult:
        """
        Calculate magical damage for a spell.

        Spells CAN critically hit (same formula as physical).
        LUK benefits all classes equally.

        Formula:
            Base = Ability_Power + (INT * 0.75)
            Damage = Base * Variance * Crit_Mult - RES

        Args:
            attacker_stats: Attacker's Stats (INT, LUK used)
            defender_stats: Defender's Stats (DEX, WIS used)
            ability_base_power: Base power of the spell
            damage_type: Type of magical damage (fire, ice, etc.)
            weapon_crit_chance: Crit chance (from focus/staff)
            weapon_crit_multiplier: Crit damage multiplier
            skill_hit_bonus: Hit chance bonus from skills
            skill_crit_bonus: Crit chance bonus from skills

        Returns:
            DamageResult with calculated damage and metadata
        """
        result = DamageResult(damage_type=damage_type)

        # Step 1: Check for miss (spells can miss too)
        hit_chance = cls.calculate_hit_chance(
            attacker_stats.luck,
            defender_stats.dexterity,
            skill_hit_bonus
        )

        if random.random() > hit_chance:
            result.is_miss = True
            result.message = "Spell missed!"
            return result

        # Step 2: Calculate base damage
        # Formula: ability + (INT * scaling_factor)
        int_bonus = attacker_stats.intelligence * CombatConstants.STAT_SCALING_FACTOR
        base_damage = ability_base_power + int_bonus

        # Step 3: Apply variance
        variance = cls.calculate_variance(attacker_stats.luck)
        result.variance_roll = variance
        damage = base_damage * variance

        # Step 4: Check for critical hit (spells CAN crit)
        crit_chance = cls.calculate_crit_chance(
            attacker_stats.luck,
            weapon_crit_chance,
            skill_crit_bonus
        )

        if random.random() < crit_chance:
            result.is_critical = True
            damage *= weapon_crit_multiplier

        # Store raw damage before resistance
        result.raw_damage = int(damage)

        # Step 5: Apply resistance mitigation
        final_damage = cls.apply_defense(int(damage), defender_stats.resistance)

        result.total_damage = final_damage
        result.elemental_damage = final_damage

        # Build message
        crit_text = " CRITICAL HIT!" if result.is_critical else ""
        result.message = f"Dealt {final_damage} {damage_type.value} damage.{crit_text}"

        return result

    @classmethod
    def calculate_elemental_weapon_damage(
        cls,
        attacker_stats: Stats,
        defender_stats: Stats,
        weapon_damage: int,
        weapon_crit_chance: float,
        weapon_crit_multiplier: float,
        physical_ratio: float,
        elemental_ratio: float,
        elemental_type: DamageType,
        ability_base_power: int = 0,
        skill_hit_bonus: float = 0.0,
        skill_crit_bonus: float = 0.0,
    ) -> DamageResult:
        """
        Calculate split damage for elemental weapons (e.g., Fire Sword).

        Elemental weapons deal both physical AND elemental damage,
        calculated separately against DEF and RES respectively.

        Formula:
            Physical = (Weapon * PHYS_RATIO + STR * 0.75 * PHYS_RATIO) - DEF
            Elemental = (Weapon * ELEM_RATIO + INT * 0.75 * ELEM_RATIO) - RES
            Total = Physical + Elemental

        Recommended Split Ratios:
            - Pure Physical: 100% / 0%
            - Fire Sword: 70% / 30%
            - Frost Blade: 60% / 40%
            - Lightning Spear: 50% / 50%

        Args:
            attacker_stats: Attacker's Stats
            defender_stats: Defender's Stats
            weapon_damage: Base weapon damage
            weapon_crit_chance: Crit chance from weapon
            weapon_crit_multiplier: Crit damage multiplier
            physical_ratio: Portion of damage that is physical (0.0-1.0)
            elemental_ratio: Portion of damage that is elemental (0.0-1.0)
            elemental_type: Type of elemental damage
            ability_base_power: Additional base power from ability
            skill_hit_bonus: Hit chance bonus from skills
            skill_crit_bonus: Crit chance bonus from skills

        Returns:
            DamageResult with split physical/elemental damage
        """
        result = DamageResult(
            damage_type=DamageType.PHYSICAL,
            elemental_type=elemental_type
        )

        # Step 1: Check for miss (single roll for entire attack)
        hit_chance = cls.calculate_hit_chance(
            attacker_stats.luck,
            defender_stats.dexterity,
            skill_hit_bonus
        )

        if random.random() > hit_chance:
            result.is_miss = True
            result.message = "Attack missed!"
            return result

        # Step 2: Check for critical (single roll applies to both components)
        variance = cls.calculate_variance(attacker_stats.luck)
        result.variance_roll = variance

        crit_chance = cls.calculate_crit_chance(
            attacker_stats.luck,
            weapon_crit_chance,
            skill_crit_bonus
        )
        is_crit = random.random() < crit_chance
        result.is_critical = is_crit
        crit_mult = weapon_crit_multiplier if is_crit else 1.0

        # Step 3: Calculate physical component
        # Physical uses STR scaling
        phys_base = (weapon_damage + ability_base_power) * physical_ratio
        str_bonus = attacker_stats.strength * CombatConstants.STAT_SCALING_FACTOR * physical_ratio
        phys_damage = (phys_base + str_bonus) * variance * crit_mult
        phys_final = cls.apply_defense(int(phys_damage), defender_stats.defense)

        # Step 4: Calculate elemental component
        # Elemental uses INT scaling
        elem_base = (weapon_damage + ability_base_power) * elemental_ratio
        int_bonus = attacker_stats.intelligence * CombatConstants.STAT_SCALING_FACTOR * elemental_ratio
        elem_damage = (elem_base + int_bonus) * variance * crit_mult
        elem_final = cls.apply_defense(int(elem_damage), defender_stats.resistance)

        # Step 5: Combine results
        result.physical_damage = phys_final
        result.elemental_damage = elem_final
        result.total_damage = phys_final + elem_final
        result.raw_damage = int(phys_damage + elem_damage)

        # Build message
        crit_text = " CRITICAL HIT!" if is_crit else ""
        result.message = (
            f"Dealt {result.total_damage} damage "
            f"({phys_final} physical + {elem_final} {elemental_type.value}).{crit_text}"
        )

        return result

    @classmethod
    def calculate_aoe_damage(
        cls,
        attacker_stats: Stats,
        defender_stats_list: List[Stats],
        ability_base_power: int,
        damage_type: DamageType = DamageType.FIRE,
        weapon_crit_chance: float = CombatConstants.DEFAULT_CRIT_CHANCE,
        weapon_crit_multiplier: float = CombatConstants.DEFAULT_CRIT_MULTIPLIER,
        skill_hit_bonus: float = 0.0,
        skill_crit_bonus: float = 0.0,
    ) -> List[DamageResult]:
        """
        Calculate AoE spell damage against multiple targets.

        AoE spells deal FULL damage to all targets (balanced by higher mana costs).
        Each target has independent hit/crit rolls but shares the base calculation.

        Args:
            attacker_stats: Attacker's Stats
            defender_stats_list: List of defender Stats (one per target)
            ability_base_power: Base power of the AoE spell
            damage_type: Type of magical damage
            weapon_crit_chance: Crit chance from focus/staff
            weapon_crit_multiplier: Crit damage multiplier
            skill_hit_bonus: Hit chance bonus from skills
            skill_crit_bonus: Crit chance bonus from skills

        Returns:
            List of DamageResult, one per target
        """
        results = []

        # Each target gets independent damage calculation
        for defender_stats in defender_stats_list:
            result = cls.calculate_magical_damage(
                attacker_stats=attacker_stats,
                defender_stats=defender_stats,
                ability_base_power=ability_base_power,
                damage_type=damage_type,
                weapon_crit_chance=weapon_crit_chance,
                weapon_crit_multiplier=weapon_crit_multiplier,
                skill_hit_bonus=skill_hit_bonus,
                skill_crit_bonus=skill_crit_bonus,
            )
            results.append(result)

        return results
