# boss_ai.py : IA des boss Vexaris et Morveth
# l'etat interne du boss est stocke dans enemy.data (pas broadcast au client)

import logging

from buff_system import player_is_immune
from combat import circle_overlap, normalize
from constants import (
    ARENA_HEIGHT, ARENA_WIDTH,
    ENEMY_ATTACK_COOLDOWN,
    MORVETH_BURST_COOLDOWN, MORVETH_BURST_COUNT,
    MORVETH_CHARGE_COOLDOWN, MORVETH_CHARGE_DURATION, MORVETH_CHARGE_SPEED,
    MORVETH_HITBOX_RADIUS, MORVETH_MELEE_DAMAGE, MORVETH_SG_DAMAGE,
    MORVETH_SPEED_P1, MORVETH_SPEED_P2, MORVETH_SPEED_P3,
    PLAYER_HITBOX_RADIUS,
    SOULGATE_HITBOX_RADIUS, SOULGATE_X, SOULGATE_Y,
    TICK_DURATION,
    VEXARIS_BURST_COOLDOWN, VEXARIS_BURST_COUNT,
    VEXARIS_CHARGE_COOLDOWN, VEXARIS_CHARGE_DURATION, VEXARIS_CHARGE_SPEED,
    VEXARIS_HITBOX_RADIUS, VEXARIS_MELEE_DAMAGE, VEXARIS_SG_DAMAGE,
    VEXARIS_SPEED_P1, VEXARIS_SPEED_P2,
)

logger = logging.getLogger(__name__)


def nearest_player(ex, ey, players):
    # joueur le plus proche de (ex, ey), None si liste vide
    best = None
    best_dist = float("inf")
    for p in players:
        d = (p.x - ex) ** 2 + (p.y - ey) ** 2  # carre, evite math.sqrt
        if d < best_dist:
            best_dist = d
            best = p
    return best


def update_boss_ai(enemy, state_players, state_soulgate, pending_spawns):
    """dispatch vers la bonne IA selon le type de boss.

    pending_spawns = liste de (type_ennemi, x, y) à créer.
    On n'appelle pas spawn_enemy() directement depuis ici pour éviter de modifier
    la liste d'ennemis pendant qu'on l'itère dans game_loop.
    """
    alive = [p for p in state_players if p.alive]   # uniquement les joueurs vivants

    if enemy.type == "morveth":
        _update_morveth(enemy, alive, state_soulgate, pending_spawns)
    else:
        # Vexaris ou Général (les généraux ont une IA simplifiée du même type)
        _update_vexaris(enemy, alive, state_soulgate, pending_spawns)


# Vexaris

def _update_vexaris(enemy, alive_players, soulgate, pending_spawns):
    # 2 phases (>50% / <=50%), charge sur joueur, burst d'eclats en phase 2
    phase2 = enemy.hp <= enemy.max_hp // 2
    speed = VEXARIS_SPEED_P2 if phase2 else VEXARIS_SPEED_P1
    d = enemy.data

    _init_vexaris_data(d)
    _tick_boss_cooldowns(enemy, d, phase2, VEXARIS_BURST_COOLDOWN)
    _check_soulgate_contact(enemy, soulgate, VEXARIS_HITBOX_RADIUS, VEXARIS_SG_DAMAGE)
    _vexaris_burst(d, enemy, phase2, pending_spawns)

    if d["is_charging"]:
        _boss_charge_move(enemy, d, alive_players, VEXARIS_CHARGE_SPEED, VEXARIS_HITBOX_RADIUS, VEXARIS_MELEE_DAMAGE * 2)
    elif d["charge_cd"] <= 0 and alive_players:
        _start_charge(d, enemy, alive_players, VEXARIS_CHARGE_DURATION, VEXARIS_CHARGE_COOLDOWN, "Vexaris")
    else:
        _boss_normal_move(enemy, alive_players, speed, VEXARIS_HITBOX_RADIUS, VEXARIS_MELEE_DAMAGE)


def _init_vexaris_data(d):
    if "charge_cd" in d:
        return
    d.update({
        "charge_cd": VEXARIS_CHARGE_COOLDOWN,
        "charge_timer": 0.0,
        "charge_tx": 0.0, "charge_ty": 0.0,
        "is_charging": False,
        "burst_cd": VEXARIS_BURST_COOLDOWN,
    })


def _vexaris_burst(d, enemy, phase2, pending):
    """Spawn un burst d'éclats autour de Vexaris (seulement en phase 2, si CD = 0)."""
    if not phase2 or d["burst_cd"] > 0:
        return   # pas en phase 2, ou burst en cooldown → on ne fait rien

    # Spawner VEXARIS_BURST_COUNT éclats à la position du boss
    for _ in range(VEXARIS_BURST_COUNT):
        pending.append(("eclat", enemy.x, enemy.y))   # ("type", x, y) — game_loop les crée après

    d["burst_cd"] = VEXARIS_BURST_COOLDOWN   # réinitialiser le cooldown du burst
    logger.debug("Vexaris burst : %d eclats", VEXARIS_BURST_COUNT)


# Morveth

def _update_morveth(enemy, alive_players, soulgate, pending_spawns):
    # 3 phases : <75% (p2), <25% (p3), invoque des generaux a 75/50/25
    hp_pct = enemy.hp / enemy.max_hp
    phase3 = hp_pct <= 0.25
    phase2 = hp_pct <= 0.75

    speed = MORVETH_SPEED_P3 if phase3 else (MORVETH_SPEED_P2 if phase2 else MORVETH_SPEED_P1)
    d = enemy.data

    _init_morveth_data(d)
    _tick_boss_cooldowns(enemy, d, phase2, MORVETH_BURST_COOLDOWN)
    _morveth_spawn_generals(d, hp_pct, enemy, pending_spawns)   # invoquer généraux aux seuils HP
    _check_soulgate_contact(enemy, soulgate, MORVETH_HITBOX_RADIUS, MORVETH_SG_DAMAGE)
    _morveth_burst(d, enemy, phase2, phase3, pending_spawns)

    # En phase 3, le cooldown de charge est divisé par 2 → il charge 2× plus souvent
    charge_reset = MORVETH_CHARGE_COOLDOWN / 2 if phase3 else MORVETH_CHARGE_COOLDOWN

    if d["is_charging"]:
        _boss_charge_move(enemy, d, alive_players, MORVETH_CHARGE_SPEED, MORVETH_HITBOX_RADIUS, MORVETH_MELEE_DAMAGE * 2)
    elif d["charge_cd"] <= 0 and alive_players:
        _start_charge(d, enemy, alive_players, MORVETH_CHARGE_DURATION, charge_reset, "Morveth")
    else:
        _boss_normal_move(enemy, alive_players, speed, MORVETH_HITBOX_RADIUS, MORVETH_MELEE_DAMAGE)


def _init_morveth_data(d):
    # init des champs internes de Morveth, fait 1 seule fois
    if "charge_cd" in d:
        return
    d.update({
        "charge_cd": MORVETH_CHARGE_COOLDOWN,
        "charge_timer": 0.0,
        "charge_tx": 0.0, "charge_ty": 0.0,
        "is_charging": False,
        "burst_cd": MORVETH_BURST_COOLDOWN,
        "general_1_spawned": False,   # généraux déjà invoqués ? (évite de les respawner)
        "general_2_spawned": False,
        "general_3_spawned": False,
    })


def _morveth_spawn_generals(d, hp_pct, enemy, pending):
    """Invoque un Général à chaque seuil HP (une seule fois par seuil).

    hp_pct = pourcentage de HP de Morveth (0.0 à 1.0).
    Les seuils : 75%, 50%, 25%.
    Le flag (ex: "general_1_spawned") empêche de respawner si Morveth repasse sous le seuil.
    """
    thresholds = [
        # (seuil HP, flag dans d, offset X spawn, offset Y spawn)
        (0.75, "general_1_spawned", -3, 0),   # général 1 à gauche de Morveth
        (0.50, "general_2_spawned",  3, 0),   # général 2 à droite
        (0.25, "general_3_spawned",  0, 2),   # général 3 devant
    ]
    for pct, flag, dx, dy in thresholds:
        if hp_pct <= pct and not d[flag]:   # seuil atteint ET pas encore spawné
            pending.append(("general", enemy.x + dx, enemy.y + dy))   # ajouter à la file
            d[flag] = True   # marquer comme spawné pour ne pas recommencer
            logger.info("Morveth invoque General (%s)", flag)


def _morveth_burst(d, enemy, phase2, phase3, pending):
    """Burst d'éclats de Morveth. En phase 3, le cooldown est divisé par 2."""
    if not phase2 or d["burst_cd"] > 0:
        return

    for _ in range(MORVETH_BURST_COUNT):
        pending.append(("eclat", enemy.x, enemy.y))

    # Phase 3 = rage → burst 2× plus fréquent
    d["burst_cd"] = MORVETH_BURST_COOLDOWN / 2 if phase3 else MORVETH_BURST_COOLDOWN
    logger.debug("Morveth burst : %d eclats", MORVETH_BURST_COUNT)


# helpers partages Vexaris/Morveth

def _tick_boss_cooldowns(enemy, d, phase2, burst_cd_const):
    if enemy.attack_cooldown > 0:
        enemy.attack_cooldown = max(0.0, enemy.attack_cooldown - TICK_DURATION)
    d["charge_cd"] = max(0.0, d["charge_cd"] - TICK_DURATION)
    if phase2:
        d["burst_cd"] = max(0.0, d["burst_cd"] - TICK_DURATION)


def _check_soulgate_contact(enemy, soulgate, hitbox, damage):
    sg_x, sg_y = float(SOULGATE_X), float(SOULGATE_Y)
    if not circle_overlap(enemy.x, enemy.y, hitbox, sg_x, sg_y, float(SOULGATE_HITBOX_RADIUS)):
        return
    if enemy.attack_cooldown > 0:
        return
    soulgate.hp = max(0, soulgate.hp - damage)
    enemy.attack_cooldown = ENEMY_ATTACK_COOLDOWN


def _start_charge(d, enemy, alive, duration, cooldown, name):
    # la cible est fixee au declenchement (le joueur peut bouger pendant la charge)
    target = nearest_player(enemy.x, enemy.y, alive)
    if target is None:
        return
    d["is_charging"] = True
    d["charge_timer"] = duration
    d["charge_tx"] = target.x
    d["charge_ty"] = target.y
    d["charge_cd"] = cooldown
    logger.debug("%s charge vers %s", name, target.username)


def _boss_charge_move(enemy, d, alive_players, charge_speed, hitbox, charge_damage):
    d["charge_timer"] -= TICK_DURATION
    if d["charge_timer"] <= 0.0:
        d["is_charging"] = False
        return

    # direction vers la cible figee au declenchement (le joueur peut bouger pendant la charge)
    dx, dy = normalize(d["charge_tx"] - enemy.x, d["charge_ty"] - enemy.y)

    if dx != 0.0 or dy != 0.0:
        half_w, half_h = ARENA_WIDTH / 2, ARENA_HEIGHT / 2
        enemy.x = max(-half_w, min(half_w, enemy.x + dx * charge_speed * TICK_DURATION))
        enemy.y = max(-half_h, min(half_h, enemy.y + dy * charge_speed * TICK_DURATION))

    # check collision avec chaque joueur vivant non immunise
    for player in alive_players:
        if not circle_overlap(enemy.x, enemy.y, hitbox, player.x, player.y, float(PLAYER_HITBOX_RADIUS)):
            continue
        if player.alive and not player_is_immune(player):
            player.hp = max(0, player.hp - charge_damage)
            if player.hp <= 0:
                player.alive = False


def _boss_normal_move(enemy, alive_players, speed, hitbox, melee_damage):
    if not alive_players:
        return
    target = nearest_player(enemy.x, enemy.y, alive_players)
    if target is None:
        return

    dx, dy = normalize(target.x - enemy.x, target.y - enemy.y)
    enemy.x += dx * speed * TICK_DURATION
    enemy.y += dy * speed * TICK_DURATION

    if not circle_overlap(enemy.x, enemy.y, hitbox, target.x, target.y, float(PLAYER_HITBOX_RADIUS)):
        return
    if enemy.attack_cooldown > 0 or player_is_immune(target):
        return

    target.hp = max(0, target.hp - melee_damage)
    if target.hp <= 0:
        target.alive = False
    enemy.attack_cooldown = ENEMY_ATTACK_COOLDOWN
