"""Celery tasks for the HR module.

All tasks are idempotent and safe to retry. Business logic is delegated to
service-layer functions; tasks only orchestrate scheduling and iteration.

Scheduled tasks (registered in config/celery.py beat_schedule):
- hr.accrue_monthly_leave        — 1st of each month, midnight
- hr.check_document_expiry       — daily at 08:00
- hr.sync_on_leave_status        — daily at 00:05
"""

from __future__ import annotations

import structlog
from celery import shared_task
from django.utils import timezone

_log = structlog.get_logger("simorgh.hr.tasks")


@shared_task(name="hr.accrue_monthly_leave", bind=True, max_retries=3, default_retry_delay=300)
def accrue_monthly_leave(self) -> int:
    """Allocate monthly leave accruals for all active employees.

    Processes every LeaveType with accrual_method='monthly' across all tenants.
    Calculates the monthly fraction of max_days_per_year and adds it to the
    employee's LeaveBalance for the current year.

    Returns the total number of balance records updated.
    """
    from decimal import Decimal, ROUND_HALF_UP

    from simorgh.apps.hr_core.models import Employee, EmployeeStatus, LeaveBalance, LeaveType

    today = timezone.localdate()
    year = today.year
    count = 0

    monthly_types = LeaveType.objects.filter(accrual_method="monthly").select_related()

    for leave_type in monthly_types:
        tenant_id = leave_type.tenant_id
        active_employees = Employee.objects.filter(
            tenant_id=tenant_id,
            is_deleted=False,
            status__in=[EmployeeStatus.ACTIVE, EmployeeStatus.ON_LEAVE],
        )
        if leave_type.max_days_per_year is None:
            continue
        monthly_days = (Decimal(str(leave_type.max_days_per_year)) / 12).quantize(
            Decimal("0.01"), rounding=ROUND_HALF_UP
        )

        for employee in active_employees:
            balance, created = LeaveBalance.objects.get_or_create(
                employee=employee,
                leave_type=leave_type,
                year=year,
                defaults={
                    "tenant_id": tenant_id,
                    "organization_node_id": employee.organization_node_id,
                    "allocated": Decimal(0),
                    "used": Decimal(0),
                    "pending": Decimal(0),
                },
            )
            balance.allocated += monthly_days
            balance.save(update_fields=["allocated", "updated_at"])
            count += 1

    _log.info("hr.tasks.accrue_monthly_leave.done", count=count, year=year)
    return count


@shared_task(name="hr.check_document_expiry", bind=True, max_retries=3, default_retry_delay=60)
def check_document_expiry(self) -> int:
    """Emit events for employee documents expiring within the warning window.

    Reads hr.document_expiry_warning_days from tenant settings (default 30).
    Dispatches hr.document.expiring_soon for each affected document.

    Returns the number of expiry events dispatched.
    """
    from datetime import timedelta

    from simorgh.apps.events.bus import dispatch_async
    from simorgh.apps.hr_core.models import EmployeeDocument

    today = timezone.localdate()
    count = 0

    # Find docs expiring within the next 30 days that haven't been flagged yet.
    warning_until = today + timedelta(days=30)
    expiring_docs = EmployeeDocument.objects.filter(
        expiry_date__gte=today,
        expiry_date__lte=warning_until,
        is_verified=True,
    ).select_related("employee")

    for doc in expiring_docs:
        try:
            dispatch_async(
                "hr.document.expiring_soon",
                {
                    "document_id": doc.pk,
                    "employee_id": doc.employee_id,
                    "tenant_id": doc.tenant_id,
                    "expiry_date": doc.expiry_date.isoformat(),
                    "doc_type": doc.doc_type,
                },
                tenant_id=doc.tenant_id,
            )
            count += 1
        except Exception as exc:  # noqa: BLE001
            _log.warning(
                "hr.tasks.document_expiry.dispatch_failed",
                document_id=doc.pk,
                error=str(exc),
            )

    _log.info("hr.tasks.check_document_expiry.done", count=count)
    return count


@shared_task(name="hr.sync_on_leave_status", bind=True, max_retries=3, default_retry_delay=60)
def sync_on_leave_status(self) -> dict:
    """Daily reconciliation of Employee.status against active leave requests.

    - Employees with an approved leave covering today → status = on_leave
    - Employees currently on_leave with no active approved leave → status = active

    Returns a dict with ``set_on_leave`` and ``restored`` counts.
    """
    from simorgh.apps.hr_core.models import Employee, EmployeeStatus, LeaveRequest, LeaveRequestStatus

    today = timezone.localdate()
    set_on_leave = 0
    restored = 0

    # Employees who should be on_leave today.
    on_leave_employee_ids = set(
        LeaveRequest.objects.filter(
            status=LeaveRequestStatus.APPROVED,
            from_date__lte=today,
            to_date__gte=today,
        ).values_list("employee_id", flat=True)
    )

    # Set active employees to on_leave.
    updated = Employee.objects.filter(
        pk__in=on_leave_employee_ids,
        status=EmployeeStatus.ACTIVE,
        is_deleted=False,
    ).update(status=EmployeeStatus.ON_LEAVE)
    set_on_leave = updated

    # Restore employees currently on_leave but with no active approved leave.
    stale_on_leave = Employee.objects.filter(
        status=EmployeeStatus.ON_LEAVE,
        is_deleted=False,
    ).exclude(pk__in=on_leave_employee_ids)
    restored = stale_on_leave.update(status=EmployeeStatus.ACTIVE)

    _log.info(
        "hr.tasks.sync_on_leave_status.done",
        set_on_leave=set_on_leave,
        restored=restored,
        date=today.isoformat(),
    )
    return {"set_on_leave": set_on_leave, "restored": restored}
