<?php

namespace App\Services\Leave;

use App\Models\Leave;
use App\Models\LeaveType;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class LeaveRuleValidator
{
    public function __construct(
        private readonly LeaveEligibilityService $eligibility,
        private readonly LeaveDayCalculator $dayCalculator,
    ) {}

    /**
     * Block overlapping pending/approved leave for the same employee.
     * Rejected leaves are ignored so the employee can re-apply for the same dates.
     */
    public function assertNoOverlappingLeave(User $user, Carbon $startDate, Carbon $endDate, ?int $ignoreLeaveId = null): void
    {
        $query = Leave::query()
            ->where('company_id', $user->company_id)
            ->where('user_id', $user->id)
            ->whereIn('status', Leave::activeStatuses())
            ->whereDate('start_date', '<=', $endDate->toDateString())
            ->whereDate('end_date', '>=', $startDate->toDateString());

        if ($ignoreLeaveId !== null) {
            $query->where('id', '!=', $ignoreLeaveId);
        }

        if ($query->exists()) {
            throw ValidationException::withMessages([
                'start_date' => 'You already have a pending or approved leave that overlaps these dates.',
            ]);
        }
    }

    /**
     * Validate leave request against leave type rules.
     *
     * Returns the day-calculation result so controllers can persist `total_days`
     * without recalculating.
     *
     * @return array{calendar_days: float, chargeable_days: float, breakdown: array}
     */
    public function validateForCreate(Request $request, User $user, LeaveType $leaveType, Carbon $startDate, Carbon $endDate, bool $isHalfDay, ?Leave $existing = null): array
    {
        $leaveType->loadMissing(['rule', 'departments', 'roles', 'users']);

        $this->assertNoOverlappingLeave($user, $startDate, $endDate, $existing?->id);

        if (! $this->isEligible($user, $leaveType)) {
            throw ValidationException::withMessages([
                'leave_type_id' => 'This leave type is not assigned to the selected employee.',
            ]);
        }

        $rule = $leaveType->rule;

        if ($rule?->gender_restriction && $user->gender && $user->gender !== $rule->gender_restriction) {
            throw ValidationException::withMessages([
                'leave_type_id' => 'This leave type is restricted by gender.',
            ]);
        }

        if (! $leaveType->probation_allowed && $this->isOnProbation($user)) {
            throw ValidationException::withMessages([
                'leave_type_id' => 'This leave type is not allowed during probation.',
            ]);
        }

        if (! $leaveType->notice_period_allowed && $this->isOnNoticePeriod($user)) {
            throw ValidationException::withMessages([
                'leave_type_id' => 'This leave type is not allowed during notice period.',
            ]);
        }

        if ($rule?->requires_attachment) {
            $file = $request->file('attachment');
            $hasExistingAttachment = $existing?->documents()->exists() ?? false;
            if (! $file && ! $hasExistingAttachment) {
                throw ValidationException::withMessages([
                    'attachment' => 'Attachment is required for this leave type.',
                ]);
            }
        }

        $calc = $this->dayCalculator->calculate($user, $leaveType, $startDate, $endDate, $isHalfDay);

        $min = $rule?->min_days_per_request;
        if ($min !== null && (float) $calc['chargeable_days'] < (float) $min) {
            throw ValidationException::withMessages([
                'start_date' => "Minimum {$min} day(s) required per request for this leave type.",
            ]);
        }

        $max = $rule?->max_days_per_request;
        if ($max !== null && (float) $calc['chargeable_days'] > (float) $max) {
            throw ValidationException::withMessages([
                'end_date' => "Maximum {$max} day(s) allowed per request for this leave type.",
            ]);
        }
        
        if ($request->has('start_date') && $request->has('end_date')) {
            $start = Carbon::parse($request->input('start_date'))->startOfDay();
            $end = Carbon::parse($request->input('end_date'))->startOfDay();
            $totalleaveDays = $this->dayCalculator->calculate($user, $leaveType, $start, $end, $isHalfDay)['chargeable_days'];

            $ledger = app(LeaveLedgerService::class);
            $accrualMode = $rule?->accrual_mode ?? LeaveLedgerService::ACCRUAL_YEARLY;
            $sameType = $existing && (int) $existing->leave_type_id === (int) $leaveType->id;
            if ($accrualMode === LeaveLedgerService::ACCRUAL_MONTHLY) {
                $availableLeaveDays = $ledger->earnedLeaveDaysAsOf($user, $leaveType, $end, $sameType ? $existing->id : null);
            } else {
                $availableLeaveDays = $this->eligibility->availableLeaveDays($user, $leaveType);
                $sameYear = $existing
                    && $ledger->leaveYear(Carbon::parse($existing->start_date)) === $ledger->leaveYear($end);
                if ($sameType && $sameYear && $availableLeaveDays < PHP_FLOAT_MAX) {
                    $availableLeaveDays += (float) $existing->total_days;
                }
            }

            if ($totalleaveDays > $availableLeaveDays) {
                throw ValidationException::withMessages([
                    'start_date' => $accrualMode === LeaveLedgerService::ACCRUAL_MONTHLY
                        ? "Only {$availableLeaveDays} day(s) are generated up to {$end->toDateString()} for this leave type."
                        : "The user has only {$availableLeaveDays} day(s) available for this leave type.",
                ]);
            }
        }

        return $calc;
    }

    private function isEligible(User $user, LeaveType $leaveType): bool
    {
        if ($leaveType->apply_to_all) {
            return true;
        }

        return $this->eligibility->eligibleUsersQuery($leaveType)->where('id', $user->id)->exists();
    }

    private function isOnProbation(User $user): bool
    {
        if (array_key_exists('is_on_probation', $user->getAttributes())) {
            return (bool) $user->getAttribute('is_on_probation');
        }
        if (array_key_exists('probation_end_date', $user->getAttributes()) && $user->getAttribute('probation_end_date')) {
            return Carbon::parse($user->getAttribute('probation_end_date'))->endOfDay()->gte(now());
        }
        return false;
    }

    private function isOnNoticePeriod(User $user): bool
    {
        if (array_key_exists('is_on_notice_period', $user->getAttributes())) {
            return (bool) $user->getAttribute('is_on_notice_period');
        }
        if (array_key_exists('notice_period_end_date', $user->getAttributes()) && $user->getAttribute('notice_period_end_date')) {
            return Carbon::parse($user->getAttribute('notice_period_end_date'))->endOfDay()->gte(now());
        }
        return false;
    }
}

