"""Contract-based hiring of pre-registered NPC specialists.

No manager may create specialists or choose skill ratings. The staff payroll
plan acts as the canonical time/compensation record; its immutable events and
scheduled liabilities are managed by the established payroll service.
"""

from datetime import timedelta
from decimal import Decimal

from django.db import transaction
from django.utils import timezone

from club_payroll.constants import ObligationStatus, PayrollFrequency, PayrollPlanStatus
from club_payroll.models import PayrollEvent, PayrollObligation, PayrollPlan
from club_payroll.services import (
    PayrollObligationService,
    PayrollPlanService,
    PayrollSettlementService,
)
from clubs.models import Club
from economy.services import WalletService
from marketplace.services import MarketWallets
from staff.constants import SOURCE_NPC, SPECIALIST_ROLES
from staff.exceptions import StaffIntegrityError, StaffRoleInvalidError
from staff.models import StaffMember
from staff.services import ClubStaffAssignmentService


class StaffEmploymentService:
    @staticmethod
    @transaction.atomic
    def hire(*, club, staff_member_id, monthly_salary: Decimal, duration_days: int):
        # Match the established club -> member -> wallet lock order.
        club = Club.objects.select_for_update().get(pk=club.pk)
        if not club.status == "ACTIVE":
            raise StaffIntegrityError("Only an active club can hire staff.")
        member = (
            StaffMember.objects.select_for_update().filter(pk=staff_member_id).first()
        )
        if (
            not member
            or member.source_type != SOURCE_NPC
            or member.specialty not in SPECIALIST_ROLES
        ):
            raise StaffRoleInvalidError(
                "Only registered free-agent NPC specialists can be hired."
            )
        if member.club_assignments.filter(status="ACTIVE").exists():
            raise StaffIntegrityError("This specialist is already under contract.")
        if monthly_salary < member.minimum_salary:
            raise StaffIntegrityError(
                f"This specialist requires at least {member.minimum_salary} FTG monthly."
            )
        # The first salary must be affordable. Future salary is a recorded
        # liability, never a hidden token mint.
        wallet = WalletService.get_or_create_club_wallet(club=club)
        if not wallet.is_active or wallet.available_balance < monthly_salary:
            raise StaffIntegrityError(
                "Club wallet must cover at least the first monthly salary."
            )
        assignment = ClubStaffAssignmentService.assign(
            club_id=club.pk, staff_member_id=member.pk
        )
        today = timezone.localdate()
        plan = PayrollPlanService.create_staff_plan(
            club=club,
            staff_member=member,
            recipient_wallet=MarketWallets.system(),
            installment_amount=monthly_salary,
            frequency=PayrollFrequency.MONTHLY,
            start_date=today,
            end_date=today + timedelta(days=duration_days),
            first_due_date=today,
        )
        assignment.employment_plan = plan
        assignment.save(update_fields=["employment_plan"])
        for obligation in PayrollObligationService.generate_for_plan(
            plan=plan, up_to=today
        ):
            PayrollSettlementService.pay(
                obligation=obligation,
                idempotency_key=f"staff-hire:{assignment.pk}:{obligation.pk}",
            )
        return assignment, plan

    @staticmethod
    @transaction.atomic
    def end(*, assignment_id):
        from staff.models import ClubStaffAssignment

        assignment = ClubStaffAssignment.objects.select_related(
            "club", "staff_member"
        ).get(pk=assignment_id)
        Club.objects.select_for_update().get(pk=assignment.club_id)
        StaffMember.objects.select_for_update().get(pk=assignment.staff_member_id)
        assignment.refresh_from_db()
        if assignment.status == "ENDED":
            return assignment
        ended = ClubStaffAssignmentService.end(assignment_id=assignment_id)
        # Do not continue accruing salaries after the official end date.
        plans = PayrollPlan.objects.select_for_update().filter(
            club_id=assignment.club_id,
            staff_member_id=assignment.staff_member_id,
            plan_type="STAFF",
            status__in=[PayrollPlanStatus.ACTIVE, PayrollPlanStatus.ENDED],
        )
        if assignment.employment_plan_id:
            plans = plans.filter(pk=assignment.employment_plan_id)
        else:
            plans = plans.filter(start_date__gte=assignment.started_at.date())
        for plan in plans:
            today = timezone.localdate()
            # Materialize already-earned installments before closing the plan.
            PayrollObligationService.generate_for_plan(plan=plan, up_to=today)
            plan.refresh_from_db()
            PayrollObligation.objects.filter(
                plan=plan, due_date__gt=today, status=ObligationStatus.PENDING
            ).update(status=ObligationStatus.CANCELLED)
            plan.end_date = max(plan.start_date, min(plan.end_date, today))
            plan.status = PayrollPlanStatus.ENDED
            plan.save(update_fields=["end_date", "status", "updated_at"])
            PayrollEvent.objects.create(
                club_id=assignment.club_id,
                event_type="STAFF_PAYROLL_PLAN_CLOSED",
                reference_type="PAYROLL_PLAN",
                reference_id=str(plan.pk),
                payload={
                    "ended_at": today.isoformat(),
                    "assignment_id": str(assignment_id),
                },
            )
        return ended
