<?php

namespace App\Domain\Applicants\Services;

use App\Domain\Applicants\Enums\ApplicantStatus;
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\Organization\Models\Branch;
use App\Support\Sequences\SequenceGenerator;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class ApplicantService
{
    public function __construct(
        private readonly SequenceGenerator $sequences,
        private readonly DuplicateDetector $duplicates,
        private readonly ActivityLogger $activity,
        private readonly CommissionEngine $commissions,
        private readonly Notifier $notifier,
    ) {}

    /**
     * @param  array<string, mixed>  $attributes
     * @param  bool  $allowDuplicate  Set when a user with override rights has
     *                                reviewed the warning and chosen to proceed.
     *
     * @throws ValidationException
     */
    public function create(array $attributes, ?User $actor = null, bool $allowDuplicate = false): Applicant
    {
        $companyId = (int) $attributes['company_id'];

        if (! $allowDuplicate && $this->duplicates->hasHardMatch($companyId, $attributes)) {
            throw ValidationException::withMessages([
                'duplicate' => ['An applicant with this national ID or passport number already exists.'],
            ]);
        }

        return DB::transaction(function () use ($attributes, $actor, $companyId) {
            $attributes['status'] ??= ApplicantStatus::Lead->value;
            $attributes['reference_no'] ??= $this->generateReference($companyId, $attributes['branch_id'] ?? null);
            $attributes['created_by'] = $actor?->getKey();
            $attributes['updated_by'] = $actor?->getKey();

            $applicant = Applicant::create($attributes);

            $this->activity->log(ActivityCategory::Profile, 'applicant.created', [
                'subject' => $applicant,
                'actor' => $actor,
                'title' => 'Applicant profile created',
                'description' => $applicant->full_name.' · '.$applicant->reference_no,
                'visibility' => 'shared',
            ]);

            return $applicant;
        });
    }

    /**
     * @param  array<string, mixed>  $attributes
     *
     * @throws ValidationException
     */
    public function update(
        Applicant $applicant,
        array $attributes,
        ?User $actor = null,
        bool $allowDuplicate = false,
    ): Applicant {
        // Identity, ownership, status and verification are never editable here.
        unset(
            $attributes['reference_no'],
            $attributes['company_id'],
            $attributes['status'],
            $attributes['verified_at'],
            $attributes['verified_by'],
            $attributes['merged_into_id'],
        );

        if (! $allowDuplicate
            && $this->duplicates->hasHardMatch($applicant->company_id, $attributes, $applicant->getKey())) {
            throw ValidationException::withMessages([
                'duplicate' => ['Another applicant already holds this national ID or passport number.'],
            ]);
        }

        return DB::transaction(function () use ($applicant, $attributes, $actor) {
            $attributes['updated_by'] = $actor?->getKey();
            $applicant->fill($attributes);

            $this->activity->logUpdate(
                ActivityCategory::Profile,
                'applicant.updated',
                $applicant,
                [
                    'mobile' => 'Phone number',
                    'preferred_country' => 'Preferred country',
                    'desired_occupation' => 'Desired job',
                    'passport_number' => 'Passport number',
                    'national_id' => 'National ID',
                ],
                ['actor' => $actor, 'title' => 'Applicant details updated'],
            );

            $applicant->save();

            return $applicant;
        });
    }

    /** @throws ValidationException */
    public function changeStatus(
        Applicant $applicant,
        ApplicantStatus $target,
        ?string $reason,
        ?User $actor = null,
    ): Applicant {
        $current = $applicant->status;

        if ($current === $target) {
            throw ValidationException::withMessages([
                'status' => ["The applicant is already {$target->label()}."],
            ]);
        }

        if (! $current->canTransitionTo($target)) {
            throw ValidationException::withMessages([
                'status' => ["An applicant cannot move from {$current->label()} to {$target->label()}."],
            ]);
        }

        if ($target->requiresReason() && blank($reason)) {
            throw ValidationException::withMessages([
                'reason' => ["A reason is required to mark an applicant {$target->label()}."],
            ]);
        }

        return DB::transaction(function () use ($applicant, $current, $target, $reason, $actor) {
            $applicant->status = $target;
            $applicant->status_reason = $reason;
            $applicant->updated_by = $actor?->getKey();
            $applicant->save();

            $this->activity->log(ActivityCategory::Status, 'applicant.status_changed', [
                'subject' => $applicant,
                'actor' => $actor,
                'title' => 'Applicant status changed',
                'description' => "{$current->label()} → {$target->label()}",
                'old' => ['status' => $current->value],
                'new' => ['status' => $target->value],
                'metadata' => $reason ? ['reason' => $reason] : null,
                'visibility' => 'shared',
            ]);

            return $applicant;
        });
    }

    /** Marks a self-submitted or agent-submitted profile as checked by staff (§10). */
    public function verify(Applicant $applicant, User $actor): Applicant
    {
        return DB::transaction(function () use ($applicant, $actor) {
            $applicant->verified_at = now();
            $applicant->verified_by = $actor->getKey();
            $applicant->updated_by = $actor->getKey();

            // Verification is what turns a lead into a registered applicant.
            if ($applicant->status === ApplicantStatus::Lead) {
                $applicant->status = ApplicantStatus::Registered;
            }

            $applicant->save();

            $this->activity->log(ActivityCategory::Status, 'applicant.verified', [
                'subject' => $applicant,
                'actor' => $actor,
                'title' => 'Applicant profile verified',
                'description' => 'Details checked and confirmed by staff.',
                'visibility' => 'shared',
            ]);

            // Verification is a system-observed event, so it is what the
            // commission engine reacts to — never a frontend claim (§13A).
            $this->commissions->handle(CommissionTrigger::ApplicantVerified, $applicant, $applicant);

            $this->notifier->send('registration_confirmed', $applicant, [
                'applicant_name' => $applicant->full_name,
                'reference_no' => $applicant->reference_no,
            ], $applicant, $actor);

            return $applicant;
        });
    }

    /**
     * Folds a duplicate into the surviving record (§9.2). The duplicate is kept
     * as a tombstone so old references and receipts still resolve.
     *
     * @throws ValidationException
     */
    public function merge(Applicant $duplicate, Applicant $survivor, ?User $actor = null): Applicant
    {
        if ($duplicate->is($survivor)) {
            throw ValidationException::withMessages([
                'survivor_id' => ['An applicant cannot be merged into itself.'],
            ]);
        }

        if ($duplicate->company_id !== $survivor->company_id) {
            throw ValidationException::withMessages([
                'survivor_id' => ['Applicants from different companies cannot be merged.'],
            ]);
        }

        if ($duplicate->merged_into_id !== null) {
            throw ValidationException::withMessages([
                'duplicate' => ['This applicant has already been merged.'],
            ]);
        }

        return DB::transaction(function () use ($duplicate, $survivor, $actor) {
            // Fill gaps in the survivor from the duplicate; never overwrite.
            $fillable = [
                'father_name', 'mother_name', 'date_of_birth', 'national_id', 'passport_number',
                'passport_expiry_date', 'mobile', 'whatsapp', 'email', 'present_address',
                'permanent_address', 'occupation', 'education_level', 'agent_id',
            ];

            foreach ($fillable as $field) {
                if (blank($survivor->{$field}) && filled($duplicate->{$field})) {
                    $survivor->{$field} = $duplicate->{$field};
                }
            }

            $survivor->updated_by = $actor?->getKey();
            $survivor->save();

            $duplicate->merged_into_id = $survivor->getKey();
            $duplicate->status = ApplicantStatus::Merged;
            $duplicate->status_reason = "Merged into {$survivor->reference_no}.";
            $duplicate->updated_by = $actor?->getKey();
            $duplicate->save();

            $this->activity->log(ActivityCategory::Profile, 'applicant.merged', [
                'subject' => $survivor,
                'actor' => $actor,
                'title' => 'Duplicate applicant merged',
                'description' => "{$duplicate->reference_no} was merged into {$survivor->reference_no}.",
                'metadata' => ['merged_reference' => $duplicate->reference_no],
            ]);

            return $survivor;
        });
    }

    /** AT-DHK-2026-000001 — the prefix is configurable (§9.3). */
    public function generateReference(int $companyId, ?int $branchId): string
    {
        $prefix = config('mams.applicant_reference.prefix', 'MAMS');
        $padding = (int) config('mams.applicant_reference.padding', 6);

        $branchCode = $branchId !== null
            ? (Branch::whereKey($branchId)->value('applicant_ref_prefix')
                ?? Branch::whereKey($branchId)->value('code')
                ?? 'HO')
            : 'HO';

        return $this->sequences->next('applicant', $prefix, [$branchCode, now()->year], $padding);
    }
}
