from datetime import timedelta
from decimal import Decimal

from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Sum
from django.utils import timezone

from apps.audit.services import log_event
from apps.workflow.models import Notification, WorkflowTask
from .models import ClearanceItem, ExitCase, FinalSettlement


DEFAULT_CLEARANCES = ("REPORTING_MANAGER", "IT", "ACCOUNTS", "HR")
ROLE_FALLBACK = {"REPORTING_MANAGER": "HR Admin", "IT": "IT", "ACCOUNTS": "Accounts", "HR": "HR Admin"}


def _employee_user(employee):
    return getattr(employee, "user", None)


def _notify_employee(employee, title, message, reference_no=""):
    user = _employee_user(employee)
    if user:
        Notification.objects.create(recipient=user, title=title, message=message, module="EXIT", reference_no=reference_no)


def default_clearance_assignees(employee):
    manager = getattr(employee, "reporting_manager", None)
    return {"REPORTING_MANAGER": getattr(manager, "user", None) if manager else None, "IT": None, "ACCOUNTS": None, "HR": None}


@transaction.atomic
def create_exit_case_from_resignation(employee_request, *, user=None):
    if employee_request.request_type != "RESIGNATION":
        raise ValidationError("Only an approved resignation request can create an exit case.")
    proposed = (employee_request.payload or {}).get("proposed_last_working_day")
    exit_case, created = ExitCase.objects.get_or_create(
        employee=employee_request.employee,
        resignation_reference=employee_request.reference_no,
        defaults={"proposed_last_working_day": proposed or None, "status": "CLEARANCE_PENDING"},
    )
    if created:
        create_clearance_workflow(exit_case, assignees=default_clearance_assignees(employee_request.employee), user=user)
        _notify_employee(employee_request.employee, "Exit clearance started", "Your resignation is approved. Department clearances have been assigned automatically.", employee_request.reference_no)
    return exit_case


@transaction.atomic
def finalize_working_day(exit_case, *, final_working_day, user=None, reason=""):
    """Authoritatively set/correct the employee's last service date before settlement payment."""
    exit_case = ExitCase.objects.select_for_update().select_related("employee__company").get(pk=exit_case.pk)
    if not final_working_day:
        raise ValidationError("Final working day is required.")
    if final_working_day < exit_case.employee.joining_date:
        raise ValidationError("Final working day cannot be before joining date.")

    settlement = FinalSettlement.objects.select_for_update().filter(exit_case=exit_case).first()
    if settlement and settlement.status in {
        FinalSettlement.Status.SUBMITTED, FinalSettlement.Status.APPROVED, FinalSettlement.Status.PAID,
    }:
        raise ValidationError(
            "Final working day cannot be changed while final settlement is Submitted, Approved or Paid. "
            "Return/reject the settlement through the controlled review flow before correcting the date."
        )

    from apps.leave.models import LeaveRequest
    conflicting_leave = LeaveRequest.objects.filter(
        employee=exit_case.employee, status="APPROVED", end_date__gt=final_working_day
    ).order_by("start_date", "pk").first()
    if conflicting_leave:
        raise ValidationError(
            f"Approved leave extends beyond the proposed Final Working Day ({conflicting_leave.start_date} to "
            f"{conflicting_leave.end_date}). Cancel/correct the leave before changing the exit date."
        )

    # A Final Working Day change alters payroll service eligibility for the affected month(s).
    # Unpaid non-Draft payroll evidence must therefore be returned to Draft/rebuilt first.
    from apps.payroll.models import PayrollPeriod
    old = exit_case.final_working_day
    month_cursor = (old or final_working_day).replace(day=1)
    month_end = final_working_day.replace(day=1)
    if month_cursor > month_end:
        month_cursor, month_end = month_end, month_cursor
    while month_cursor <= month_end:
        period = PayrollPeriod.objects.filter(
            company=exit_case.employee.company, year=month_cursor.year, month=month_cursor.month
        ).first()
        if (
            period
            and period.status != PayrollPeriod.Status.DRAFT
            and period.payment_status == PayrollPeriod.PaymentStatus.UNPAID
        ):
            raise ValidationError(
                f"Payroll {month_cursor:%Y-%m} is {period.get_status_display()} and unpaid. "
                "Return/reject/reopen it to Draft and rebuild payroll before changing the Final Working Day."
            )
        if month_cursor.month == 12:
            month_cursor = month_cursor.replace(year=month_cursor.year + 1, month=1)
        else:
            month_cursor = month_cursor.replace(month=month_cursor.month + 1)

    exit_case.final_working_day = final_working_day
    exit_case.final_working_day_finalized_by = user
    exit_case.final_working_day_finalized_at = timezone.now()
    exit_case.save(update_fields=["final_working_day", "final_working_day_finalized_by", "final_working_day_finalized_at", "updated_at"])
    log_event(
        user=user, action="FINALIZE_WORKING_DAY", module="exits", object_id=exit_case.pk,
        object_repr=str(exit_case.employee), old_values={"final_working_day": str(old or "")},
        new_values={"final_working_day": str(final_working_day)}, reason=(reason or "").strip(),
    )
    return exit_case


@transaction.atomic
def create_clearance_workflow(exit_case, *, assignees=None, user=None):
    exit_case = ExitCase.objects.select_for_update().select_related("employee__company", "employee__reporting_manager__user").get(pk=exit_case.pk)
    assignees = assignees or default_clearance_assignees(exit_case.employee)
    created = []
    for clearance_type in DEFAULT_CLEARANCES:
        assigned_to = assignees.get(clearance_type)
        item, _ = ClearanceItem.objects.get_or_create(exit_case=exit_case, clearance_type=clearance_type, defaults={"assigned_to": assigned_to})
        if assigned_to and item.assigned_to_id != assigned_to.id:
            item.assigned_to = assigned_to
            item.save(update_fields=["assigned_to", "updated_at"])
        task, task_created = WorkflowTask.objects.get_or_create(
            company=exit_case.employee.company,
            module="EXIT_CLEARANCE",
            reference_no=f"EXIT-{exit_case.pk}-{clearance_type}",
            defaults={
                "title": f"{clearance_type.replace('_', ' ').title()} clearance — {exit_case.employee.full_name}",
                "assigned_to": assigned_to,
                "assigned_role": "" if assigned_to else ROLE_FALLBACK[clearance_type],
                "source_type": "clearance_item",
                "source_id": str(item.pk),
                "payload": {"exit_case_id": exit_case.pk, "clearance_type": clearance_type},
            },
        )
        if not task_created and assigned_to and task.assigned_to_id != assigned_to.id:
            task.assigned_to = assigned_to
            task.assigned_role = ""
            task.save(update_fields=["assigned_to", "assigned_role", "updated_at"])
        created.append(task)
    log_event(user=user, action="CREATE_CLEARANCE_WORKFLOW", module="exits", object_id=exit_case.pk, object_repr=str(exit_case.employee), new_values={"clearance_count": len(created)})
    return created


def refresh_exit_case_status(exit_case):
    items = list(exit_case.clearances.all())
    exit_case.status = "SETTLEMENT_PENDING" if items and all(i.status == "CLEARED" for i in items) else "CLEARANCE_PENDING"
    exit_case.save(update_fields=["status", "updated_at"])
    return exit_case


@transaction.atomic
def record_clearance_decision(item, *, decision, note, user):
    item = ClearanceItem.objects.select_for_update().select_related("exit_case__employee__user").get(pk=item.pk)
    if decision == "APPROVED":
        item.status = "CLEARED"
        item.cleared_at = timezone.now()
    elif decision == "REJECTED":
        item.status = "REJECTED"
        item.cleared_at = None
    else:
        item.status = "RETURNED"
        item.cleared_at = None
    item.note = note.strip()
    item.save(update_fields=["status", "note", "cleared_at", "updated_at"])
    refresh_exit_case_status(item.exit_case)
    _notify_employee(item.exit_case.employee, f"{item.clearance_type.replace('_', ' ').title()} clearance updated", note or item.status, f"EXIT-{item.exit_case_id}")
    return item


@transaction.atomic
def override_clearance(item, *, user, reason):
    if not reason.strip():
        raise ValidationError("Override reason is required.")
    item = ClearanceItem.objects.select_for_update().select_related("exit_case__employee").get(pk=item.pk)
    old_status = item.status
    item.status = "CLEARED"
    item.overridden = True
    item.override_reason = reason.strip()
    item.cleared_at = timezone.now()
    item.note = f"HR/Super Admin override: {reason.strip()}"
    item.save(update_fields=["status", "overridden", "override_reason", "cleared_at", "note", "updated_at"])
    refresh_exit_case_status(item.exit_case)
    log_event(user=user, action="OVERRIDE_CLEARANCE", module="exits", object_id=item.pk, object_repr=item.clearance_type, old_values={"status": old_status}, new_values={"status": item.status, "overridden": True}, reason=reason.strip())
    return item


def _settlement_can_be_prepared(settlement):
    return not settlement or settlement.status in {
        FinalSettlement.Status.DRAFT,
        FinalSettlement.Status.RETURNED,
        FinalSettlement.Status.REJECTED,
    }


@transaction.atomic
def prepare_final_settlement(exit_case, *, user, leave_encashment=0, other_earnings=0, other_deductions=0):
    exit_case = ExitCase.objects.select_for_update().select_related("employee").get(pk=exit_case.pk)
    current_settlement = FinalSettlement.objects.select_for_update().filter(exit_case=exit_case).first()
    if not _settlement_can_be_prepared(current_settlement):
        raise ValidationError("Approved/paid final settlement cannot be edited. Use an audited rollback process if correction is required.")
    if exit_case.clearances.exclude(status="CLEARED").exists() or not exit_case.clearances.exists():
        raise ValidationError("All assigned clearances must be cleared before final settlement preparation.")
    if not exit_case.final_working_day:
        raise ValidationError("Finalize the employee's Final Working Day before preparing settlement.")

    # Reconcile attendance through the actual exit date so partial-month pay uses one day ledger.
    # If the month is already finalized, do not mutate it: consume the existing
    # authoritative finalized ledgers and fail if that close is incomplete.
    from apps.attendance.models import AttendanceDayLedger, AttendancePeriod
    from apps.attendance.services import reconcile_attendance_day
    attendance_start = max(exit_case.final_working_day.replace(day=1), exit_case.employee.joining_date)
    attendance_period = AttendancePeriod.objects.filter(
        company=exit_case.employee.company,
        year=exit_case.final_working_day.year,
        month=exit_case.final_working_day.month,
    ).first()
    if attendance_period and attendance_period.status == AttendancePeriod.Status.FINALIZED:
        expected_days = (exit_case.final_working_day - attendance_start).days + 1
        finalized_ledgers = AttendanceDayLedger.objects.filter(
            employee=exit_case.employee,
            work_date__range=(attendance_start, exit_case.final_working_day),
            finalized=True,
        ).count()
        if finalized_ledgers != expected_days:
            raise ValidationError(
                "Finalized attendance ledger is incomplete for the exit period. Reopen/reconcile/finalize attendance before settlement."
            )
    else:
        current = attendance_start
        while current <= exit_case.final_working_day:
            reconcile_attendance_day(exit_case.employee, current, force=True)
            current += timedelta(days=1)

    from apps.payroll.services import calculate_exit_period_due
    from apps.payroll.models import PayrollRecord, PayrollPeriod
    from apps.loans.models import LoanAccount

    payroll_due, pay_snapshot = calculate_exit_period_due(exit_case.employee, exit_case.final_working_day)
    payroll_record = PayrollRecord.objects.filter(
        employee=exit_case.employee,
        period__year=exit_case.final_working_day.year,
        period__month=exit_case.final_working_day.month,
        period__status=PayrollPeriod.Status.LOCKED,
    ).select_related("period").first()
    outstanding = LoanAccount.objects.filter(employee=exit_case.employee, status=LoanAccount.Status.ACTIVE).aggregate(total=Sum("outstanding_balance"))["total"] or Decimal("0")
    leave_encashment = Decimal(str(leave_encashment or 0))
    other_earnings = Decimal(str(other_earnings or 0))
    other_deductions = Decimal(str(other_deductions or 0))
    if leave_encashment < 0 or other_earnings < 0 or other_deductions < 0:
        raise ValidationError("Final settlement monetary inputs cannot be negative.")
    net = payroll_due + leave_encashment + other_earnings - outstanding - other_deductions
    if net < 0:
        raise ValidationError(
            "Final settlement net payable cannot be negative. Reconcile recoverable loan/other deductions "
            "through an approved receivable/recovery process before preparing the payable settlement."
        )
    settlement, _ = FinalSettlement.objects.update_or_create(
        exit_case=exit_case,
        defaults={
            "payroll_record": payroll_record,
            "payroll_due_amount": payroll_due,
            "leave_encashment": leave_encashment,
            "other_earnings": other_earnings,
            "loan_outstanding_recovery": outstanding,
            "other_deductions": other_deductions,
            "net_payable": net,
            "calculation_snapshot": {
                "final_working_day": str(exit_case.final_working_day),
                "partial_month_pay": pay_snapshot,
                "loan_outstanding": str(outstanding),
            },
            "status": FinalSettlement.Status.SUBMITTED,
            "prepared_by": user,
            "reviewer_note": "",
            "last_reviewed_by": None,
            "last_reviewed_at": None,
            "approved_by": None,
            "approved_at": None,
            "submission_count": (current_settlement.submission_count + 1) if current_settlement else 1,
        },
    )
    exit_case.final_settlement_status = "SUBMITTED"
    exit_case.save(update_fields=["final_settlement_status", "updated_at"])
    existing = WorkflowTask.objects.filter(source_type="final_settlement", source_id=str(settlement.pk), status=WorkflowTask.Status.OPEN).first()
    if not existing:
        WorkflowTask.objects.create(company=exit_case.employee.company, module="FINAL_SETTLEMENT", reference_no=f"FSET-{exit_case.pk:06d}", title=f"Final settlement approval — {exit_case.employee.full_name}", assigned_role="HR Admin", priority="HIGH", source_type="final_settlement", source_id=str(settlement.pk), payload={"exit_case_id": exit_case.pk})
    log_event(user=user, action="PREPARE_FINAL_SETTLEMENT", module="exits", object_id=settlement.pk, object_repr=str(exit_case.employee), new_values={"final_working_day": str(exit_case.final_working_day), "payroll_due": str(payroll_due), "net_payable": str(net), "loan_outstanding": str(outstanding)})
    return settlement


def _assert_final_settlement_inputs_current(settlement):
    """Block approval/payment if authoritative salary/attendance/loan inputs changed."""
    from apps.payroll.services import calculate_exit_period_due
    from apps.loans.models import LoanAccount

    case = settlement.exit_case
    current_payroll_due, _ = calculate_exit_period_due(case.employee, case.final_working_day)
    if Decimal(current_payroll_due) != Decimal(settlement.payroll_due_amount):
        raise ValidationError(
            "Final-settlement payroll due changed after preparation. Rebuild and re-submit the settlement before continuing."
        )
    current_outstanding = LoanAccount.objects.filter(
        employee=case.employee, status=LoanAccount.Status.ACTIVE
    ).aggregate(total=Sum("outstanding_balance"))["total"] or Decimal("0")
    if Decimal(current_outstanding) != Decimal(settlement.loan_outstanding_recovery):
        raise ValidationError(
            "Loan outstanding changed after settlement preparation. Rebuild and re-submit the settlement before continuing."
        )
    return current_payroll_due, current_outstanding


@transaction.atomic
def review_final_settlement(settlement, *, decision, user, note=""):
    settlement = FinalSettlement.objects.select_for_update().select_related("exit_case__employee").get(pk=settlement.pk)
    decision = (decision or "").upper().strip()
    if settlement.status != FinalSettlement.Status.SUBMITTED:
        raise ValidationError("Only a submitted final settlement can be reviewed.")
    if settlement.prepared_by_id and settlement.prepared_by_id == getattr(user, "id", None):
        raise ValidationError("The settlement preparer cannot review/approve the same settlement.")
    if decision == "APPROVED":
        return approve_final_settlement(settlement, user=user, note=note)
    if decision not in {"RETURNED", "REJECTED"}:
        raise ValidationError("Unsupported final-settlement decision.")
    if not (note or "").strip():
        raise ValidationError("Reviewer reason/comment is required when returning or rejecting a final settlement.")
    settlement.status = FinalSettlement.Status.RETURNED if decision == "RETURNED" else FinalSettlement.Status.REJECTED
    settlement.reviewer_note = note.strip()
    settlement.last_reviewed_by = user
    settlement.last_reviewed_at = timezone.now()
    settlement.approved_by = None
    settlement.approved_at = None
    settlement.save(update_fields=["status", "reviewer_note", "last_reviewed_by", "last_reviewed_at", "approved_by", "approved_at", "updated_at"])
    case = settlement.exit_case
    case.final_settlement_status = settlement.status
    case.status = "SETTLEMENT_PENDING"
    case.save(update_fields=["final_settlement_status", "status", "updated_at"])
    _notify_employee(case.employee, f"Final settlement {settlement.get_status_display()}", settlement.reviewer_note, f"FSET-{case.pk:06d}")
    log_event(user=user, action=f"FINAL_SETTLEMENT_{decision}", module="exits", object_id=settlement.pk, object_repr=str(case.employee), new_values={"status": settlement.status, "reviewer_note": settlement.reviewer_note}, reason=settlement.reviewer_note)
    return settlement


@transaction.atomic
def approve_final_settlement(settlement, *, user, note=""):
    settlement = FinalSettlement.objects.select_for_update().select_related("exit_case__employee").get(pk=settlement.pk)
    if settlement.status != FinalSettlement.Status.SUBMITTED:
        raise ValidationError("Only a submitted final settlement can be approved.")
    if settlement.prepared_by_id and settlement.prepared_by_id == getattr(user, "id", None):
        raise ValidationError("The settlement preparer cannot approve the same settlement.")
    if not settlement.exit_case.final_working_day:
        raise ValidationError("Final working day must be finalized before settlement approval.")
    _assert_final_settlement_inputs_current(settlement)
    settlement.status = FinalSettlement.Status.APPROVED
    settlement.reviewer_note = (note or "").strip()
    settlement.last_reviewed_by = user
    settlement.last_reviewed_at = timezone.now()
    settlement.approved_by = user
    settlement.approved_at = timezone.now()
    settlement.save(update_fields=["status", "reviewer_note", "last_reviewed_by", "last_reviewed_at", "approved_by", "approved_at", "updated_at"])
    case = settlement.exit_case
    case.final_settlement_status = "APPROVED"
    case.status = "READY_TO_CLOSE"
    case.save(update_fields=["final_settlement_status", "status", "updated_at"])
    _notify_employee(case.employee, "Final settlement approved", note or f"Net payable: {settlement.net_payable}", f"FSET-{case.pk:06d}")
    return settlement


@transaction.atomic
def mark_settlement_paid(settlement, *, user, payment_reference):
    if not payment_reference.strip():
        raise ValidationError("Payment reference is required.")
    settlement = FinalSettlement.objects.select_for_update().select_related("exit_case__employee").get(pk=settlement.pk)
    if settlement.status != FinalSettlement.Status.APPROVED:
        raise ValidationError("Final settlement must be approved before payment posting.")
    _assert_final_settlement_inputs_current(settlement)
    from apps.loans.services import settle_employee_loans_via_exit
    settle_employee_loans_via_exit(settlement, user=user)

    settlement.status = FinalSettlement.Status.PAID
    settlement.payment_reference = payment_reference.strip()
    settlement.paid_at = timezone.now()
    settlement.save(update_fields=["status", "payment_reference", "paid_at", "updated_at"])
    case = settlement.exit_case
    case.final_settlement_status = "PAID"
    case.status = "COMPLETED"
    case.completed_at = timezone.now()
    case.employee.status = case.employee.Status.RESIGNED
    case.employee.employment_end_date = case.final_working_day
    case.employee.save(update_fields=["status", "employment_end_date", "updated_at"])
    case.save(update_fields=["final_settlement_status", "status", "completed_at", "updated_at"])
    _notify_employee(case.employee, "Exit process completed", f"Final settlement paid. Reference: {payment_reference}", f"FSET-{case.pk:06d}")
    log_event(user=user, action="FINAL_EXIT_COMPLETE", module="exits", object_id=case.pk, object_repr=str(case.employee), new_values={"payment_reference": payment_reference, "employee_status": "RESIGNED", "final_working_day": str(case.final_working_day)})
    return settlement
