<?php

namespace App\Modules\HR\Recruitment\Services;

use App\Modules\Admin\Tenancy\Models\Tenant;
use App\Modules\Customization\CustomFields\DTOs\CustomFieldsPayload;
use App\Modules\HR\Contracts\DTOs\ContractWriteData;
use App\Modules\HR\Contracts\Services\EmploymentContractService;
use App\Modules\HR\Employees\DTOs\EmployeeFileUploads;
use App\Modules\HR\Employees\DTOs\EmployeeWriteData;
use App\Modules\HR\Employees\Models\Employee;
use App\Modules\HR\Employees\Services\EmployeeService;
use App\Modules\HR\Employees\Services\EmployeeUserAccountService;
use App\Modules\HR\Recruitment\DTOs\HiringWizardData;
use App\Modules\HR\Recruitment\Events\RecruitmentCandidateHired;
use App\Modules\HR\Recruitment\Models\RecruitmentHiring;
use App\Modules\HR\Recruitment\Models\RecruitmentOfferLetter;
use App\Modules\HR\Recruitment\Repositories\RecruitmentHiringRepository;
use App\Modules\HR\Recruitment\Repositories\RecruitmentOfferLetterRepository;
use App\Modules\HR\Recruitment\Support\OfferLetterHiringGate;
use App\Modules\HR\Recruitment\Support\OfferLetterStatus;
use App\Support\SystemReferenceAssigner;
use App\Support\SystemReferenceGenerator;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class HiringWizardService
{
    public function __construct(
        private readonly RecruitmentHiringRepository $hirings,
        private readonly RecruitmentOfferLetterRepository $offers,
        private readonly HiringLinkService $links,
        private readonly HiringVacancyFulfillmentService $vacancies,
        private readonly EmployeeService $employees,
        private readonly EmployeeUserAccountService $userAccounts,
        private readonly EmploymentContractService $contracts,
        private readonly SystemReferenceAssigner $references,
        private readonly Dispatcher $events,
    ) {}

    public function complete(RecruitmentOfferLetter $offer, HiringWizardData $data, int $userId): RecruitmentHiring
    {
        $tenant = auth()->user()?->tenant;
        abort_if($tenant === null, 403);

        $hiring = DB::transaction(function () use ($offer, $data, $userId, $tenant) {
            $locked = $this->lockAcceptedOffer($offer);
            $employee = $this->employees->store(
                EmployeeWriteData::fromValidated(array_merge($data->employee, ['is_active' => true])),
                new EmployeeFileUploads,
                new CustomFieldsPayload,
            );
            $contractId = $this->storeContract($employee, $locked, $data, $userId);
            $createdUserId = $this->maybeCreateUser($tenant, $employee, $data);
            $hiredAt = now();
            $this->links->linkEmployee($locked, $employee, $hiredAt, $userId);
            $this->vacancies->recordHire((int) $locked->tenant_id, (int) $locked->vacancy_id);

            return $this->persistHiring($locked, $employee, $contractId, $createdUserId, $userId, $hiredAt, $data->notes);
        });

        $hiring->load(['employee']);
        $this->events->dispatch(new RecruitmentCandidateHired($hiring));

        return $hiring;
    }

    private function lockAcceptedOffer(RecruitmentOfferLetter $offer): RecruitmentOfferLetter
    {
        $locked = $this->offers->lockForTenant((int) $offer->tenant_id, (int) $offer->id);
        abort_if($locked === null, 404);
        OfferLetterHiringGate::assertAccepted((string) $locked->status);
        if ($locked->employee_id !== null || $locked->status === OfferLetterStatus::HIRED) {
            throw ValidationException::withMessages([
                'offer' => [__('recruitment.hiring.errors.already_hired')],
            ]);
        }
        if ($this->hirings->findHiringForOffer((int) $locked->tenant_id, (int) $locked->id) !== null) {
            throw ValidationException::withMessages([
                'offer' => [__('recruitment.hiring.errors.already_hired')],
            ]);
        }

        return $locked;
    }

    private function storeContract(
        Employee $employee,
        RecruitmentOfferLetter $offer,
        HiringWizardData $data,
        int $userId,
    ): ?int {
        if (! $data->createEmploymentContract) {
            return null;
        }
        $start = $offer->start_date?->format('Y-m-d')
            ?? (string) ($data->employee['hire_date'] ?? now()->toDateString());
        $contract = $this->contracts->store(new ContractWriteData(
            employeeId: (int) $employee->id,
            contractType: (string) $offer->employment_type,
            startDate: $start,
            endDate: null,
            notes: __('recruitment.hiring.contract_note', ['ref' => $offer->reference_code]),
            isActive: true,
        ), null, $userId);

        return (int) $contract->id;
    }

    private function maybeCreateUser(Tenant $tenant, Employee $employee, HiringWizardData $data): ?int
    {
        if (! $data->createUserAccount) {
            return null;
        }
        $user = $this->userAccounts->createAccount(
            $tenant,
            $employee,
            (string) $data->password,
            $data->roleIds,
        );

        return (int) $user->id;
    }

    private function persistHiring(
        RecruitmentOfferLetter $offer,
        Employee $employee,
        ?int $contractId,
        ?int $userId,
        int $hiredBy,
        $hiredAt,
        ?string $notes,
    ): RecruitmentHiring {
        $row = $this->hirings->create([
            'tenant_id' => (int) $offer->tenant_id,
            'reference_code' => $this->references->pendingPlaceholder(SystemReferenceGenerator::HIRING),
            'offer_letter_id' => (int) $offer->id,
            'application_id' => (int) $offer->application_id,
            'candidate_id' => (int) $offer->candidate_id,
            'vacancy_id' => (int) $offer->vacancy_id,
            'employee_id' => (int) $employee->id,
            'employment_contract_id' => $contractId,
            'user_id' => $userId,
            'hired_by' => $hiredBy,
            'hired_at' => $hiredAt,
            'notes' => $notes,
        ]);

        return $this->references->assign($row, SystemReferenceGenerator::HIRING, 'reference_code');
    }
}
