<?php

namespace App\Domain\Applicants\Services;

use App\Domain\Applicants\Models\Applicant;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;

/**
 * Duplicate applicant detection (§9.2).
 *
 * National ID and passport are treated as hard matches — one person cannot hold
 * two of either. Phone, email and name+date-of-birth are soft signals: families
 * share phones and names repeat, so these warn rather than block.
 */
class DuplicateDetector
{
    public const HARD = 'hard';

    public const SOFT = 'soft';

    /**
     * @param  array<string, mixed>  $attributes
     * @return Collection<int, array{applicant: Applicant, level: string, matched_on: array<int, string>}>
     */
    public function check(int $companyId, array $attributes, ?int $ignoreApplicantId = null): Collection
    {
        $matches = [];

        foreach ($this->hardCandidates($companyId, $attributes, $ignoreApplicantId) as $field => $applicants) {
            foreach ($applicants as $applicant) {
                $this->record($matches, $applicant, self::HARD, $field);
            }
        }

        foreach ($this->softCandidates($companyId, $attributes, $ignoreApplicantId) as $field => $applicants) {
            foreach ($applicants as $applicant) {
                $this->record($matches, $applicant, self::SOFT, $field);
            }
        }

        return collect($matches)
            ->sortByDesc(fn (array $match) => $match['level'] === self::HARD ? 1 : 0)
            ->values();
    }

    /** Whether saving should be refused outright rather than warned about. */
    public function hasHardMatch(int $companyId, array $attributes, ?int $ignoreApplicantId = null): bool
    {
        return $this->check($companyId, $attributes, $ignoreApplicantId)
            ->contains(fn (array $match) => $match['level'] === self::HARD);
    }

    /** @return array<string, Collection<int, Applicant>> */
    private function hardCandidates(int $companyId, array $attributes, ?int $ignore): array
    {
        $found = [];

        foreach (['national_id', 'passport_number'] as $field) {
            $value = $attributes[$field] ?? null;

            if (blank($value)) {
                continue;
            }

            $found[$field] = $this->base($companyId, $ignore)->where($field, $value)->get();
        }

        return $found;
    }

    /** @return array<string, Collection<int, Applicant>> */
    private function softCandidates(int $companyId, array $attributes, ?int $ignore): array
    {
        $found = [];

        foreach (['mobile', 'email'] as $field) {
            $value = $attributes[$field] ?? null;

            if (blank($value)) {
                continue;
            }

            $found[$field] = $this->base($companyId, $ignore)->where($field, $value)->get();
        }

        // Same name and date of birth is the classic re-registration by a second agent.
        $name = $attributes['full_name'] ?? null;
        $dob = $attributes['date_of_birth'] ?? null;

        if (filled($name) && filled($dob)) {
            $found['name_and_date_of_birth'] = $this->base($companyId, $ignore)
                ->whereDate('date_of_birth', $dob)
                ->whereRaw('LOWER(full_name) = ?', [mb_strtolower((string) $name)])
                ->get();
        }

        return $found;
    }

    private function base(int $companyId, ?int $ignore): Builder
    {
        return Applicant::query()
            ->where('company_id', $companyId)
            // A record already merged away is not a live duplicate.
            ->whereNull('merged_into_id')
            ->when($ignore !== null, fn (Builder $query) => $query->whereKeyNot($ignore));
    }

    /** @param array<int, array{applicant: Applicant, level: string, matched_on: array<int, string>}> $matches */
    private function record(array &$matches, Applicant $applicant, string $level, string $field): void
    {
        $key = $applicant->getKey();

        if (! isset($matches[$key])) {
            $matches[$key] = ['applicant' => $applicant, 'level' => $level, 'matched_on' => []];
        }

        // One hard match makes the whole pairing hard.
        if ($level === self::HARD) {
            $matches[$key]['level'] = self::HARD;
        }

        $matches[$key]['matched_on'][] = $field;
    }
}
