from django.db import IntegrityError, transaction
from django.utils import timezone

from careers.models import Career, CareerStatus, CareerType
from clubs.models import Club, ClubStatus

from .constants import (
    ASSIGNMENT_ACTIVE,
    ASSIGNMENT_ENDED,
    ASSIGN_EVENT_ASSIGNED,
    ASSIGN_EVENT_ENDED,
    HEAD_COACH,
    SOURCE_CAREER,
    SOURCE_NPC,
    SPECIALIST_ROLES,
    STAFF_ROLES,
)
from .exceptions import (
    StaffAssignmentConflictError,
    StaffAssignmentEndedError,
    StaffAssignmentNotFoundError,
    StaffCareerInvalidError,
    StaffClubInvalidError,
    StaffIntegrityError,
    StaffNotFoundError,
    StaffRoleInvalidError,
)
from .models import (
    ClubStaffAssignment,
    ClubStaffAssignmentEvent,
    StaffCapabilityEvent,
    StaffCapabilityProfile,
    StaffMember,
    TeamTrainingStaffSnapshot,
)
from .policy import CapabilityState, StaffTrainingContext, StaffTrainingEffectPolicyV1
from .validators import validate_capabilities


class StaffRegistrationService:
    @staticmethod
    @transaction.atomic
    def register_head_coach(*, club_id, level, leadership, tactical, development, fitness, goalkeeping):
        club = _lock_club(club_id)
        _validate_active_club(club)
        if not club.coach_career_id:
            raise StaffCareerInvalidError("Club is in caretaker mode; appoint a career coach first.")
        career = Career.objects.select_for_update().select_related("user").get(pk=club.coach_career_id)
        if career.career_type != CareerType.COACH or career.status != CareerStatus.ACTIVE:
            raise StaffCareerInvalidError("Club head coach must be an ACTIVE COACH career.")
        member = StaffMember.objects.select_for_update().filter(coach_career=career).first()
        caps = validate_capabilities(
            level=level, leadership=leadership, tactical=tactical,
            development=development, fitness=fitness, goalkeeping=goalkeeping,
        )
        if member is None:
            member = StaffMember.objects.create(
                source_type=SOURCE_CAREER,
                specialty=HEAD_COACH,
                coach_career=career,
                first_name=career.user.first_name or "",
                last_name=career.user.last_name or "",
                nationality="",
            )
            _create_capability(member=member, revision=1, **caps)
        else:
            profile = StaffCapabilityProfile.objects.select_for_update().get(staff_member=member)
            if _profile_matches(profile, caps):
                pass
            else:
                _update_capability(profile=profile, **caps)
        assignment = _assign_locked(club=club, member=member, role=HEAD_COACH)
        return member, assignment

    @staticmethod
    @transaction.atomic
    def create_specialist(*, specialty, first_name, last_name, nationality, level, leadership, tactical, development, fitness, goalkeeping):
        if specialty not in SPECIALIST_ROLES:
            raise StaffRoleInvalidError("Specialist must be assistant, fitness, goalkeeper coach, or analyst.")
        caps = validate_capabilities(
            level=level, leadership=leadership, tactical=tactical,
            development=development, fitness=fitness, goalkeeping=goalkeeping,
        )
        if not first_name.strip() or not last_name.strip() or not nationality.strip():
            raise StaffIntegrityError("NPC staff identity fields are required.")
        member = StaffMember.objects.create(
            source_type=SOURCE_NPC,
            specialty=specialty,
            first_name=first_name.strip(),
            last_name=last_name.strip(),
            nationality=nationality.strip(),
        )
        _create_capability(member=member, revision=1, **caps)
        return member


class StaffCapabilityService:
    @staticmethod
    @transaction.atomic
    def update(*, staff_member_id, level, leadership, tactical, development, fitness, goalkeeping):
        member = _lock_member(staff_member_id)
        caps = validate_capabilities(
            level=level, leadership=leadership, tactical=tactical,
            development=development, fitness=fitness, goalkeeping=goalkeeping,
        )
        profile = StaffCapabilityProfile.objects.select_for_update().get(staff_member=member)
        if _profile_matches(profile, caps):
            return profile
        return _update_capability(profile=profile, **caps)


class ClubStaffAssignmentService:
    @staticmethod
    @transaction.atomic
    def assign(*, club_id, staff_member_id):
        club = _lock_club(club_id)
        _validate_active_club(club)
        member = _lock_member(staff_member_id)
        return _assign_locked(club=club, member=member, role=member.specialty)

    @staticmethod
    @transaction.atomic
    def end(*, assignment_id):
        identity = ClubStaffAssignment.objects.filter(pk=assignment_id).values("club_id", "staff_member_id").first()
        if identity is None:
            raise StaffAssignmentNotFoundError("Staff assignment does not exist.")
        _lock_club(identity["club_id"])
        _lock_member(identity["staff_member_id"])
        assignment = ClubStaffAssignment.objects.select_for_update().get(pk=assignment_id)
        if assignment.status == ASSIGNMENT_ENDED:
            return assignment
        assignment.status = ASSIGNMENT_ENDED
        assignment.ended_at = timezone.now()
        assignment.save(update_fields=["status", "ended_at"])
        ClubStaffAssignmentEvent.objects.create(
            assignment=assignment,
            event_type=ASSIGN_EVENT_ENDED,
            previous_status=ASSIGNMENT_ACTIVE,
            new_status=ASSIGNMENT_ENDED,
        )
        return assignment


class StaffTrainingEffectService:
    @classmethod
    def resolve_and_snapshot(cls, *, session, club):
        existing = TeamTrainingStaffSnapshot.objects.filter(session=session).first()
        if existing is not None:
            return existing
        assignments = list(
            ClubStaffAssignment.objects.select_for_update(of=("self", "staff_member"))
            .select_related("staff_member__capability")
            .filter(club=club, status=ASSIGNMENT_ACTIVE)
            .order_by("role", "pk")
        )
        from staff.match_effects import current_effect
        eligible = {row["assignment_id"] for row in current_effect(club.pk)["staff"]}
        assignments = [row for row in assignments if (not row.employment_plan_id and row.role != HEAD_COACH) or str(row.pk) in eligible]
        by_role = {}
        for assignment in sorted(assignments, key=lambda a: (a.staff_member.capability.level, a.staff_member.capability.tactical, str(a.pk))):
            by_role[assignment.role] = assignment
        context = StaffTrainingContext(
            head_coach=_capability_state(by_role.get(HEAD_COACH)),
            assistant_coach=_capability_state(by_role.get("ASSISTANT_COACH")),
            fitness_coach=_capability_state(by_role.get("FITNESS_COACH")),
            goalkeeper_coach=_capability_state(by_role.get("GOALKEEPER_COACH")),
        )
        effect = StaffTrainingEffectPolicyV1.calculate(target_attribute=session.target_attribute, context=context)
        payload = {
            "target_attribute": session.target_attribute,
            "roles": {
                role: _assignment_payload(by_role.get(role))
                for role in STAFF_ROLES
            },
        }
        context_hash = TeamTrainingStaffSnapshot.hash_payload(payload)
        try:
            return TeamTrainingStaffSnapshot.objects.create(
                session=session,
                club=club,
                head_assignment=by_role.get(HEAD_COACH),
                assistant_assignment=by_role.get("ASSISTANT_COACH"),
                fitness_assignment=by_role.get("FITNESS_COACH"),
                goalkeeper_assignment=by_role.get("GOALKEEPER_COACH"),
                target_attribute=session.target_attribute,
                multiplier=effect.multiplier,
                context_payload=payload,
                context_hash=context_hash,
                policy_key=effect.policy_key,
                policy_version=effect.policy_version,
            )
        except IntegrityError as exc:
            raise StaffIntegrityError("Staff training snapshot violates a database invariant.") 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 StaffClubInvalidError("Club does not exist.") from exc


def _validate_active_club(club):
    if club.status != ClubStatus.ACTIVE:
        raise StaffClubInvalidError("Only an ACTIVE club can manage staff.")


def _lock_member(staff_member_id):
    try:
        return StaffMember.objects.select_for_update().get(pk=staff_member_id)
    except StaffMember.DoesNotExist as exc:
        raise StaffNotFoundError("Staff member does not exist.") from exc


def _create_capability(*, member, revision, **caps):
    StaffCapabilityProfile.objects.create(staff_member=member, revision=revision, **caps)
    StaffCapabilityEvent.objects.create(staff_member=member, revision=revision, **caps)


def _update_capability(*, profile, **caps):
    profile.revision += 1
    for name, value in caps.items():
        setattr(profile, name, value)
    profile.save(update_fields=[*caps.keys(), "revision", "updated_at"])
    StaffCapabilityEvent.objects.create(staff_member=profile.staff_member, revision=profile.revision, **caps)
    return profile


def _profile_matches(profile, caps):
    return all(getattr(profile, name) == value for name, value in caps.items())


def _assign_locked(*, club, member, role):
    if role != member.specialty:
        raise StaffRoleInvalidError("Assignment role must match staff specialty.")
    if role == HEAD_COACH:
        if member.source_type != SOURCE_CAREER or member.coach_career_id != club.coach_career_id:
            raise StaffCareerInvalidError("HEAD_COACH must be the club's own coach career.")
    elif member.source_type != SOURCE_NPC:
        raise StaffRoleInvalidError("Specialist staff must be NPC staff members.")

    same = ClubStaffAssignment.objects.filter(club=club, staff_member=member, status=ASSIGNMENT_ACTIVE).first()
    if same is not None:
        return same
    existing = ClubStaffAssignment.objects.select_for_update().filter(
        club=club, role=role, status=ASSIGNMENT_ACTIVE
    ).first()
    if existing is not None:
        if existing.staff_member_id == member.pk:
            return existing
        if role != "ASSISTANT_COACH":
            raise StaffAssignmentConflictError("Club already has an active staff member in this role.")
        if ClubStaffAssignment.objects.filter(club=club, role=role, status=ASSIGNMENT_ACTIVE).count() >= 3:
            raise StaffAssignmentConflictError("A club may employ at most three assistant coaches.")
    other = ClubStaffAssignment.objects.select_for_update().filter(
        staff_member=member, status=ASSIGNMENT_ACTIVE
    ).first()
    if other is not None:
        raise StaffAssignmentConflictError("Staff member already has an active club assignment.")
    try:
        assignment = ClubStaffAssignment.objects.create(
            club=club,
            staff_member=member,
            role=role,
            status=ASSIGNMENT_ACTIVE,
            started_at=timezone.now(),
        )
        ClubStaffAssignmentEvent.objects.create(
            assignment=assignment,
            event_type=ASSIGN_EVENT_ASSIGNED,
            previous_status="",
            new_status=ASSIGNMENT_ACTIVE,
        )
        return assignment
    except IntegrityError as exc:
        raise StaffAssignmentConflictError("Staff assignment conflicts with an active role or club.") from exc


def _capability_state(assignment):
    if assignment is None:
        return None
    profile = assignment.staff_member.capability
    return CapabilityState(
        level=profile.level,
        leadership=profile.leadership,
        tactical=profile.tactical,
        development=profile.development,
        fitness=profile.fitness,
        goalkeeping=profile.goalkeeping,
    )


def _assignment_payload(assignment):
    if assignment is None:
        return None
    profile = assignment.staff_member.capability
    return {
        "assignment_id": str(assignment.pk),
        "staff_member_id": str(assignment.staff_member_id),
        "capability_revision": profile.revision,
        "level": profile.level,
        "leadership": profile.leadership,
        "tactical": profile.tactical,
        "development": profile.development,
        "fitness": profile.fitness,
        "goalkeeping": profile.goalkeeping,
    }
