"""
Procedural Monster Generation Engine for FreeExile.
Generates 10,000 deterministic monster variants across 100 levels.
Computes size scaling, ferocity profiles, stats, and modular GPU visual bindings.
"""

import math
from dataclasses import replace
from typing import Dict, List, Optional, Tuple
try:
    from server.world.martial_matrix import FiveElements
    from server.world.monster_types import (
        MonsterGenus,
        MonsterRank,
        AggressionProfile,
        MonsterVisualProfile,
        ProceduralMonster,
        PackAuraType,
        MonsterPack,
        MonsterAffix,
        MonsterRuntimeState,
    )
    from server.world.monster_archetype_catalog import (
        CORE_GENUS_SPECS,
        SUB_SPECIES_CATALOG,
        PREFIX_AFFIXES,
        SUFFIX_AFFIXES,
        ELEMENT_PALETTES,
        MODULAR_ATTACHMENTS,
    )
    from server.world.monster_affix_catalog import (
        roll_monster_affixes,
        apply_affixes_to_stats,
    )
except ImportError:
    from world.martial_matrix import FiveElements
    from world.monster_types import (
        MonsterGenus,
        MonsterRank,
        AggressionProfile,
        MonsterVisualProfile,
        ProceduralMonster,
        PackAuraType,
        MonsterPack,
        MonsterAffix,
        MonsterRuntimeState,
    )
    from world.monster_archetype_catalog import (
        CORE_GENUS_SPECS,
        SUB_SPECIES_CATALOG,
        PREFIX_AFFIXES,
        SUFFIX_AFFIXES,
        ELEMENT_PALETTES,
        MODULAR_ATTACHMENTS,
    )
    from world.monster_affix_catalog import (
        roll_monster_affixes,
        apply_affixes_to_stats,
    )



class MonsterProceduralEngine:
    """Server-Authoritative Procedural Monster Generator (Deterministic $O(1)$)."""

    def __init__(self) -> None:
        self._genus_list = list(MonsterGenus)
        self._element_list = list(FiveElements)

    def _hash_seed(self, seed: int, salt: int) -> int:
        """Lightweight deterministic integer hashing (splitmix64-inspired)."""
        z = (seed + salt * 0x9E3779B9) & 0xFFFFFFFF
        z = ((z ^ (z >> 16)) * 0x45D9F3B) & 0xFFFFFFFF
        z = ((z ^ (z >> 16)) * 0x45D9F3B) & 0xFFFFFFFF
        return (z ^ (z >> 16)) & 0xFFFFFFFF

    def _determine_rank(self, seed: int) -> MonsterRank:
        val = self._hash_seed(seed, 101) % 100
        if val < 65:
            return MonsterRank.COMMON
        elif val < 85:
            return MonsterRank.MAGIC
        elif val < 97:
            return MonsterRank.RARE
        return MonsterRank.BOSS

    def _calc_rank_modifiers(self, rank: MonsterRank) -> tuple[float, float, float, float, float, float]:
        if rank == MonsterRank.COMMON:
            return (1.0, 1.0, 1.0, 0.0, 0.0, 0.0)
        elif rank == MonsterRank.MAGIC:
            return (1.25, 2.2, 1.3, 0.08, 2.0, 0.3)
        elif rank == MonsterRank.RARE:
            return (1.55, 4.5, 1.6, 0.15, 4.0, 0.6)
        else:  # BOSS
            return (2.20, 12.0, 2.4, 0.25, 8.0, 1.2)

    def generate_monster(
        self,
        monster_id: int,
        target_level: Optional[int] = None,
        forced_rank: Optional[MonsterRank] = None,
        allowed_genus: Optional[List[MonsterGenus]] = None,
    ) -> ProceduralMonster:
        """Generates a single deterministic monster by ID (1 .. 10,000)."""
        if monster_id < 1:
            monster_id = 1

        level = target_level if target_level is not None else min(100, max(1, ((monster_id - 1) // 100) + 1))
        genus_pool = allowed_genus if allowed_genus else self._genus_list
        genus_idx = self._hash_seed(monster_id, 11) % len(genus_pool)
        genus = genus_pool[genus_idx]
        base_name, base_hp, base_dmg, base_scale, native_elem = CORE_GENUS_SPECS[genus]

        # Sub-species & Affixes
        sub_list = SUB_SPECIES_CATALOG[genus]
        sub_code, sub_title = sub_list[self._hash_seed(monster_id, 23) % len(sub_list)]
        prefix_title, p_hp_mod, p_dmg_mod, _ = PREFIX_AFFIXES[self._hash_seed(monster_id, 37) % len(PREFIX_AFFIXES)]
        suffix_title, _ = SUFFIX_AFFIXES[self._hash_seed(monster_id, 47) % len(SUFFIX_AFFIXES)]

        # Elemental affinity & Rank
        element = self._element_list[self._hash_seed(monster_id, 59) % len(self._element_list)]
        rank = forced_rank if forced_rank is not None else self._determine_rank(monster_id)
        rank_scale_m, rank_hp_m, rank_dmg_m, rank_fero_b, rank_aggro_b, rank_spd_b = self._calc_rank_modifiers(rank)

        # Mathematical curves for 100 Levels
        lvl_ratio = level / 100.0
        final_scale = round(base_scale * (0.80 + 0.50 * (lvl_ratio ** 0.75)) * rank_scale_m, 2)
        collision_r = round(0.50 * final_scale, 2)

        # Aggression calculations
        ferocity = min(1.0, round(0.20 + 0.55 * (lvl_ratio ** 0.85) + rank_fero_b, 3))
        aggro_radius = round(5.0 + 15.0 * (lvl_ratio ** 0.90) + rank_aggro_b, 1)
        movement_spd = round(2.2 + 4.5 * (lvl_ratio ** 0.80) + rank_spd_b, 2)
        windup_ms = max(180, int(1800 - 1500 * (lvl_ratio ** 0.75)))
        cooldown_ms = max(350, int(2500 - 2000 * (lvl_ratio ** 0.80)))
        cohesion = min(1.0, round(0.1 + 0.8 * lvl_ratio, 2))
        combo_hits = 1 + (1 if level >= 30 else 0) + (1 if level >= 70 else 0) + (1 if rank == MonsterRank.BOSS else 0)

        aggression = AggressionProfile(
            ferocity_index=ferocity,
            aggro_radius_meters=aggro_radius,
            leash_radius_meters=round(aggro_radius * 2.2, 1),
            movement_speed=movement_spd,
            attack_windup_ms=windup_ms,
            attack_cooldown_ms=cooldown_ms,
            flocking_cohesion=cohesion,
            combo_hits=combo_hits,
        )

        # Visual profile (GPU Palette LUT & Modular Attachments)
        tint_hex, glow_hex, lut_idx = ELEMENT_PALETTES[element]
        num_attachments = 0 if rank == MonsterRank.COMMON else (1 if rank == MonsterRank.MAGIC else (2 if rank == MonsterRank.RARE else 3))
        selected_attachments: List[str] = []
        for i in range(num_attachments):
            att_idx = self._hash_seed(monster_id, 71 + i) % len(MODULAR_ATTACHMENTS)
            att_id = MODULAR_ATTACHMENTS[att_idx]
            if att_id not in selected_attachments:
                selected_attachments.append(att_id)

        visual = MonsterVisualProfile(
            base_scale=final_scale,
            collision_radius=collision_r,
            elemental_tint_hex=tint_hex,
            emissive_glow_intensity=round(0.2 * (lut_idx + (1.0 if rank != MonsterRank.COMMON else 0.0)), 2),
            attachment_ids=selected_attachments,
            palette_lut_index=lut_idx,
        )

        # Stats curves (Calibrated: level 1 starter mobs deal ~5.0 - 6.5 dmg for player survivability)
        lvl_dmg_mult = 0.32 if level == 1 else ((1.0 + 0.12 * level) ** 1.8)
        lvl_hp_mult = 0.65 if level == 1 else ((1.0 + 0.18 * level) ** 2.1)
        max_hp = round(base_hp * lvl_hp_mult * rank_hp_m * p_hp_mod, 1)
        base_dmg_val = round(base_dmg * lvl_dmg_mult * rank_dmg_m * p_dmg_mod, 1)
        armor_val = round(max_hp * 0.12, 1)
        full_name = f"{prefix_title} {sub_title} · {suffix_title}"

        # 15-Tier Procedural Affixes
        affixes = roll_monster_affixes(level=level, rank=rank, seed=monster_id)
        if affixes:
            max_hp, base_dmg_val, armor_val = apply_affixes_to_stats(max_hp, base_dmg_val, armor_val, affixes)

        return ProceduralMonster(
            monster_id=monster_id,
            level=level,
            name=full_name,
            genus=genus,
            sub_species_code=sub_code,
            element=element,
            rank=rank,
            prefix=prefix_title,
            suffix=suffix_title,
            max_hp=max_hp,
            base_damage=base_dmg_val,
            armor=armor_val,
            aggression=aggression,
            visual=visual,
            experience_reward=int(level * 25 * rank_hp_m),
            loot_tier=min(16, max(1, (level // 6) + (1 if rank != MonsterRank.COMMON else 0))),
            affixes=affixes,
        )

    def generate_pack(
        self,
        pack_id: str,
        zone_id: str,
        level: int,
        center_x: float,
        center_y: float,
        seed: int,
        minion_count: Optional[int] = None,
        allowed_genus: Optional[List[MonsterGenus]] = None,
    ) -> MonsterPack:
        """
        Generates a coordinated monster pack with a Rare pack leader and radial minions (1.2m to 3.2m).
        """
        leader_id = (self._hash_seed(seed, 1001) % 9000) + 1000
        leader = self.generate_monster(
            leader_id,
            target_level=level,
            forced_rank=MonsterRank.RARE,
            allowed_genus=allowed_genus,
        )
        aura_list = list(PackAuraType)
        chosen_aura = aura_list[self._hash_seed(seed, 2002) % len(aura_list)]
        leader_affixes = roll_monster_affixes(level, MonsterRank.RARE, self._hash_seed(seed, 2505))
        leader = replace(
            leader,
            pack_id=pack_id,
            is_pack_leader=True,
            leader_aura=chosen_aura,
            rank=MonsterRank.RARE,
            affixes=leader_affixes,
        )

        count = minion_count if minion_count is not None else 3 + (self._hash_seed(seed, 3003) % 4)
        minions: List[ProceduralMonster] = []

        for i in range(count):
            m_id = (self._hash_seed(seed, 4004 + i) % 9000) + 1000
            m = self.generate_monster(
                m_id,
                target_level=level,
                forced_rank=MonsterRank.COMMON,
                allowed_genus=allowed_genus,
            )
            m = replace(
                m,
                pack_id=pack_id,
                is_pack_leader=False,
                pack_leader_id=leader.monster_id,
            )
            minions.append(m)

        return MonsterPack(pack_id, zone_id, leader, minions, center_x, center_y)

    def create_pack_runtime_states(
        self,
        pack: MonsterPack,
        seed: int = 12345
    ) -> List[MonsterRuntimeState]:
        """Instantiates runtime states with radial positioning around the pack leader."""
        states: List[MonsterRuntimeState] = []
        leader_st = MonsterRuntimeState(
            monster=pack.leader,
            current_x=pack.center_x,
            current_y=pack.center_y,
            spawn_x=pack.center_x,
            spawn_y=pack.center_y,
            current_hp=pack.leader.max_hp,
            pack_id=pack.pack_id,
            is_pack_leader=True,
            leader_aura=pack.leader.leader_aura,
            affixes=pack.leader.affixes,
        )
        states.append(leader_st)

        count = len(pack.minions)
        for i, minion in enumerate(pack.minions):
            angle = (2.0 * math.pi * i) / max(1, count) + (self._hash_seed(seed, 5005 + i) % 100) * 0.005
            dist = 1.2 + (self._hash_seed(seed, 6006 + i) % 200) * 0.01
            m_x = pack.center_x + dist * math.cos(angle)
            m_y = pack.center_y + dist * math.sin(angle)
            m_st = MonsterRuntimeState(
                monster=minion,
                current_x=m_x,
                current_y=m_y,
                spawn_x=m_x,
                spawn_y=m_y,
                current_hp=minion.max_hp,
                pack_id=pack.pack_id,
                is_pack_leader=False,
                pack_leader_id=pack.leader.monster_id,
                affixes=minion.affixes,
            )
            states.append(m_st)
        return states

    def generate_batch(self, start_id: int = 1, count: int = 10000) -> List[ProceduralMonster]:
        """Generates a batch of monsters efficiently."""
        return [self.generate_monster(mid) for mid in range(start_id, start_id + count)]
