from django.core.exceptions import ValidationError
from django.db import transaction
from datetime import timedelta
from decimal import Decimal

from apps.audit.services import log_event
from .models import LeaveBalance, LeaveRequest


def count_leave_days(employee, start_date, end_date, *, half_day=False):
    """Count leave days using the same weekly-holiday/company-holiday rules as application validation."""
    from decimal import Decimal
    from django.db.models import Q
    from apps.organization.models import Holiday
    if end_date < start_date:
        return Decimal("0.00")
    weekly_name = (employee.weekly_holiday or "").strip().lower()
    holidays = set(Holiday.objects.filter(
        company=employee.company, holiday_date__range=(start_date, end_date), is_active=True,
    ).filter(Q(branch__isnull=True) | Q(branch=employee.branch)).values_list("holiday_date", flat=True))
    total = Decimal("0.00")
    current = start_date
    while current <= end_date:
        if current.strftime("%A").lower() != weekly_name and current not in holidays:
            total += Decimal("1.00")
        current += timedelta(days=1)
    if half_day and start_date == end_date and total > 0:
        return Decimal("0.50")
    return total


def validate_leave_request_input(*, employee, leave_type, start_date, end_date, days, exclude_request_id=None):
    """Validate authoritative leave boundaries, calendar-derived days and overlap.

    This service-level guard is intentionally reused by create/resubmit/approval so
    UI validation cannot be bypassed and pending requests cannot be approved with
    stale calendar-derived day counts.
    """
    if leave_type.company_id != employee.company_id:
        raise ValidationError("Leave type must belong to the employee's company.")
    if not start_date or not end_date:
        raise ValidationError("Leave start and end date are required.")
    if end_date < start_date:
        raise ValidationError("End date cannot be earlier than start date.")
    if start_date.year != end_date.year:
        raise ValidationError("Cross-year leave requires separate requests in this release.")

    from apps.attendance.services import employee_in_service_on
    if not employee_in_service_on(employee, start_date) or not employee_in_service_on(employee, end_date):
        raise ValidationError("Leave dates must fall within the employee's employment service period.")

    requested_days = Decimal(str(days or 0))
    if requested_days <= 0:
        raise ValidationError("Leave days must be greater than zero.")
    half_day = start_date == end_date and requested_days == Decimal("0.50")
    if requested_days < Decimal("1.00") and not half_day:
        raise ValidationError("Only a valid single-date half-day leave may be less than one day.")
    if half_day and not leave_type.half_day_allowed:
        raise ValidationError("The selected leave type does not allow half-day leave.")

    authoritative_days = count_leave_days(employee, start_date, end_date, half_day=half_day)
    if authoritative_days <= 0:
        raise ValidationError("The selected date range has no working day according to the current holiday setup.")
    if authoritative_days != requested_days:
        raise ValidationError(
            "Leave day count no longer matches the current weekly-holiday/holiday setup. "
            "Correct and resubmit the leave request before approval."
        )

    overlap = LeaveRequest.objects.filter(
        employee=employee,
        status__in=["PENDING", "UNDER_REVIEW", "APPROVED"],
        start_date__lte=end_date,
        end_date__gte=start_date,
    )
    if exclude_request_id is not None:
        overlap = overlap.exclude(pk=exclude_request_id)
    if overlap.exists():
        raise ValidationError("Another pending/approved leave request overlaps this date range.")
    return authoritative_days


@transaction.atomic
def create_leave_request(*, employee, leave_type, start_date, end_date, days, reason="", user=None, request=None):
    authoritative_days = validate_leave_request_input(
        employee=employee, leave_type=leave_type, start_date=start_date, end_date=end_date, days=days
    )
    req = LeaveRequest.objects.create(
        employee=employee,
        leave_type=leave_type,
        start_date=start_date,
        end_date=end_date,
        days=authoritative_days,
        reason=reason.strip(),
        status="PENDING",
    )
    from apps.organization.services import get_effective_policy
    from apps.workflow.services import create_workflow_task_for_employee
    policy = get_effective_policy(employee.company, start_date)
    create_workflow_task_for_employee(
        employee=employee,
        module="Leave",
        reference_no=f"LEAVE-{req.pk:06d}",
        title=f"Leave application: {employee.full_name} / {start_date} to {end_date}",
        source_type="leave_request",
        source_id=req.pk,
        priority="NORMAL",
        preferred_role="Manager" if policy.leave_manager_approval_required else "HR Admin",
    )
    log_event(
        user=user,
        action="APPLY",
        module="leave",
        object_id=req.pk,
        object_repr=f"{employee} / {start_date} to {end_date}",
        new_values={"leave_type": leave_type.code, "days": str(authoritative_days), "status": req.status},
        reason=reason.strip(),
        request=request,
    )
    return req


@transaction.atomic
def decide_leave_request(request_obj, *, decision, user=None, note=""):
    req = LeaveRequest.objects.select_for_update().select_related("employee", "leave_type").get(pk=request_obj.pk)
    decision = decision.upper().strip()
    if decision not in {"APPROVED", "REJECTED", "RETURNED"}:
        raise ValidationError("Unsupported leave decision.")
    if decision in {"REJECTED", "RETURNED"} and not note.strip():
        raise ValidationError("Reason/comment is required.")
    if req.status not in {"PENDING", "UNDER_REVIEW", "RETURNED"}:
        raise ValidationError("This leave request has already been finalized.")
    if req.start_date.year != req.end_date.year:
        raise ValidationError("Cross-year leave requires separate requests in this release.")

    if decision == "APPROVED":
        validate_leave_request_input(
            employee=req.employee, leave_type=req.leave_type, start_date=req.start_date, end_date=req.end_date,
            days=req.days, exclude_request_id=req.pk,
        )

    old_status = req.status
    if decision == "APPROVED":
        # An approved leave changes the authoritative attendance day ledger.
        # Do not allow that mutation underneath a finalized attendance close.
        from apps.attendance.models import AttendancePeriod
        current = req.start_date
        while current <= req.end_date:
            if AttendancePeriod.objects.filter(
                company=req.employee.company, year=current.year, month=current.month,
                status=AttendancePeriod.Status.FINALIZED,
            ).exists():
                raise ValidationError(
                    "Attendance is finalized for one or more leave dates. Reopen the affected attendance period before approving leave."
                )
            current += timedelta(days=1)
        balance = LeaveBalance.objects.select_for_update().filter(
            employee=req.employee,
            leave_type=req.leave_type,
            year=req.start_date.year,
        ).first()
        if not balance:
            raise ValidationError("Leave balance is not configured for this employee/year.")
        if balance.available < req.days:
            raise ValidationError("Insufficient leave balance.")
        balance.used += req.days
        balance.save(update_fields=["used", "updated_at"])
        req.status = "APPROVED"
        req.reviewer_note = note.strip()
        req.rejection_reason = ""
    elif decision == "REJECTED":
        req.status = "REJECTED"
        req.rejection_reason = note.strip()
        req.reviewer_note = ""
    else:
        req.status = "RETURNED"
        req.reviewer_note = note.strip()
        req.rejection_reason = ""
    req.save(update_fields=["status", "reviewer_note", "rejection_reason", "updated_at"])
    if decision == "APPROVED":
        from apps.attendance.services import reconcile_attendance_day
        current = req.start_date
        while current <= req.end_date:
            reconcile_attendance_day(req.employee, current, force=True)
            current += timedelta(days=1)
    log_event(
        user=user,
        action=decision,
        module="leave",
        object_id=req.pk,
        object_repr=f"{req.employee} / {req.start_date} to {req.end_date}",
        old_values={"status": old_status},
        new_values={"status": req.status},
        reason=note.strip(),
    )
    return req


@transaction.atomic
def resubmit_leave_request(request_obj, *, leave_type, start_date, end_date, days, reason="", user=None, request=None):
    req = LeaveRequest.objects.select_for_update().select_related("employee").get(pk=request_obj.pk)
    if req.status != "RETURNED":
        raise ValidationError("Only a returned leave request can be resubmitted.")
    if getattr(req.employee, "user_id", None) != getattr(user, "id", None):
        raise ValidationError("Only the employee who submitted this leave may resubmit it.")
    authoritative_days = validate_leave_request_input(
        employee=req.employee, leave_type=leave_type, start_date=start_date, end_date=end_date,
        days=days, exclude_request_id=req.pk,
    )
    req.leave_type = leave_type
    req.start_date = start_date
    req.end_date = end_date
    req.days = authoritative_days
    req.reason = (reason or "").strip()
    req.status = "PENDING"
    req.reviewer_note = ""
    req.rejection_reason = ""
    req.save(update_fields=["leave_type", "start_date", "end_date", "days", "reason", "status", "reviewer_note", "rejection_reason", "updated_at"])
    from apps.organization.services import get_effective_policy
    from apps.workflow.services import create_workflow_task_for_employee
    policy = get_effective_policy(req.employee.company, start_date)
    create_workflow_task_for_employee(
        employee=req.employee, module="Leave", reference_no=f"LEAVE-{req.pk:06d}",
        title=f"Resubmitted leave: {req.employee.full_name} / {start_date} to {end_date}",
        source_type="leave_request", source_id=req.pk, priority="NORMAL",
        preferred_role="Manager" if policy.leave_manager_approval_required else "HR Admin",
        payload={"resubmitted": True},
    )
    log_event(user=user, action="RESUBMIT", module="leave", object_id=req.pk, object_repr=f"{req.employee} / {start_date} to {end_date}", new_values={"status": req.status, "days": str(authoritative_days)}, reason=req.reason, request=request)
    return req


@transaction.atomic
def cancel_approved_leave(request_obj, *, user=None, reason=""):
    req = LeaveRequest.objects.select_for_update().select_related("employee", "leave_type").get(pk=request_obj.pk)
    if req.status != "APPROVED":
        raise ValidationError("Only an approved leave request can be cancelled/reversed.")
    if not (reason or "").strip():
        raise ValidationError("Cancellation reason is required.")
    from apps.attendance.models import AttendancePeriod
    current = req.start_date
    while current <= req.end_date:
        period = AttendancePeriod.objects.filter(company=req.employee.company, year=current.year, month=current.month).first()
        if period and period.status == AttendancePeriod.Status.FINALIZED:
            raise ValidationError("Attendance is finalized for one or more leave dates. Reopen the affected attendance period before cancelling leave.")
        current += timedelta(days=1)
    balance = LeaveBalance.objects.select_for_update().filter(employee=req.employee, leave_type=req.leave_type, year=req.start_date.year).first()
    if balance:
        balance.used = max(0, balance.used - req.days)
        balance.save(update_fields=["used", "updated_at"])
    old_status = req.status
    req.status = "CANCELLED"
    req.reviewer_note = f"Cancelled: {(reason or '').strip()}"
    req.save(update_fields=["status", "reviewer_note", "updated_at"])
    from apps.attendance.services import restore_attendance_after_leave_change
    current = req.start_date
    while current <= req.end_date:
        restore_attendance_after_leave_change(req.employee, current)
        current += timedelta(days=1)
    log_event(user=user, action="CANCEL_APPROVED", module="leave", object_id=req.pk, object_repr=f"{req.employee} / {req.start_date} to {req.end_date}", old_values={"status": old_status}, new_values={"status": req.status}, reason=(reason or "").strip())
    return req


@transaction.atomic
def initialize_leave_year(*, company, year, user=None):
    """Create safe opening/allocation rows from the prior year's approved balance policy."""
    from apps.employees.models import Employee
    from .models import LeaveType
    if year < 2020:
        raise ValidationError("Invalid leave year.")
    employees = Employee.objects.filter(company=company, status__in=[Employee.Status.ACTIVE, Employee.Status.PROBATION, Employee.Status.CONFIRMED])
    leave_types = LeaveType.objects.filter(company=company, is_active=True)
    created = updated = 0
    for employee in employees:
        for leave_type in leave_types:
            previous = LeaveBalance.objects.filter(employee=employee, leave_type=leave_type, year=year-1).first()
            opening = 0
            if previous and leave_type.carry_forward:
                opening = max(0, previous.available)
                if leave_type.max_carry_forward and opening > leave_type.max_carry_forward:
                    opening = leave_type.max_carry_forward
            balance, was_created = LeaveBalance.objects.get_or_create(
                employee=employee, leave_type=leave_type, year=year,
                defaults={"opening": opening, "allocated": leave_type.yearly_allocation, "used": 0, "adjusted": 0},
            )
            if was_created:
                created += 1
            else:
                # Do not rewrite a year after usage has begun.
                if balance.used == 0:
                    balance.opening = opening
                    balance.allocated = leave_type.yearly_allocation
                    balance.save(update_fields=["opening", "allocated", "updated_at"])
                    updated += 1
    log_event(user=user, company=company, action="INITIALIZE_LEAVE_YEAR", module="leave", object_id=str(year), object_repr=f"{company.code} leave year {year}", new_values={"created": created, "updated": updated, "year": year})
    return {"created": created, "updated": updated}
