from django.db import IntegrityError, transaction
from django.utils import timezone

from .constants import SeasonStatus
from .exceptions import (
    InvalidInitialSeasonStatusError,
    InvalidSeasonStatusError,
    SeasonAlreadyActiveError,
    SeasonIntegrityError,
    SeasonNameAlreadyExistsError,
    SeasonNotActiveError,
    SeasonNotFoundError,
    SeasonYearAlreadyExistsError,
)
from .models import Season
from .validators import (
    validate_season_dates,
    validate_season_name,
    validate_season_year,
)


class SeasonService:
    """
    Owns Season lifecycle mutations.

    DRAFT, UPCOMING and ACTIVE are accepted as bootstrap states for backwards
    compatibility with existing project fixtures/imports. Terminal states must
    only be reached through lifecycle transitions.
    """

    INITIAL_STATUSES = {
        SeasonStatus.DRAFT,
        SeasonStatus.UPCOMING,
        SeasonStatus.ACTIVE,
    }

    @staticmethod
    @transaction.atomic
    def create_season(
        *,
        name,
        year,
        start_date,
        end_date,
        status=SeasonStatus.DRAFT,
    ):
        name = validate_season_name(name)
        year = validate_season_year(year)
        validate_season_dates(
            start_date=start_date,
            end_date=end_date,
        )

        if status not in SeasonStatus.values:
            raise InvalidSeasonStatusError(
                f"Invalid season status: {status}"
            )

        if status not in SeasonService.INITIAL_STATUSES:
            raise InvalidInitialSeasonStatusError(
                "A season cannot be created directly in a terminal status. "
                "Use the Season lifecycle service."
            )

        # Friendly checks. Database constraints remain the final authority and
        # protect concurrent requests.
        if Season.objects.filter(name=name).exists():
            raise SeasonNameAlreadyExistsError(
                "A season with this name already exists."
            )

        if Season.objects.filter(year=year).exists():
            raise SeasonYearAlreadyExistsError(
                "A season with this year already exists."
            )

        if (
            status == SeasonStatus.ACTIVE
            and Season.objects.filter(status=SeasonStatus.ACTIVE).exists()
        ):
            raise SeasonAlreadyActiveError(
                "Another active season already exists."
            )

        try:
            # The inner atomic block provides a real savepoint.  If a
            # concurrent request wins a unique constraint race, the outer
            # transaction remains usable for deterministic error mapping.
            with transaction.atomic():
                return Season.objects.create(
                    name=name,
                    year=year,
                    start_date=start_date,
                    end_date=end_date,
                    status=status,
                    activated_at=(
                        timezone.now()
                        if status == SeasonStatus.ACTIVE
                        else None
                    ),
                )
        except IntegrityError as exc:
            SeasonService._raise_create_integrity_error(
                name=name,
                year=year,
                status=status,
                exc=exc,
            )

    @staticmethod
    @transaction.atomic
    def mark_upcoming(*, season_id):
        season = SeasonService._lock_season(season_id)

        if season.status != SeasonStatus.DRAFT:
            raise InvalidSeasonStatusError(
                "Only a DRAFT season can become UPCOMING."
            )

        season.status = SeasonStatus.UPCOMING
        season.save(
            update_fields=[
                "status",
                "updated_at",
            ]
        )
        return season

    @staticmethod
    @transaction.atomic
    def activate(*, season_id):
        season = SeasonService._lock_season(season_id)

        if season.status != SeasonStatus.UPCOMING:
            raise InvalidSeasonStatusError(
                "Only an UPCOMING season can become ACTIVE."
            )

        from .models import SeasonAutomationPolicy
        if SeasonAutomationPolicy.objects.filter(season=season).exists():
            from .automation import readiness
            gate = readiness(season)
            if not gate["ready"]:
                from .exceptions import SeasonActivationGateError
                raise SeasonActivationGateError("; ".join(gate["reasons"]))

        if Season.objects.filter(
            status=SeasonStatus.ACTIVE
        ).exclude(pk=season.pk).exists():
            raise SeasonAlreadyActiveError(
                "Another season is already active."
            )

        season.status = SeasonStatus.ACTIVE
        season.activated_at = timezone.now()

        try:
            # Two distinct UPCOMING rows cannot be globally row-locked when no
            # ACTIVE row exists. The partial unique constraint is therefore
            # the authoritative concurrency guard.
            with transaction.atomic():
                season.save(
                    update_fields=[
                        "status",
                        "activated_at",
                        "updated_at",
                    ]
                )
        except IntegrityError as exc:
            if Season.objects.filter(
                status=SeasonStatus.ACTIVE
            ).exclude(pk=season.pk).exists():
                raise SeasonAlreadyActiveError(
                    "Another season is already active."
                ) from exc
            raise SeasonIntegrityError(
                "Season activation violated a database invariant."
            ) from exc

        return season

    @staticmethod
    @transaction.atomic
    def complete(*, season_id):
        season = SeasonService._lock_season(season_id)

        if season.status != SeasonStatus.ACTIVE:
            raise SeasonNotActiveError(
                "Only an ACTIVE season can be completed."
            )

        SeasonService._assert_competitions_terminal(season)
        season.status = SeasonStatus.COMPLETED
        season.completed_at = timezone.now()
        season.save(
            update_fields=[
                "status",
                "completed_at",
                "updated_at",
            ]
        )
        from training.progression_services import PlayerSeasonAgingService
        from training.exceptions import PlayerProgressionUnavailableError, PlayerProgressionIntegrityError
        try:
            PlayerSeasonAgingService.process_season(season_id=season.pk)
            from staff.career_management import review_season
            review_season(season)
            from contracts.lifecycle_processing import process_season
            process_season(season)
        except (PlayerProgressionUnavailableError, PlayerProgressionIntegrityError) as exc:
            from .exceptions import SeasonIntegrityError
            raise SeasonIntegrityError(f"Season progression could not be finalized: {exc}") from exc
        return season

    @staticmethod
    def _assert_competitions_terminal(season):
        # Domain gate applies to BOTH direct admin completion and the orchestrator.
        # Empty/bootstrap seasons remain valid for existing historical fixtures.
        from leagues.models import LeagueCompetition
        from leagues.constants import LeagueCompetitionStatus
        from federations.models import FederationCompetition
        from federations.constants import CompetitionStatus
        from .exceptions import SeasonOrchestrationNotReadyError

        if LeagueCompetition.objects.filter(season=season).exclude(
            status=LeagueCompetitionStatus.COMPLETED
        ).exists():
            raise SeasonOrchestrationNotReadyError(
                "All league competitions must be completed before the season closes."
            )
        if FederationCompetition.objects.filter(season=season).exclude(
            status__in=(CompetitionStatus.COMPLETED, CompetitionStatus.CANCELLED)
        ).exists():
            raise SeasonOrchestrationNotReadyError(
                "All federation competitions must be completed or cancelled before the season closes."
            )

    @staticmethod
    @transaction.atomic
    def archive(*, season_id):
        season = SeasonService._lock_season(season_id)

        if season.status != SeasonStatus.COMPLETED:
            raise InvalidSeasonStatusError(
                "Only a COMPLETED season can be archived."
            )

        # Regulatory finality gate is federation-policy controlled for backward
        # compatibility with historical/simple Season lifecycles. Once the mother
        # federation activates a ratification policy, archival requires the sealed
        # official record.
        from federations.constants import FederationStatus, FederationType
        from federations.models import Federation
        from season_ratification.models import SeasonRatificationPolicy, SeasonRatificationRecord
        mother_ids = Federation.objects.filter(
            federation_type=FederationType.MOTHER, status=FederationStatus.ACTIVE
        ).values_list("pk", flat=True)
        ratification_required = SeasonRatificationPolicy.objects.filter(
            mother_federation_id__in=mother_ids, is_active=True
        ).exists()
        if ratification_required and not SeasonRatificationRecord.objects.filter(season=season).exists():
            raise InvalidSeasonStatusError(
                "A COMPLETED season must be formally ratified before archival."
            )

        season.status = SeasonStatus.ARCHIVED
        season.archived_at = timezone.now()
        season.save(
            update_fields=[
                "status",
                "archived_at",
                "updated_at",
            ]
        )
        return season

    @staticmethod
    @transaction.atomic
    def cancel(*, season_id):
        season = SeasonService._lock_season(season_id)

        if season.status not in {
            SeasonStatus.DRAFT,
            SeasonStatus.UPCOMING,
        }:
            raise InvalidSeasonStatusError(
                "Only DRAFT or UPCOMING seasons can be cancelled."
            )

        season.status = SeasonStatus.CANCELLED
        season.save(
            update_fields=[
                "status",
                "updated_at",
            ]
        )
        return season

    @staticmethod
    def _lock_season(season_id):
        try:
            return (
                Season.objects
                .select_for_update()
                .get(pk=season_id)
            )
        except Season.DoesNotExist as exc:
            raise SeasonNotFoundError(
                "Season does not exist."
            ) from exc

    @staticmethod
    def _raise_create_integrity_error(*, name, year, status, exc):
        if (
            status == SeasonStatus.ACTIVE
            and Season.objects.filter(status=SeasonStatus.ACTIVE).exists()
        ):
            raise SeasonAlreadyActiveError(
                "Another active season already exists."
            ) from exc

        if Season.objects.filter(year=year).exists():
            raise SeasonYearAlreadyExistsError(
                "A season with this year already exists."
            ) from exc

        if Season.objects.filter(name=name).exists():
            raise SeasonNameAlreadyExistsError(
                "A season with this name already exists."
            ) from exc

        raise SeasonIntegrityError(
            "Season creation violated a database invariant."
        ) from exc
