<?php

namespace App\Modules\HR\Employees\Services;

use App\Modules\Admin\Tenancy\Models\Tenant;
use App\Modules\Admin\Tenancy\Services\PlanLimitEnforcementService;
use App\Modules\Admin\Tenancy\Support\PlanLimitKeys;
use App\Modules\HR\Departments\Models\Department;
use App\Modules\Customization\CustomFields\DTOs\CustomFieldsPayload;
use App\Modules\Customization\CustomFields\Services\CustomFieldValueSyncService;
use App\Modules\HR\Employees\DTOs\EmployeeFileUploads;
use App\Modules\HR\Employees\DTOs\EmployeeWriteData;
use App\Modules\HR\Employees\Events\EmployeeCreated;
use App\Modules\HR\Employees\Events\EmployeeDeactivated;
use App\Modules\HR\Employees\Events\EmployeeUpdated;
use App\Modules\HR\Employees\Models\Employee;
use App\Modules\HR\Employees\Repositories\EmployeeAddressRepository;
use App\Modules\HR\Employees\Repositories\EmployeeRepository;
use App\Modules\HR\Employees\Services\EmployeeAddressGeoResolver;
use App\Modules\HR\Positions\Models\Position;
use App\Support\SystemReferenceAssigner;
use App\Support\SystemReferenceGenerator;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;

class EmployeeService
{
    public function __construct(
        private readonly EmployeeRepository $employees,
        private readonly EmployeeAddressRepository $addresses,
        private readonly EmployeeAddressGeoResolver $addressGeo,
        private readonly EmployeeDocumentSyncService $documentSync,
        private readonly CustomFieldValueSyncService $customFieldValues,
        private readonly PlanLimitEnforcementService $planLimits,
        private readonly Dispatcher $events,
        private readonly SystemReferenceAssigner $references,
    ) {}

    public function paginateForIndex(int $perPage = 20): LengthAwarePaginator
    {
        return $this->employees->paginateForList($perPage);
    }

    public function findForEdit(Employee $employee): Employee
    {
        return $this->employees->findOrFailForEdit($employee->id);
    }

    /**
     * @return array<int, string>
     */
    public function managerOptionsForForm(int $tenantId, ?Employee $existing = null): array
    {
        $options = $this->employees->listForSelect($tenantId);
        if ($existing !== null) {
            unset($options[(int) $existing->id]);
        }

        return $options;
    }

    public function store(EmployeeWriteData $data, EmployeeFileUploads $files, CustomFieldsPayload $customFields): Employee
    {
        $tenantId = $this->assertHierarchy($data, null);
        $tenant = Tenant::query()->findOrFail($tenantId);
        $this->planLimits->assertCanIncrement($tenant, PlanLimitKeys::EMPLOYEES_LIMIT);

        $fresh = DB::transaction(function () use ($data, $files, $tenantId, $customFields) {
            $payload = $this->applySalaryPolicy(array_merge($data->toEmployeeArray(), [
                'tenant_id' => $tenantId,
                'employee_code' => $this->references->pendingPlaceholder(SystemReferenceGenerator::EMPLOYEE),
                'created_by' => auth()->id(),
            ]), null);
            $employee = $this->employees->create($payload);
            $this->references->assign($employee, SystemReferenceGenerator::EMPLOYEE, 'employee_code');
            $this->addresses->syncForEmployee($employee->id, $this->addressPayload($data));
            $this->documentSync->syncNewUploads($employee, $files);
            $this->customFieldValues->syncForEmployee($employee, $customFields->values, $customFields->files);
            $created = $this->employees->findOrFailForEdit($employee->id);
            DB::afterCommit(function () use ($created): void {
                $this->events->dispatch(new EmployeeCreated($created));
            });

            return $created;
        });

        $this->planLimits->evaluateAfterIncrement($tenant, PlanLimitKeys::EMPLOYEES_LIMIT);

        return $fresh;
    }

    public function update(Employee $employee, EmployeeWriteData $data, EmployeeFileUploads $files, CustomFieldsPayload $customFields): Employee
    {
        $this->assertHierarchy($data, $employee);

        return DB::transaction(function () use ($employee, $data, $files, $customFields) {
            $payload = $this->applySalaryPolicy(array_merge($data->toEmployeeArray(), [
                'updated_by' => auth()->id(),
            ]), $employee);
            $updated = $this->employees->update($employee, $payload);
            $this->addresses->syncForEmployee($updated->id, $this->addressPayload($data));
            $this->documentSync->syncNewUploads($updated, $files);
            $this->customFieldValues->syncForEmployee($updated, $customFields->values, $customFields->files);
            $fresh = $this->employees->findOrFailForEdit($updated->id);
            DB::afterCommit(function () use ($fresh): void {
                $this->events->dispatch(new EmployeeUpdated($fresh));
            });

            return $fresh;
        });
    }

    public function deactivate(Employee $employee): void
    {
        if (! $employee->is_active) {
            return;
        }

        $this->employees->update($employee, [
            'is_active' => false,
            'updated_by' => auth()->id(),
        ]);
        $fresh = $this->employees->findOrFailForEdit($employee->id);
        $this->events->dispatch(new EmployeeDeactivated($fresh));
    }

    private function assertHierarchy(EmployeeWriteData $data, ?Employee $existing): int
    {
        $dept = Department::query()->findOrFail($data->departmentId);
        $pos = Position::query()->findOrFail($data->positionId);
        $tenantId = (int) $dept->tenant_id;
        if ($tenantId < 1) {
            throw ValidationException::withMessages([
                'department_id' => [__('employees.validation.department_tenant')],
            ]);
        }
        if ((int) $pos->tenant_id !== $tenantId) {
            throw ValidationException::withMessages([
                'position_id' => [__('employees.validation.position_tenant')],
            ]);
        }
        if ((int) $pos->department_id !== (int) $data->departmentId) {
            throw ValidationException::withMessages([
                'position_id' => [__('employees.validation.position_department')],
            ]);
        }
        if ($existing !== null && (int) $existing->tenant_id !== $tenantId) {
            throw ValidationException::withMessages([
                'department_id' => [__('employees.validation.tenant_mismatch')],
            ]);
        }

        return $tenantId;
    }

    /** @param  array<string, mixed>  $payload @return array<string, mixed> */
    private function applySalaryPolicy(array $payload, ?Employee $existing): array
    {
        $user = auth()->user();
        if ($user !== null && (
            Gate::forUser($user)->allows('employees.salary.view')
            || Gate::forUser($user)->allows('recruitment.manage')
        )) {
            return $payload;
        }
        $payload['salary'] = $existing !== null
            ? (string) $existing->salary
            : '0.00';

        return $payload;
    }

    /** @return array<string, string|int|null> */
    private function addressPayload(EmployeeWriteData $data): array
    {
        return $this->addressGeo->toPersistenceArray([
            'address_country_id' => $data->address->countryId,
            'address_region_id' => $data->address->regionId,
            'address_city_id' => $data->address->cityId,
            'address_postal_code' => $data->address->postalCode,
            'address_street' => $data->address->street,
            'address_building_number' => $data->address->buildingNumber,
            'address_location_link' => $data->address->locationLink,
        ]);
    }
}
