from datetime import date
from typing import Optional

from django.utils import timezone

from clubs.models import ClubStatus
from careers.models import CareerStatus
from contracts.constants import ContractStatus
from contracts.models import Contract
from seasons.constants import SeasonStatus
from transfers.constants import TransferWindowStatus

from transfers.eligibility.constants import (
    EligibilityCode,
    EligibilityStatus,
    MINOR_AGE,
)
from transfers.eligibility.context import TransferEligibilityContext
from transfers.eligibility.result import EligibilityReason


def decision(
    code: EligibilityCode,
    status: EligibilityStatus,
    message: str,
) -> EligibilityReason:
    return EligibilityReason(
        code=code,
        status=status,
        message=message,
    )


def calculate_age(
    birth_date: date,
    on_date: date,
) -> int:
    age = on_date.year - birth_date.year

    if (on_date.month, on_date.day) < (
        birth_date.month,
        birth_date.day,
    ):
        age -= 1

    return age


class PlayerExistsRule:

    @staticmethod
    def evaluate(
        context: TransferEligibilityContext,
    ) -> Optional[EligibilityReason]:

        if context.player is None:
            return decision(
                EligibilityCode.PLAYER_NOT_FOUND,
                EligibilityStatus.DENIED,
                "Player is required for transfer eligibility evaluation.",
            )

        if not context.player.pk:
            return decision(
                EligibilityCode.PLAYER_INVALID,
                EligibilityStatus.DENIED,
                "Player must be a persisted database object.",
            )

        return None


class ActiveCareerRule:

    @staticmethod
    def evaluate(
        context: TransferEligibilityContext,
    ) -> Optional[EligibilityReason]:

        player = context.player
        if player is not None and player.pk:
            if player.career.status != CareerStatus.ACTIVE:
                return decision(
                    EligibilityCode.CAREER_INACTIVE,
                    EligibilityStatus.DENIED,
                    "Player and coach careers must be active.",
                )

        for club in (context.source_club, context.destination_club):
            if club is not None and club.pk:
                if club.coach_career.status != CareerStatus.ACTIVE:
                    return decision(
                        EligibilityCode.CAREER_INACTIVE,
                        EligibilityStatus.DENIED,
                        "Player and coach careers must be active.",
                    )

        return None


class TransferWindowRule:

    @staticmethod
    def evaluate(
        context: TransferEligibilityContext,
    ) -> Optional[EligibilityReason]:

        if context.destination_club is not None and context.destination_club.pk:
            from transfers.recruitment import recruitment_error
            error = recruitment_error(club=context.destination_club, player=context.player,
                                      source_club=context.source_club,
                                      season=context.window.season if context.window else None,
                                      now=context.now, window=context.window)
            if error:
                return decision(EligibilityCode.WINDOW_NOT_OPEN, EligibilityStatus.DENIED, error)
            # League outsiders may sign genuinely free agents even when the season window is closed.
            from leagues.models import LeagueMembership
            if context.source_club is None and context.window is not None and not LeagueMembership.objects.filter(
                club=context.destination_club, season=context.window.season, status="REGISTERED",
            ).exists():
                return None

        if context.window is None:
            return decision(
                EligibilityCode.WINDOW_NOT_FOUND,
                EligibilityStatus.DENIED,
                "Transfer window is required.",
            )

        if not context.window.pk:
            return decision(
                EligibilityCode.WINDOW_NOT_FOUND,
                EligibilityStatus.DENIED,
                "Transfer window must be a persisted database object.",
            )

        if context.window.status != TransferWindowStatus.OPEN:
            return decision(
                EligibilityCode.WINDOW_NOT_OPEN,
                EligibilityStatus.DENIED,
                "Transfer window is not open.",
            )

        now = context.now or timezone.now()

        if not (
            context.window.start_at <= now < context.window.end_at
        ):
            return decision(
                EligibilityCode.WINDOW_NOT_CURRENT,
                EligibilityStatus.DENIED,
                "Transfer window is not currently active.",
            )

        return None


class SeasonActiveRule:

    @staticmethod
    def evaluate(
        context: TransferEligibilityContext,
    ) -> Optional[EligibilityReason]:

        if context.window is None:
            return None

        season = context.window.season

        allowed_preseason = False
        if season.status == SeasonStatus.UPCOMING:
            from seasons.models import SeasonAutomationPolicy
            policy = SeasonAutomationPolicy.objects.filter(season=season).first()
            now = context.now or timezone.now()
            allowed_preseason = policy is not None and policy.registration_opens_at <= now < policy.starts_at
        if season.status != SeasonStatus.ACTIVE and not allowed_preseason:
            return decision(
                EligibilityCode.SEASON_NOT_ACTIVE,
                EligibilityStatus.DENIED,
                "The season associated with the transfer window is not active.",
            )

        return None


class SourceClubRule:

    @staticmethod
    def evaluate(
        context: TransferEligibilityContext,
    ) -> Optional[EligibilityReason]:

        source = context.source_club

        # None is valid for free-agent transfers.
        if source is None:
            return None

        if not source.pk:
            return decision(
                EligibilityCode.SOURCE_CLUB_NOT_FOUND,
                EligibilityStatus.DENIED,
                "Source club must be a persisted database object.",
            )

        if source.status != ClubStatus.ACTIVE:
            return decision(
                EligibilityCode.SOURCE_CLUB_INACTIVE,
                EligibilityStatus.DENIED,
                "Source club is not active.",
            )

        return None


class DestinationClubRule:

    @staticmethod
    def evaluate(
        context: TransferEligibilityContext,
    ) -> Optional[EligibilityReason]:

        destination = context.destination_club

        if destination is None:
            return decision(
                EligibilityCode.DESTINATION_CLUB_NOT_FOUND,
                EligibilityStatus.DENIED,
                "Destination club is required.",
            )

        if not destination.pk:
            return decision(
                EligibilityCode.DESTINATION_CLUB_NOT_FOUND,
                EligibilityStatus.DENIED,
                "Destination club must be a persisted database object.",
            )

        if destination.status != ClubStatus.ACTIVE:
            return decision(
                EligibilityCode.DESTINATION_CLUB_INACTIVE,
                EligibilityStatus.DENIED,
                "Destination club is not active.",
            )

        return None


class SameClubRule:

    @staticmethod
    def evaluate(
        context: TransferEligibilityContext,
    ) -> Optional[EligibilityReason]:

        source = context.source_club
        destination = context.destination_club

        if (
            source is not None
            and destination is not None
            and source.pk == destination.pk
        ):
            return decision(
                EligibilityCode.SAME_CLUB,
                EligibilityStatus.DENIED,
                "Source and destination clubs cannot be the same.",
            )

        return None


class ContractConsistencyRule:

    @staticmethod
    def evaluate(
        context: TransferEligibilityContext,
    ) -> Optional[EligibilityReason]:

        player = context.player

        if player is None or not player.pk:
            return None

        active_contract = (
            Contract.objects
            .filter(
                player=player,
                status=ContractStatus.ACTIVE,
            )
            .select_related("club")
            .first()
        )

        # No active contract => free agent.
        if active_contract is None:

            if context.source_club is not None:
                return decision(
                    EligibilityCode.FREE_AGENT_SOURCE_INVALID,
                    EligibilityStatus.DENIED,
                    "A player without an active contract cannot have a source club.",
                )

            return None

        # Active contract exists, source club is mandatory.
        if context.source_club is None:
            return decision(
                EligibilityCode.CONTRACT_MISMATCH,
                EligibilityStatus.DENIED,
                "Player has an active contract but no source club was provided.",
            )

        if active_contract.club_id != context.source_club.pk:
            return decision(
                EligibilityCode.CONTRACT_MISMATCH,
                EligibilityStatus.DENIED,
                "Player is contracted to a different club.",
            )

        return None


class RosterConsistencyRule:

    @staticmethod
    def evaluate(
        context: TransferEligibilityContext,
    ) -> Optional[EligibilityReason]:

        player = context.player
        if player is None or not player.pk:
            return None

        from clubs.models import ClubSquadMembership
        from contracts.constants import ContractRosterScope
        from academy.models import YouthSquadMembership

        active_contract = (
            Contract.objects
            .filter(player=player, status=ContractStatus.ACTIVE)
            .first()
        )
        senior_membership = ClubSquadMembership.objects.filter(player=player).first()
        youth_membership = YouthSquadMembership.objects.filter(player=player).select_related(
            "youth_team__academy"
        ).first()

        if active_contract is None:
            if senior_membership is not None or youth_membership is not None:
                return decision(
                    EligibilityCode.ROSTER_MISMATCH,
                    EligibilityStatus.DENIED,
                    "Free agent still has a current squad membership.",
                )
            return None

        if active_contract.squad_scope == ContractRosterScope.SENIOR:
            if youth_membership is not None or senior_membership is None:
                return decision(
                    EligibilityCode.ROSTER_MISMATCH,
                    EligibilityStatus.DENIED,
                    "Senior contract does not match current senior roster state.",
                )
            if (
                senior_membership.source_contract_id != active_contract.pk
                or senior_membership.club_id != active_contract.club_id
            ):
                return decision(
                    EligibilityCode.ROSTER_MISMATCH,
                    EligibilityStatus.DENIED,
                    "Current senior squad membership does not match the active contract.",
                )
            return None

        if active_contract.squad_scope == ContractRosterScope.YOUTH:
            if senior_membership is not None or youth_membership is None:
                return decision(
                    EligibilityCode.ROSTER_MISMATCH,
                    EligibilityStatus.DENIED,
                    "Youth contract does not match current youth roster state.",
                )
            if (
                youth_membership.source_contract_id != active_contract.pk
                or youth_membership.youth_team.academy.club_id != active_contract.club_id
            ):
                return decision(
                    EligibilityCode.ROSTER_MISMATCH,
                    EligibilityStatus.DENIED,
                    "Current youth squad membership does not match the active contract.",
                )
            return None

        return decision(
            EligibilityCode.ROSTER_MISMATCH,
            EligibilityStatus.DENIED,
            "Active contract has an unsupported squad scope.",
        )


class ActiveTransferConflictRule:

    @staticmethod
    def evaluate(
        context: TransferEligibilityContext,
    ) -> Optional[EligibilityReason]:

        player = context.player
        if player is None or not player.pk:
            return None

        # Local import avoids a models -> eligibility -> models cycle.
        from transfers.constants import TransferStatus
        from transfers.models import Transfer

        query = Transfer.objects.filter(
            player=player,
            status__in=[
                TransferStatus.PENDING,
                TransferStatus.REQUIRES_APPROVAL,
                TransferStatus.APPROVED,
            ],
        )
        if context.current_transfer_id is not None:
            query = query.exclude(pk=context.current_transfer_id)

        if query.exists():
            return decision(
                EligibilityCode.ACTIVE_TRANSFER_CONFLICT,
                EligibilityStatus.DENIED,
                "Player already has an active transfer process.",
            )

        return None


class MinorInternationalRule:

    @staticmethod
    def evaluate(
        context: TransferEligibilityContext,
    ) -> Optional[EligibilityReason]:

        if not context.effective_is_international:
            return None

        player = context.player

        if player is None or not player.pk:
            return None

        now = context.now or timezone.now()
        if timezone.is_naive(now):
            now = timezone.make_aware(now)

        from players.age import sporting_age
        age = sporting_age(player)

        if age < MINOR_AGE:
            return decision(
                EligibilityCode.MINOR_INTERNATIONAL_REQUIRES_APPROVAL,
                EligibilityStatus.REQUIRES_APPROVAL,
                "International transfer of a minor requires additional approval.",
            )

        return None


DEFAULT_RULES = (
    PlayerExistsRule,
    ActiveCareerRule,
    TransferWindowRule,
    SeasonActiveRule,
    SourceClubRule,
    DestinationClubRule,
    SameClubRule,
    ContractConsistencyRule,
    RosterConsistencyRule,
    ActiveTransferConflictRule,
    MinorInternationalRule,
)
