<?php

namespace App\Services\NationalIdentity;

use App\Contracts\NationalIdentityProvider;
use App\Models\Business;
use App\Models\Setting;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use RuntimeException;
use Throwable;

class NidaNationalIdentityProvider implements NationalIdentityProvider
{
    public function isConfigured(): bool
    {
        $business = Business::firstOrCreate(['id' => 1], ['name' => 'Microfinance']);

        return Setting::query()
            ->where('business_id', $business->id)
            ->where('group', 'identity')
            ->where('key', 'nin_lookup_enabled')
            ->where('value', '1')
            ->exists();
    }

    public function lookup(string $nin): ?array
    {
        if (! $this->isConfigured()) {
            return null;
        }

        $nin = trim($nin);
        if (preg_match('/^\d{20}$/', $nin) !== 1) {
            throw new RuntimeException('NIN must contain exactly 20 digits.');
        }

        $business = Business::firstOrCreate(['id' => 1], ['name' => 'Microfinance']);
        $baseUrl = rtrim((string) Setting::query()
            ->where('business_id', $business->id)
            ->where('group', 'identity')
            ->where('key', 'nida_api_url')
            ->value('value'), '/');

        if ($baseUrl === '') {
            throw new RuntimeException('NIN verification is enabled, but the NIDA API URL has not been configured in System Settings. Add the provider URL under Customer NIN / NIDA and try again.');
        }

        $timeout = max(3, min(60, (int) config('nida.timeout_seconds', 15)));
        $connectTimeout = max(1, min($timeout, (int) config('nida.connect_timeout_seconds', 5)));
        $url = $baseUrl.'/'.$nin;

        try {
            $response = Http::acceptJson()
                ->connectTimeout($connectTimeout)
                ->timeout($timeout)
                ->withHeaders([
                    'Content-Type' => 'application/json',
                    'Content-Length' => '0',
                    'Connection' => 'keep-alive',
                ])
                ->send('POST', $url, ['body' => '']);
        } catch (Throwable $exception) {
            throw new RuntimeException('NIDA lookup service is currently unreachable. You can continue with manual customer entry.', previous: $exception);
        }

        if (in_array($response->status(), [401, 403], true)) {
            throw new RuntimeException('Live NIDA verification is not authorized for this server. The configured lookup provider denied access. Continue with manual NIN entry or configure an authorized NIDA integration.');
        }

        if ($response->status() === 404) {
            return null;
        }

        if ($response->status() === 429) {
            throw new RuntimeException('The NIDA lookup provider is temporarily rate-limiting requests. Please try again later or continue with manual entry.');
        }

        if (! $response->successful()) {
            throw new RuntimeException('NIDA lookup is temporarily unavailable (HTTP '.$response->status().'). You can continue with manual customer entry.');
        }

        $payload = $response->json();
        if (! is_array($payload)) {
            throw new RuntimeException('NIDA lookup returned an invalid response. You can continue with manual customer entry.');
        }

        $record = $payload['obj'] ?? $payload['data'] ?? null;
        if (! is_array($record) || $record === []) {
            return null;
        }

        return $this->normalize($record);
    }

    /** @param array<string,mixed> $record */
    private function normalize(array $record): array
    {
        $normalized = [];
        foreach ($record as $key => $value) {
            $normalized[$this->normalizeKey((string) $key)] = $value;
        }

        $gender = $this->stringValue($normalized, ['sex', 'gender']);
        $gender = match (strtolower((string) $gender)) {
            'm', 'male' => 'male',
            'f', 'female' => 'female',
            default => null,
        };

        $dateOfBirth = $this->stringValue($normalized, ['dateofbirth', 'dob', 'birthdate']);
        if ($dateOfBirth !== null) {
            try {
                $dateOfBirth = CarbonImmutable::parse($dateOfBirth)->format('Y-m-d');
            } catch (Throwable) {
                $dateOfBirth = null;
            }
        }

        $maritalStatus = strtolower((string) ($this->stringValue($normalized, ['maritalstatus']) ?? ''));
        if (! in_array($maritalStatus, ['single', 'married', 'divorced', 'widowed', 'other'], true)) {
            $maritalStatus = null;
        }

        return array_filter([
            'nin' => $this->stringValue($normalized, ['nin', 'nationalidnumber']) ?: null,
            'first_name' => $this->stringValue($normalized, ['firstname', 'first']) ?: null,
            'middle_name' => $this->stringValue($normalized, ['middlename', 'middle']) ?: null,
            'last_name' => $this->stringValue($normalized, ['surname', 'lastname', 'last']) ?: null,
            'gender' => $gender,
            'date_of_birth' => $dateOfBirth,
            'phone' => $this->stringValue($normalized, ['phonenumber', 'phone']) ?: null,
            'occupation' => $this->stringValue($normalized, ['occupation']) ?: null,
            'marital_status' => $maritalStatus,
            'region' => $this->stringValue($normalized, ['residentregion']) ?: null,
            'district' => $this->stringValue($normalized, ['residentdistrict']) ?: null,
            'ward' => $this->stringValue($normalized, ['residentward']) ?: null,
            'street_village' => $this->stringValue($normalized, ['residentstreet', 'residentvillage']) ?: null,
            'house_number' => $this->stringValue($normalized, ['residenthouseno', 'residenthousenumber']) ?: null,
        ], static fn ($value): bool => $value !== null && $value !== '');
    }

    private function normalizeKey(string $key): string
    {
        return Str::of($key)
            ->lower()
            ->replaceMatches('/[^a-z0-9]/', '')
            ->toString();
    }

    /** @param array<string,mixed> $record */
    private function stringValue(array $record, array $keys): ?string
    {
        foreach ($keys as $key) {
            $value = $record[$key] ?? null;
            if (is_scalar($value)) {
                $value = trim((string) $value);
                if ($value !== '') {
                    return $value;
                }
            }
        }

        return null;
    }
}
