from players.age import sporting_age
from decimal import Decimal

from django.db import IntegrityError, transaction
from django.utils import timezone

from careers.models import CareerStatus
from clubs.models import Club, ClubSquadMembership, ClubStatus
from players.models import Player

from .engine import TrainingEffectInput, TrainingEffectPolicyV1
from .exceptions import (
    DuplicateTrainingParticipantError,
    EmptyTeamTrainingError,
    PlayerNotInClubRosterError,
    TrainingClubInactiveError,
    TrainingClubNotFoundError,
    TrainingIntegrityError,
    TrainingPlayerInactiveError,
    TrainingPlayerNotFoundError,
    TrainingSessionCancelledError,
    TrainingSessionNotFoundError,
)
from .models import (
    PlayerDevelopmentProgress,
    TrainingDevelopmentResult,
    TrainingParticipant,
    TrainingSession,
    TrainingSessionStatus,
    TrainingSessionType,
)
from .scheduling_policy import check_daily_capacity, check_execution_date, check_schedule_date
from .modifiers import AuthoritativeTrainingModifierResolver
from staff.services import StaffTrainingEffectService
from .validators import validate_training_attribute, validate_training_date


class TrainingSchedulingService:
    @staticmethod
    @transaction.atomic
    def schedule_individual(*, player_id, training_date, target_attribute, policy_version="1.0.0") -> TrainingSession:
        training_date = validate_training_date(training_date)
        target_attribute = validate_training_attribute(target_attribute)
        check_schedule_date(training_date)
        player = _lock_player(player_id)
        _validate_player_active(player)
        if training_date < player.date_of_birth:
            from .exceptions import InvalidTrainingDateError
            raise InvalidTrainingDateError("Training date cannot be before player birth date.")
        if policy_version == "2.0.0":
            from .timed import check_individual_capacity
            check_individual_capacity(player, training_date)
        else:
            check_daily_capacity(players=[player], training_date=training_date)

        session = TrainingSession.objects.create(
            session_type=TrainingSessionType.INDIVIDUAL,
            status=TrainingSessionStatus.SCHEDULED,
            training_date=training_date,
            target_attribute=target_attribute,
            base_effect=Decimal("1.000000"),
            policy_version=policy_version,
            policy_key="ftg.training.drill.v2" if policy_version == "2.0.0" else "ftg.training.effect.v1",
        )
        TrainingParticipant.objects.create(session=session, player=player)
        return session

    @staticmethod
    @transaction.atomic
    def schedule_team(
        *,
        club_id,
        training_date,
        target_attribute,
        player_ids=None,
    ) -> TrainingSession:
        training_date = validate_training_date(training_date)
        target_attribute = validate_training_attribute(target_attribute)
        check_schedule_date(training_date)
        club = _lock_club(club_id)
        _validate_club_active(club)

        # Read roster identity first without locking, then acquire locks in the
        # project-wide canonical prefix Club -> Players -> Roster. This avoids
        # a Roster -> Player inversion against Contracts/Transfers.
        roster_ids = list(
            ClubSquadMembership.objects
            .filter(club_id=club.pk)
            .order_by("player_id")
            .values_list("player_id", flat=True)
        )

        if player_ids is None:
            selected_ids = roster_ids
        else:
            selected_ids = list(player_ids)
            if len(selected_ids) != len(set(selected_ids)):
                raise DuplicateTrainingParticipantError(
                    "Team training cannot contain duplicate players."
                )
            selected_set = set(selected_ids)
            roster_set = set(roster_ids)
            missing = selected_set - roster_set
            if missing:
                raise PlayerNotInClubRosterError(
                    "Every team-training participant must belong to the current club roster."
                )
            selected_ids = sorted(selected_set, key=str)

        if not selected_ids:
            raise EmptyTeamTrainingError("Team training requires at least one current roster player.")

        players = list(
            Player.objects
            .select_for_update()
            .select_related("career")
            .filter(pk__in=selected_ids)
            .order_by("pk")
        )
        if len(players) != len(selected_ids):
            raise TrainingPlayerNotFoundError("One or more team-training players do not exist.")

        locked_roster_ids = set(
            ClubSquadMembership.objects
            .select_for_update()
            .filter(club_id=club.pk, player_id__in=selected_ids)
            .values_list("player_id", flat=True)
        )
        if locked_roster_ids != set(selected_ids):
            raise PlayerNotInClubRosterError(
                "Team-training participants changed roster membership during scheduling."
            )

        for player in players:
            _validate_player_active(player)
            if training_date < player.date_of_birth:
                from .exceptions import InvalidTrainingDateError
                raise InvalidTrainingDateError("Training date cannot be before player birth date.")

        check_daily_capacity(players=players, training_date=training_date)
        session = TrainingSession.objects.create(
            session_type=TrainingSessionType.TEAM,
            status=TrainingSessionStatus.SCHEDULED,
            training_date=training_date,
            club=club,
            target_attribute=target_attribute,
            base_effect=Decimal("1.000000"),
        )
        TrainingParticipant.objects.bulk_create(
            [TrainingParticipant(session=session, player=player) for player in players]
        )
        return session


class TrainingExecutionService:
    modifier_resolver = AuthoritativeTrainingModifierResolver()
    effect_policy = TrainingEffectPolicyV1

    @classmethod
    @transaction.atomic
    def execute(cls, *, session_id, attending_player_id=None):
        identity = TrainingSession.objects.filter(pk=session_id).values(
            "id", "session_type", "club_id", "starts_at"
        ).first()
        if identity is None:
            raise TrainingSessionNotFoundError("Training session does not exist.")

        if identity["starts_at"] and attending_player_id is None:
            from .exceptions import TrainingValidationError
            raise TrainingValidationError("Timed group training requires player self check-in.")
        participant_filter = {"session_id": session_id}
        if attending_player_id is not None:
            participant_filter["player_id"] = attending_player_id
        participant_ids = list(
            TrainingParticipant.objects.filter(**participant_filter)
            .order_by("player_id")
            .values_list("player_id", flat=True)
        )
        if not participant_ids:
            raise TrainingIntegrityError("Training session has no participants.")

        club = None
        if identity["session_type"] == TrainingSessionType.TEAM:
            club = _lock_club(identity["club_id"])
            _validate_club_active(club)

        players = list(
            Player.objects
            .select_for_update()
            .select_related("career")
            .filter(pk__in=participant_ids)
            .order_by("pk")
        )
        if len(players) != len(participant_ids):
            raise TrainingIntegrityError("One or more training participants no longer exist.")

        try:
            session = TrainingSession.objects.select_for_update().get(pk=session_id)
        except TrainingSession.DoesNotExist as exc:
            raise TrainingSessionNotFoundError("Training session does not exist.") from exc

        if session.status == TrainingSessionStatus.COMPLETED:
            existing_results = list(
                TrainingDevelopmentResult.objects
                .filter(participant__session=session)
                .select_related("participant", "player")
                .order_by("player_id")
            )
            cls._materialize_results(session=session, results=existing_results)
            return existing_results
        if session.status == TrainingSessionStatus.CANCELLED:
            raise TrainingSessionCancelledError("Cancelled training cannot be executed.")
        check_execution_date(session.training_date)
        if session.policy_version == "2.0.0" and not session.starts_at and session.training_date != timezone.localdate():
            from .exceptions import InvalidTrainingDateError
            raise InvalidTrainingDateError("New training must be executed on its scheduled date.")

        coach_multiplier = Decimal("1.000000")
        if session.session_type == TrainingSessionType.TEAM:
            staff_snapshot = StaffTrainingEffectService.resolve_and_snapshot(session=session, club=club)
            coach_multiplier = staff_snapshot.multiplier
            # Facility utility is server-authoritative and composes with staff quality.
            from club_assets.services import TrainingGroundEffectService
            coach_multiplier = (coach_multiplier * TrainingGroundEffectService.resolve_multiplier(club=club)).quantize(Decimal("0.000001"))

        if session.session_type == TrainingSessionType.TEAM:
            current_roster_ids = set(
                ClubSquadMembership.objects
                .select_for_update()
                .filter(club_id=club.pk, player_id__in=participant_ids)
                .values_list("player_id", flat=True)
            )
            if session.starts_at:
                from .club_players import roster_ids
                current_roster_ids = roster_ids(club.pk,lock=True) & set(participant_ids)
            if current_roster_ids != set(participant_ids):
                raise PlayerNotInClubRosterError(
                    "A team-training participant is no longer in the current club roster."
                )

        participants = {
            participant.player_id: participant
            for participant in TrainingParticipant.objects
            .select_for_update()
            .filter(session=session)
        }

        results = []
        for player in players:
            _validate_player_active(player)
            participant = participants.get(player.pk)
            if participant is None:
                raise TrainingIntegrityError("Training participant mapping is inconsistent.")

            from .drills import weights
            from .timed import personal_facility
            targets = weights(session.target_attribute) if session.policy_version == "2.0.0" else {session.target_attribute: Decimal("1")}
            facility = Decimal("1")
            if session.policy_version == "2.0.0" and session.session_type == TrainingSessionType.INDIVIDUAL:
                facility, facility_evidence = personal_facility(player)
                participant.evidence = {"facility": facility_evidence, "weights": {k: str(v) for k,v in targets.items()}}
                participant.checked_in_at = timezone.now()
                participant.save(update_fields=["evidence", "checked_in_at"])
            for attribute, weight in targets.items():
                progress, _ = PlayerDevelopmentProgress.objects.select_for_update().get_or_create(
                    player=player,
                    attribute_name=attribute,
                    defaults={"total_points": Decimal("0.000000")},
                )
                progress_before = progress.total_points
                effect = cls.effect_policy.calculate(
                    TrainingEffectInput(
                        training_date=session.training_date,
                        date_of_birth=player.date_of_birth,
                        age_years=sporting_age(player, session.training_date),
                        overall=player.overall,
                        potential=player.potential,
                        base_effect=(session.base_effect * weight * facility).quantize(Decimal("0.000001")),
                        modifiers=cls.modifier_resolver.resolve(
                            player=player, session=session, coach_multiplier=coach_multiplier
                        ),
                    )
                )
                progress_after = progress_before + effect.final_effect
                progress.total_points = progress_after
                progress.save(update_fields=["total_points", "updated_at"])

                try:
                    result = TrainingDevelopmentResult.objects.create(
                        participant=participant,
                        player=player,
                        attribute_name=attribute,
                        training_date=session.training_date,
                        age_years=effect.age_years,
                        overall_before=player.overall,
                        potential_at_training=player.potential,
                        attribute_rating_before=getattr(player, attribute),
                        base_effect=effect.base_effect,
                        potential_factor=effect.potential_factor,
                        age_factor=effect.age_factor,
                        nutrition_multiplier=effect.nutrition_multiplier,
                        coach_multiplier=effect.coach_multiplier,
                        fatigue_multiplier=effect.fatigue_multiplier,
                        injury_multiplier=effect.injury_multiplier,
                        final_effect=effect.final_effect,
                        progress_before=progress_before,
                        progress_after=progress_after,
                        policy_key=session.policy_key,
                        policy_version=session.policy_version,
                    )
                except IntegrityError as exc:
                    raise TrainingIntegrityError(
                        "Training result violates a database invariant."
                    ) from exc
                results.append(result)

        if not session.starts_at:
            session.status = TrainingSessionStatus.COMPLETED
            session.completed_at = timezone.now()
            session.save(update_fields=["status", "completed_at"])
        cls._materialize_results(session=session, results=results)
        return results

    @staticmethod
    def _materialize_results(*, session, results):
        from .models import PlayerRatingMaterializationTrigger
        from .progression_services import PlayerProgressionMaterializationService

        for player_id in sorted({row.player_id for row in results}, key=str):
            PlayerProgressionMaterializationService.materialize_player(
                player_id=player_id,
                trigger_type=PlayerRatingMaterializationTrigger.TRAINING,
                source_reference=session.pk,
                observed_on=session.training_date,
            )


class TrainingLifecycleService:
    @staticmethod
    @transaction.atomic
    def cancel(*, session_id):
        try:
            session = TrainingSession.objects.select_for_update().get(pk=session_id)
        except TrainingSession.DoesNotExist as exc:
            raise TrainingSessionNotFoundError("Training session does not exist.") from exc
        if session.status == TrainingSessionStatus.COMPLETED:
            from .exceptions import TrainingSessionAlreadyCompletedError
            raise TrainingSessionAlreadyCompletedError("Completed training cannot be cancelled.")
        if session.starts_at and session.participants.filter(checked_in_at__isnull=False).exists():
            from .exceptions import TrainingValidationError
            raise TrainingValidationError("Training with recorded attendance cannot be cancelled.")
        if session.status == TrainingSessionStatus.CANCELLED:
            return session
        session.status = TrainingSessionStatus.CANCELLED
        session.cancelled_at = timezone.now()
        session.save(update_fields=["status", "cancelled_at"])
        return session


def _lock_player(player_id):
    try:
        return (
            Player.objects.select_for_update()
            .select_related("career")
            .get(pk=player_id)
        )
    except Player.DoesNotExist as exc:
        raise TrainingPlayerNotFoundError("Player does not exist.") from exc


def _lock_club(club_id):
    try:
        return (
            Club.objects.select_for_update(of=("self",))
            .select_related("coach_career")
            .get(pk=club_id)
        )
    except Club.DoesNotExist as exc:
        raise TrainingClubNotFoundError("Club does not exist.") from exc


def _validate_player_active(player):
    if player.career.status != CareerStatus.ACTIVE:
        raise TrainingPlayerInactiveError("Only an ACTIVE player career can train.")


def _validate_club_active(club):
    if club.status != ClubStatus.ACTIVE or club.coach_career.status != CareerStatus.ACTIVE:
        raise TrainingClubInactiveError("Only an ACTIVE club with an ACTIVE coach career can train.")
