<?php

namespace App\Modules\HR\Leave\Services;

use App\Models\User;
use App\Modules\Admin\Tenancy\Models\Tenant;
use App\Modules\HR\Employees\Models\Employee;
use App\Modules\HR\Employees\Repositories\EmployeeRepository;
use App\Modules\HR\Leave\DTOs\LeaveRequestSubmitData;
use App\Modules\HR\Leave\Events\LeaveRequestSubmitted;
use App\Modules\HR\Leave\Models\LeaveRequest;
use App\Modules\HR\Leave\Models\LeaveType;
use App\Modules\HR\Leave\Repositories\LeaveRequestRepository;
use App\Modules\HR\Leave\Repositories\LeaveTypeRepository;
use App\Modules\HR\Leave\Support\LeaveRequestStatus;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class LeaveRequestService
{
    public function __construct(
        private readonly LeaveRequestRepository $requests,
        private readonly LeaveTypeRepository $types,
        private readonly LeaveBalanceService $balances,
        private readonly LeaveWorkingDaysCalculator $daysCalculator,
        private readonly LeaveAttachmentStorage $attachments,
        private readonly LeaveApprovalService $approval,
        private readonly EmployeeRepository $employees,
        private readonly Dispatcher $events,
    ) {}

    public function submit(
        Tenant $tenant,
        LeaveRequestSubmitData $data,
        int $userId,
        ?UploadedFile $attachment = null,
    ): LeaveRequest {
        return DB::transaction(function () use ($tenant, $data, $userId, $attachment): LeaveRequest {
            $p = $data->toPersistenceArray();
            [$type, $employee, $days] = $this->validateAndResolve($tenant, $p, $attachment);
            $path = $attachment !== null
                ? $this->attachments->store((int) $tenant->id, $attachment)
                : null;
            $row = $this->createPending($tenant, $p, $userId, $days, $path);
            if (! $type->requires_approval) {
                $row = $this->approval->autoApprove($row, User::query()->findOrFail($userId));
            }
            DB::afterCommit(function () use ($row, $userId): void {
                $this->events->dispatch(new LeaveRequestSubmitted($row, $userId));
            });

            return $row;
        });
    }

    /**
     * @param  array{employee_id: int, leave_type_id: int, start_date: string, end_date: string, reason: ?string, is_half_day: bool}  $p
     * @return array{0: LeaveType, 1: Employee, 2: float}
     */
    private function validateAndResolve(Tenant $tenant, array $p, ?UploadedFile $attachment): array
    {
        $type = $this->resolveActiveType($tenant, (int) $p['leave_type_id'], $attachment);
        $employee = $this->employees->findForTenant((int) $tenant->id, (int) $p['employee_id']);
        if ($employee === null) {
            throw ValidationException::withMessages([
                'employee_id' => __('leave.errors.invalid_employee'),
            ]);
        }
        if ((bool) $p['is_half_day'] && ! $type->allow_half_day) {
            throw ValidationException::withMessages([
                'is_half_day' => __('leave.errors.half_day_not_allowed'),
            ]);
        }
        $hours = isset($p['hours_requested']) ? (float) $p['hours_requested'] : null;
        if ($hours !== null) {
            if (! $type->allow_hourly || ($type->max_hourly_hours && $hours > (float) $type->max_hourly_hours)
                || $p['start_date'] !== $p['end_date']) {
                throw ValidationException::withMessages([
                    'hours_requested' => __('leave.errors.hourly_not_allowed'),
                ]);
            }
            $days = $hours / (float) config('attendance.standard_work_hours', 8);
        } else {
            $days = $this->daysCalculator->resolve(
                $employee,
                $p['start_date'],
                $p['end_date'],
                (bool) $type->allow_half_day,
                (bool) $p['is_half_day'],
            );
        }
        $this->balances->assertCanRequest($type, (int) $p['employee_id'], $days, $p['start_date']);

        return [$type, $employee, $days];
    }

    private function resolveActiveType(Tenant $tenant, int $typeId, ?UploadedFile $attachment): LeaveType
    {
        $type = $this->types->findForTenant((int) $tenant->id, $typeId);
        if ($type === null || ! $type->is_active) {
            throw ValidationException::withMessages([
                'leave_type_id' => __('leave.errors.invalid_type'),
            ]);
        }
        if ($type->requires_attachment && $attachment === null) {
            throw ValidationException::withMessages([
                'attachment' => __('leave.errors.attachment_required'),
            ]);
        }

        return $type;
    }

    /**
     * @param  array{employee_id: int, leave_type_id: int, start_date: string, end_date: string, reason: ?string, is_half_day: bool}  $p
     */
    private function createPending(
        Tenant $tenant,
        array $p,
        int $userId,
        float $days,
        ?string $path,
    ): LeaveRequest {
        $row = $this->requests->create([
            'tenant_id' => $tenant->id,
            'employee_id' => $p['employee_id'],
            'requested_by' => $userId,
            'leave_type_id' => $p['leave_type_id'],
            'start_date' => CarbonImmutable::parse($p['start_date'])->toDateString(),
            'end_date' => CarbonImmutable::parse($p['end_date'])->toDateString(),
            'total_days' => $days,
            'is_half_day' => (bool) $p['is_half_day'],
            'hours_requested' => $p['hours_requested'] ?? null,
            'reason' => $p['reason'] ?? null,
            'attachment_path' => $path,
            'status' => LeaveRequestStatus::PENDING,
        ]);

        return $row->fresh(['employee', 'leaveType']);
    }
}
