"""
FreeExile Party Service.
Authoritative lifecycle management for 6-player parties:
- Creation, invites, accept/decline, leader promotion, leave with auto-promotion, kick, disband.
- Configurable loot allocation modes (Leader only).
- Thread-safe concurrency via RLock.
- Spatial nearby membership queries.

Follows Engineering Standards 2026: Soft cap <= 350 lines, strict typing, zero-trust server authority.
"""

from __future__ import annotations
import math
import threading
import time
import uuid
from typing import Dict, List, Optional, Set

from .party_types import (
    LootAllocationMode,
    PartyAction,
    PartyActionResult,
    PartyInvite,
    PartyMemberState,
    PartyRole,
    PartyState,
)
from .party_scaling_calculator import PartyScalingCalculator


class PartyService:
    """Authoritative service managing 6-player party sessions and membership states."""

    MAX_PARTY_SIZE: int = 6
    INVITE_TTL_MS: int = 30000  # 30 seconds

    def __init__(self) -> None:
        self._lock = threading.RLock()
        self.parties: Dict[str, PartyState] = {}
        self.player_party_map: Dict[str, str] = {}
        self.pending_invites: Dict[str, Dict[str, PartyInvite]] = {}

    def create_party(
        self,
        leader_id: str,
        name: str,
        level: int,
        zone_id: str = "zone_boundless_sanctuary",
        instance_id: str = "inst_1",
    ) -> PartyState:
        """Initializes a new party with creator as Party Leader."""
        with self._lock:
            if leader_id in self.player_party_map:
                raise ValueError(f"Player {leader_id} is already in a party")

            party_id = f"party_{leader_id}_{int(time.time() * 1000)}"
            leader_member = PartyMemberState(
                player_id=leader_id,
                character_name=name,
                level=level,
                zone_id=zone_id,
                instance_id=instance_id,
                wx=0.0,
                wy=0.0,
                role=PartyRole.LEADER,
                is_online=True,
            )
            party = PartyState(
                party_id=party_id,
                leader_id=leader_id,
                members={leader_id: leader_member},
                loot_mode=LootAllocationMode.SHORT,
                max_members=self.MAX_PARTY_SIZE,
                created_at=time.time(),
            )
            self.parties[party_id] = party
            self.player_party_map[leader_id] = party_id
            self.pending_invites[party_id] = {}
            return party

    def invite_player(self, party_id: str, sender_id: str, target_id: str) -> bool:
        """Leader invites a target player to the party (Max 6 members enforced)."""
        with self._lock:
            party = self.parties.get(party_id)
            if not party or party.leader_id != sender_id:
                return False
            if len(party.members) >= party.max_members or target_id in self.player_party_map:
                return False

            now_ms = int(time.time() * 1000)
            inv = PartyInvite(
                invite_id=f"inv_{uuid.uuid4().hex[:8]}",
                party_id=party_id,
                inviter_id=sender_id,
                inviter_name=party.members[sender_id].character_name if sender_id in party.members else "Leader",
                target_player_id=target_id,
                created_at_ms=now_ms,
                expires_at_ms=now_ms + self.INVITE_TTL_MS,
            )
            self.pending_invites.setdefault(party_id, {})[target_id] = inv
            party.pending_invites[target_id] = inv
            return True

    def accept_invite(
        self,
        party_id: str,
        player_id: str,
        name: str,
        level: int,
        zone_id: str = "zone_boundless_sanctuary",
        instance_id: str = "inst_1",
    ) -> bool:
        """Target player accepts an outstanding invitation and enters the party."""
        with self._lock:
            party = self.parties.get(party_id)
            party_invs = self.pending_invites.get(party_id, {})
            inv = party_invs.get(player_id)
            if not party or not inv:
                return False
            if inv.is_expired():
                party_invs.pop(player_id, None)
                party.pending_invites.pop(player_id, None)
                return False
            if len(party.members) >= party.max_members or player_id in self.player_party_map:
                return False

            party_invs.pop(player_id, None)
            party.pending_invites.pop(player_id, None)

            member = PartyMemberState(
                player_id=player_id,
                character_name=name,
                level=level,
                zone_id=zone_id,
                instance_id=instance_id,
                wx=0.0,
                wy=0.0,
                role=PartyRole.MEMBER,
                is_online=True,
            )
            party.members[player_id] = member
            self.player_party_map[player_id] = party_id
            return True

    def decline_invite(self, party_id: str, player_id: str) -> bool:
        """Target player declines an invitation."""
        with self._lock:
            party = self.parties.get(party_id)
            party_invs = self.pending_invites.get(party_id, {})
            if player_id in party_invs:
                party_invs.pop(player_id, None)
                if party:
                    party.pending_invites.pop(player_id, None)
                return True
            return False

    def promote_leader(self, party_id: str, current_leader_id: str, new_leader_id: str) -> bool:
        """Transfers leadership to another existing member."""
        with self._lock:
            party = self.parties.get(party_id)
            if not party or party.leader_id != current_leader_id or new_leader_id not in party.members:
                return False

            old_leader = party.members[current_leader_id]
            party.members[current_leader_id] = PartyMemberState(
                player_id=old_leader.player_id,
                character_name=old_leader.character_name,
                level=old_leader.level,
                zone_id=old_leader.zone_id,
                instance_id=old_leader.instance_id,
                wx=old_leader.wx,
                wy=old_leader.wy,
                role=PartyRole.MEMBER,
                is_online=old_leader.is_online,
                hp=old_leader.hp,
                max_hp=old_leader.max_hp,
            )

            target = party.members[new_leader_id]
            party.members[new_leader_id] = PartyMemberState(
                player_id=target.player_id,
                character_name=target.character_name,
                level=target.level,
                zone_id=target.zone_id,
                instance_id=target.instance_id,
                wx=target.wx,
                wy=target.wy,
                role=PartyRole.LEADER,
                is_online=target.is_online,
                hp=target.hp,
                max_hp=target.max_hp,
            )
            party.leader_id = new_leader_id
            return True

    def leave_party(self, player_id: str) -> Optional[PartyState]:
        """Player leaves party; auto-promotes senior member if leaving player was leader."""
        with self._lock:
            party_id = self.player_party_map.pop(player_id, None)
            if not party_id or party_id not in self.parties:
                return None

            party = self.parties[party_id]
            party.members.pop(player_id, None)

            if not party.members:
                del self.parties[party_id]
                self.pending_invites.pop(party_id, None)
                return None

            if party.leader_id == player_id:
                next_leader_id = next(iter(party.members.keys()))
                party.leader_id = next_leader_id
                target = party.members[next_leader_id]
                party.members[next_leader_id] = PartyMemberState(
                    player_id=target.player_id,
                    character_name=target.character_name,
                    level=target.level,
                    zone_id=target.zone_id,
                    instance_id=target.instance_id,
                    wx=target.wx,
                    wy=target.wy,
                    role=PartyRole.LEADER,
                    is_online=target.is_online,
                    hp=target.hp,
                    max_hp=target.max_hp,
                )
            return party

    def kick_member(self, party_id: str, leader_id: str, target_id: str) -> bool:
        """Leader expels a member from the party."""
        with self._lock:
            party = self.parties.get(party_id)
            if not party or party.leader_id != leader_id:
                return False
            if target_id not in party.members or target_id == leader_id:
                return False

            del party.members[target_id]
            self.player_party_map.pop(target_id, None)
            return True

    def disband_party(self, party_id: str, leader_id: str) -> bool:
        """Leader dissolves the party entirely."""
        with self._lock:
            party = self.parties.get(party_id)
            if not party or party.leader_id != leader_id:
                return False

            for pid in list(party.members.keys()):
                self.player_party_map.pop(pid, None)

            del self.parties[party_id]
            self.pending_invites.pop(party_id, None)
            return True

    def set_loot_allocation_mode(self, party_id: str, leader_id: str, mode: LootAllocationMode) -> bool:
        """Sets loot allocation mode (Permanent, Short 5.0s, FFA). Leader only."""
        with self._lock:
            party = self.parties.get(party_id)
            if not party or party.leader_id != leader_id:
                return False
            party.loot_mode = mode
            return True

    def get_nearby_members(self, player_id: str, radius: float = 15.0) -> List[PartyMemberState]:
        """Retrieves party members within radius of the specified player."""
        with self._lock:
            party_id = self.player_party_map.get(player_id)
            if not party_id or party_id not in self.parties:
                return []
            party = self.parties[party_id]
            ref = party.members.get(player_id)
            if not ref:
                return []
            ref_x = getattr(ref, "wx", getattr(ref, "x", 0.0))
            ref_y = getattr(ref, "wy", getattr(ref, "y", 0.0))
            return PartyScalingCalculator.calculate_nearby_members(
                members=list(party.members.values()),
                ref_wx=ref_x,
                ref_wy=ref_y,
                ref_zone=ref.zone_id,
                ref_inst=ref.instance_id or "inst_1",
                radius=radius,
            )

    def update_member_position(
        self, player_id: str, wx: float, wy: float, zone_id: Optional[str] = None, instance_id: Optional[str] = None
    ) -> bool:
        """Updates a member's position and map instance."""
        with self._lock:
            party_id = self.player_party_map.get(player_id)
            if not party_id or party_id not in self.parties:
                return False
            party = self.parties[party_id]
            member = party.members.get(player_id)
            if not member:
                return False
            party.members[player_id] = member.with_position(wx, wy, zone_id, instance_id)
            return True

    def update_member_vitals(self, player_id: str, hp: float, max_hp: float, is_online: Optional[bool] = None) -> bool:
        """Updates a member's health and online status."""
        with self._lock:
            party_id = self.player_party_map.get(player_id)
            if not party_id or party_id not in self.parties:
                return False
            party = self.parties[party_id]
            member = party.members.get(player_id)
            if not member:
                return False
            party.members[player_id] = member.with_vitals(hp, max_hp, is_online)
            return True

    def get_party(self, party_id: str) -> Optional[PartyState]:
        """Retrieves a party by party_id."""
        with self._lock:
            return self.parties.get(party_id)

    def get_player_party(self, player_id: str) -> Optional[PartyState]:
        """Retrieves the party a player belongs to."""
        with self._lock:
            party_id = self.player_party_map.get(player_id)
            return self.parties.get(party_id) if party_id else None
