from __future__ import annotations

from datetime import date
from decimal import Decimal
from typing import TYPE_CHECKING

import structlog
from django.db import models, transaction
from django.utils import timezone

from simorgh.apps.hr_core.services.working_days import (
    _get_tenant_holidays,
    _get_tenant_work_days,
    count_working_days,
)

_log = structlog.get_logger("simorgh.hr_core.services.leave")

if TYPE_CHECKING:
    from simorgh.apps.hr_core.models import (
        LeaveRequest,
        LeaveType,
        PublicHoliday,
        Employee,
        LeaveBalance as LeaveBalanceModel,
    )


# ── LeaveType ────────────────────────────────────────────────────────────

def create_leave_type(
    *,
    tenant_id: int,
    name: str,
    code: str,
    color: str = "#4A90D9",
    is_paid: bool = True,
    requires_approval: bool = True,
    max_days_per_year=None,
    carry_forward_days=0,
    accrual_method: str = "upfront",
) -> "LeaveType":
    from simorgh.apps.hr_core.models import LeaveType

    return LeaveType.objects.create(
        tenant_id=tenant_id,
        name=name.strip(),
        code=code.strip(),
        color=color,
        is_paid=is_paid,
        requires_approval=requires_approval,
        max_days_per_year=max_days_per_year,
        carry_forward_days=carry_forward_days,
        accrual_method=accrual_method,
    )


def update_leave_type(
    leave_type: "LeaveType",
    *,
    name: str | None = None,
    code: str | None = None,
    color: str | None = None,
    is_paid: bool | None = None,
    requires_approval: bool | None = None,
    max_days_per_year=None,
    carry_forward_days=None,
    accrual_method: str | None = None,
) -> "LeaveType":
    if name is not None:
        leave_type.name = name.strip()
    if code is not None:
        leave_type.code = code.strip()
    if color is not None:
        leave_type.color = color
    if is_paid is not None:
        leave_type.is_paid = is_paid
    if requires_approval is not None:
        leave_type.requires_approval = requires_approval
    if max_days_per_year is not None:
        leave_type.max_days_per_year = max_days_per_year
    if carry_forward_days is not None:
        leave_type.carry_forward_days = carry_forward_days
    if accrual_method is not None:
        leave_type.accrual_method = accrual_method
    leave_type.save()
    return leave_type


def delete_leave_type(leave_type: "LeaveType") -> None:
    leave_type.delete()


# ── PublicHoliday ────────────────────────────────────────────────────────

def create_public_holiday(
    *,
    tenant_id: int,
    date: date,
    name: str,
    calendar_type: str = "gregorian",
    is_recurring: bool = False,
) -> "PublicHoliday":
    from simorgh.apps.hr_core.models import PublicHoliday

    return PublicHoliday.objects.create(
        tenant_id=tenant_id,
        date=date,
        name=name.strip(),
        calendar_type=calendar_type,
        is_recurring=is_recurring,
    )


def delete_public_holiday(holiday: "PublicHoliday") -> None:
    holiday.delete()


# ── LeaveBalance ─────────────────────────────────────────────────────────

def allocate_leave_balance(
    *,
    employee: "Employee",
    leave_type: "LeaveType",
    year: int,
    days,
) -> "LeaveBalanceModel":
    from simorgh.apps.hr_core.models import LeaveBalance

    obj, created = LeaveBalance.objects.get_or_create(
        employee=employee,
        leave_type=leave_type,
        year=year,
        defaults={
            "tenant_id": employee.tenant_id,
            "organization_node_id": employee.organization_node_id,
            "allocated": Decimal(str(days)),
            "used": 0,
            "pending": 0,
        },
    )
    if not created:
        obj.allocated = Decimal(str(days))
        obj.save()
    return obj


def _update_balance_on_approve(request: "LeaveRequest") -> None:
    from simorgh.apps.hr_core.models import LeaveBalance

    year = request.from_date.year
    try:
        balance = (
            LeaveBalance.objects
            .select_for_update()
            .get(
                employee=request.employee,
                leave_type=request.leave_type,
                year=year,
            )
        )
        days = Decimal(str(request.days_requested))
        balance.pending = max(Decimal(0), balance.pending - days)
        balance.used += days
        balance.save()
    except LeaveBalance.DoesNotExist:
        pass


def _update_balance_on_cancel_or_reject(request: "LeaveRequest") -> None:
    from simorgh.apps.hr_core.models import LeaveBalance, LeaveRequestStatus

    if request.status not in (
        LeaveRequestStatus.PENDING, LeaveRequestStatus.APPROVED
    ):
        return
    year = request.from_date.year
    try:
        balance = (
            LeaveBalance.objects
            .select_for_update()
            .get(
                employee=request.employee,
                leave_type=request.leave_type,
                year=year,
            )
        )
        days = Decimal(str(request.days_requested))
        if request.status == LeaveRequestStatus.PENDING:
            balance.pending = max(Decimal(0), balance.pending - days)
        else:
            balance.used = max(Decimal(0), balance.used - days)
        balance.save()
    except LeaveBalance.DoesNotExist:
        pass


# ── LeaveRequest ─────────────────────────────────────────────────────────

def create_leave_request(
    *,
    tenant_id: int,
    employee: "Employee",
    leave_type: "LeaveType",
    from_date: date,
    to_date: date,
    reason: str = "",
) -> "LeaveRequest":
    from simorgh.apps.hr_core.models import LeaveRequest

    if from_date > to_date:
        raise ValueError("from_date must be before or equal to to_date.")

    work_days = _get_tenant_work_days(tenant_id)
    holidays = _get_tenant_holidays(tenant_id, from_date, to_date)
    days = Decimal(str(count_working_days(from_date, to_date, work_days=work_days, holiday_dates=holidays)))

    return LeaveRequest.objects.create(
        tenant_id=tenant_id,
        organization_node_id=employee.organization_node_id,
        employee=employee,
        leave_type=leave_type,
        from_date=from_date,
        to_date=to_date,
        days_requested=days,
        reason=reason,
        status="draft",
    )


@transaction.atomic
def submit_leave_request(request: "LeaveRequest") -> "LeaveRequest":
    from simorgh.apps.events.bus import dispatch_async
    from simorgh.apps.hr_core.models import LeaveBalance, LeaveRequest, LeaveRequestStatus

    if request.status != LeaveRequestStatus.DRAFT:
        raise ValueError("Only draft requests can be submitted.")

    # Overlap check with row-level lock
    overlapping = (
        LeaveRequest.objects
        .select_for_update()
        .filter(
            employee=request.employee,
            status__in=[LeaveRequestStatus.PENDING, LeaveRequestStatus.APPROVED],
            from_date__lte=request.to_date,
            to_date__gte=request.from_date,
        ).exclude(pk=request.pk)
    )
    if overlapping.exists():
        raise ValueError("You already have a leave request covering these dates.")

    # Balance check with row-level lock
    year = request.from_date.year
    try:
        balance = (
            LeaveBalance.objects
            .select_for_update()
            .get(
                employee=request.employee,
                leave_type=request.leave_type,
                year=year,
            )
        )
        allow_neg = False
        try:
            from simorgh.apps.app_settings.services import resolve
            allow_neg = resolve("hr.leave.allow_negative_balance", tenant_id=request.tenant_id) or False
        except Exception:
            pass
        if not allow_neg:
            remaining = balance.allocated - balance.used - balance.pending
            if Decimal(str(request.days_requested)) > remaining:
                raise ValueError(
                    f"Insufficient balance. You only have {remaining} days remaining."
                )
        balance.pending += Decimal(str(request.days_requested))
        balance.save()
    except LeaveBalance.DoesNotExist:
        pass

    if not request.leave_type.requires_approval:
        request.status = LeaveRequestStatus.APPROVED
        request.submitted_at = timezone.now()
        request.reviewed_at = timezone.now()
        request.save()
        _update_balance_on_approve(request)
        dispatch_async(
            "hr.leave.approved",
            {
                "request_id": request.pk,
                "employee_id": request.employee_id,
                "approved_by": None,
                "tenant_id": request.tenant_id,
            },
            tenant_id=request.tenant_id,
        )
    else:
        request.status = LeaveRequestStatus.PENDING
        request.submitted_at = timezone.now()
        request.save()
        dispatch_async(
            "hr.leave.requested",
            {
                "request_id": request.pk,
                "employee_id": request.employee_id,
                "leave_type_id": request.leave_type_id,
                "tenant_id": request.tenant_id,
            },
            tenant_id=request.tenant_id,
        )

    return request


def _restore_employee_status_if_needed(request: "LeaveRequest") -> None:
    from simorgh.apps.hr_core.models.choices import EmployeeStatus
    from simorgh.apps.hr_core.models.leave import LeaveRequestStatus

    employee = request.employee
    if employee.status != EmployeeStatus.ON_LEAVE:
        return
    today = date.today()
    has_other = request.__class__.objects.filter(
        employee=employee,
        status=LeaveRequestStatus.APPROVED,
        from_date__lte=today,
        to_date__gte=today,
    ).exclude(pk=request.pk).exists()
    if not has_other:
        employee.status = EmployeeStatus.ACTIVE
        employee.save(update_fields=["status"])


@transaction.atomic
def approve_leave_request(
    request: "LeaveRequest",
    *,
    reviewed_by,
    note: str = "",
) -> "LeaveRequest":
    from simorgh.apps.events.bus import dispatch_async
    from simorgh.apps.hr_core.models.choices import EmployeeStatus
    from simorgh.apps.hr_core.models.leave import LeaveRequestStatus

    if request.status != LeaveRequestStatus.PENDING:
        raise ValueError("Only pending requests can be approved.")

    request.status = LeaveRequestStatus.APPROVED
    request.reviewed_by = reviewed_by
    request.reviewed_at = timezone.now()
    request.review_note = note
    request.save()

    _update_balance_on_approve(request)

    today = date.today()
    employee = request.employee
    if request.from_date <= today <= request.to_date:
        if employee.status == EmployeeStatus.ACTIVE:
            employee.status = EmployeeStatus.ON_LEAVE
            employee.save(update_fields=["status"])

    dispatch_async(
        "hr.leave.approved",
        {
            "request_id": request.pk,
            "employee_id": request.employee_id,
            "approved_by": reviewed_by.pk if reviewed_by else None,
            "tenant_id": request.tenant_id,
        },
        tenant_id=request.tenant_id,
    )

    from simorgh.core.audit import record_event as _audit
    _audit(
        "approved",
        resource_type="hr.leave_request",
        resource_id=str(request.pk),
        after={
            "employee_id": request.employee_id,
            "from_date": request.from_date.isoformat(),
            "to_date": request.to_date.isoformat(),
            "days_requested": str(request.days_requested),
            "approved_by": reviewed_by.pk if reviewed_by else None,
        },
        tenant_id=request.tenant_id,
    )
    _log.info(
        "hr_core.services.approve_leave_request.done",
        leave_request_id=request.pk,
        employee_id=request.employee_id,
        tenant_id=request.tenant_id,
        days_requested=str(request.days_requested),
    )
    return request


@transaction.atomic
def reject_leave_request(
    request: "LeaveRequest",
    *,
    reviewed_by,
    note: str,
) -> "LeaveRequest":
    from simorgh.apps.events.bus import dispatch_async
    from simorgh.apps.hr_core.models.leave import LeaveRequestStatus

    if request.status != LeaveRequestStatus.PENDING:
        raise ValueError("Only pending requests can be rejected.")
    if not note or not note.strip():
        raise ValueError("A rejection note is required.")

    _update_balance_on_cancel_or_reject(request)
    _restore_employee_status_if_needed(request)

    request.status = LeaveRequestStatus.REJECTED
    request.reviewed_by = reviewed_by
    request.reviewed_at = timezone.now()
    request.review_note = note.strip()
    request.save()

    dispatch_async(
        "hr.leave.rejected",
        {
            "request_id": request.pk,
            "employee_id": request.employee_id,
            "rejected_by": reviewed_by.pk if reviewed_by else None,
            "reason": note,
            "tenant_id": request.tenant_id,
        },
        tenant_id=request.tenant_id,
    )

    from simorgh.core.audit import record_event as _audit
    _audit(
        "rejected",
        resource_type="hr.leave_request",
        resource_id=str(request.pk),
        after={"rejected_by": reviewed_by.pk if reviewed_by else None, "reason": note},
        tenant_id=request.tenant_id,
    )
    _log.info(
        "hr_core.services.reject_leave_request.done",
        leave_request_id=request.pk,
        employee_id=request.employee_id,
        tenant_id=request.tenant_id,
    )
    return request


@transaction.atomic
def cancel_leave_request(request: "LeaveRequest") -> "LeaveRequest":
    from simorgh.apps.events.bus import dispatch_async
    from simorgh.apps.hr_core.models.leave import LeaveRequestStatus

    if request.status == LeaveRequestStatus.REJECTED:
        raise ValueError("Rejected requests cannot be cancelled.")
    if request.status == LeaveRequestStatus.CANCELLED:
        raise ValueError("Request is already cancelled.")

    _update_balance_on_cancel_or_reject(request)
    _restore_employee_status_if_needed(request)

    request.status = LeaveRequestStatus.CANCELLED
    request.save()

    dispatch_async(
        "hr.leave.cancelled",
        {
            "request_id": request.pk,
            "employee_id": request.employee_id,
            "tenant_id": request.tenant_id,
        },
        tenant_id=request.tenant_id,
    )
    return request
