<?php

namespace App\Domain\Processing\Services;

use App\Domain\Applicants\Models\Applicant;
use App\Domain\Audit\Enums\ActivityCategory;
use App\Domain\Audit\Services\ActivityLogger;
use App\Domain\Commissions\Enums\CommissionTrigger;
use App\Domain\Commissions\Services\CommissionEngine;
use App\Domain\Communication\Services\Notifier;
use App\Domain\Identity\Models\User;
use App\Domain\Processing\Enums\VisaStatus;
use App\Domain\Processing\Models\MedicalRecord;
use App\Domain\Processing\Models\VisaRecord;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class VisaService
{
    public function __construct(
        private readonly ActivityLogger $activity,
        private readonly CommissionEngine $commissions,
        private readonly Notifier $notifier,
    ) {}

    /**
     * @param  array<string, mixed>  $attributes
     *
     * @throws ValidationException
     */
    public function open(Applicant $applicant, array $attributes, User $actor): VisaRecord
    {
        $existing = VisaRecord::where('applicant_id', $applicant->getKey())
            ->whereNotIn('status', [
                VisaStatus::Rejected->value,
                VisaStatus::Cancelled->value,
            ])
            ->exists();

        if ($existing) {
            throw ValidationException::withMessages([
                'applicant' => ['This applicant already has a visa application in progress.'],
            ]);
        }

        return DB::transaction(function () use ($applicant, $attributes, $actor) {
            $record = VisaRecord::create(array_merge($attributes, [
                'company_id' => $applicant->company_id,
                'applicant_id' => $applicant->getKey(),
                'employer_id' => $attributes['employer_id'] ?? $applicant->employer_id,
                'job_demand_id' => $attributes['job_demand_id'] ?? $applicant->job_demand_id,
                'country' => strtoupper($attributes['country'] ?? $applicant->destination_country ?? ''),
                'status' => VisaStatus::NotStarted->value,
                'recorded_by' => $actor->getKey(),
            ]));

            $this->activity->log(ActivityCategory::Processing, 'visa.opened', [
                'subject' => $applicant,
                'actor' => $actor,
                'title' => 'Visa application opened',
                'description' => $record->country.($record->visa_type ? " · {$record->visa_type}" : ''),
                'visibility' => 'shared',
            ]);

            return $record;
        });
    }

    /** @throws ValidationException */
    public function changeStatus(
        VisaRecord $record,
        VisaStatus $target,
        User $actor,
        ?string $reason = null,
        array $attributes = [],
    ): VisaRecord {
        if ($record->status === $target) {
            throw ValidationException::withMessages([
                'status' => ["The application is already {$target->label()}."],
            ]);
        }

        if ($record->status->isClosed()) {
            throw ValidationException::withMessages([
                'status' => ["A {$record->status->label()} application cannot be changed."],
            ]);
        }

        if ($target->requiresReason() && blank($reason)) {
            throw ValidationException::withMessages([
                'reason' => ["Say why the application is {$target->label()}."],
            ]);
        }

        // Submitting someone to an embassy without a current medical wastes the
        // fee and the applicant's time (§8, §21).
        if ($target === VisaStatus::Submitted) {
            $this->assertMedicallyCleared($record);
        }

        if ($target === VisaStatus::Approved && blank($attributes['visa_number'] ?? $record->visa_number)) {
            throw ValidationException::withMessages([
                'visa_number' => ['Record the visa number when marking an application approved.'],
            ]);
        }

        return DB::transaction(function () use ($record, $target, $actor, $reason, $attributes) {
            $previous = $record->status;

            $record->fill($attributes);
            $record->status = $target;

            if ($target === VisaStatus::Submitted && $record->submitted_on === null) {
                $record->submitted_on = now()->toDateString();
            }

            if ($target === VisaStatus::Approved) {
                $record->approved_on = now()->toDateString();
            }

            if ($target === VisaStatus::Rejected) {
                $record->rejection_reason = $reason;
            }

            $record->save();

            $this->activity->log(ActivityCategory::Processing, 'visa.status_changed', [
                'subject' => $record->applicant,
                'actor' => $actor,
                'title' => 'Visa status changed',
                // The portal sees the sanitised description (§23).
                'description' => $previous->publicLabel().' → '.$target->publicLabel(),
                'old' => ['status' => $previous->value],
                'new' => ['status' => $target->value],
                'metadata' => $reason ? ['reason' => $reason] : null,
                'visibility' => 'shared',
            ]);

            // The applicant hears the sanitised label, never the internal one.
            $this->notifier->send('visa_update', $record->applicant, [
                'applicant_name' => $record->applicant->full_name,
                'status' => $target->publicLabel(),
            ], $record, $actor);

            if ($target === VisaStatus::Approved) {
                $this->commissions->handle(
                    CommissionTrigger::VisaApproved,
                    $record->applicant,
                    $record,
                );
            }

            return $record;
        });
    }

    /** @throws ValidationException */
    private function assertMedicallyCleared(VisaRecord $record): void
    {
        $medical = MedicalRecord::where('applicant_id', $record->applicant_id)
            ->current()
            ->first();

        if ($medical === null) {
            throw ValidationException::withMessages([
                'status' => ['No medical examination is on file for this applicant.'],
            ]);
        }

        if (! $medical->clearsForDeployment()) {
            $status = $medical->effectiveStatus();

            throw ValidationException::withMessages([
                'status' => ["The applicant's medical is {$status->label()}; a current fit result is needed before submission."],
            ]);
        }

        // Most destinations reject a medical certificate expiring during
        // processing, so a certificate about to lapse is refused here too.
        if ($medical->expiry_date !== null && $medical->expiry_date->isBefore(now()->addMonth())) {
            throw ValidationException::withMessages([
                'status' => ['The medical certificate expires within a month; renew it before submitting.'],
            ]);
        }
    }

    /** Fitness of an applicant to be submitted, for the UI to explain. */
    public function submissionBlockers(VisaRecord $record): array
    {
        try {
            $this->assertMedicallyCleared($record);
        } catch (ValidationException $exception) {
            return $exception->validator->errors()->all();
        }

        return [];
    }
}
