<?php

namespace App\Services;

use App\Models\Address;
use Illuminate\Http\Client\Pool;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;

class ZipCodeCoordinatesService
{
    private array $resolvedZipCodes = [];

    public function resolve(?float $latitude, ?float $longitude, ?string $zipCode): ?array
    {
        if ($latitude !== null && $longitude !== null) {
            return compact('latitude', 'longitude');
        }

        return $this->findByZipCode($zipCode);
    }

    //

    public function calculateDistance(
        ?float  $originLatitude,
        ?float  $originLongitude,
        ?string $originZipCode,
        ?float  $targetLatitude,
        ?float  $targetLongitude,
        ?string $targetZipCode,
    ): ?float {
        $origin = $this->resolve($originLatitude, $originLongitude, $originZipCode);
        $target = $this->resolve($targetLatitude, $targetLongitude, $targetZipCode);

        $distance = DistanceService::calculate(
            data_get($origin, 'latitude'),
            data_get($origin, 'longitude'),
            data_get($target, 'latitude'),
            data_get($target, 'longitude'),
        );

        return $distance;
    }

    public function findByZipCode(?string $zipCode): ?array
    {
        $zipCode = $this->normalize($zipCode);

        if ($zipCode === null) {
            return null;
        }

        if (array_key_exists($zipCode, $this->resolvedZipCodes)) {
            return data_get($this->resolvedZipCodes, $zipCode);
        }

        if ($this->restoreFromCache($zipCode)) {
            return data_get($this->resolvedZipCodes, $zipCode);
        }

        if (! $this->googleGeocodingIsConfigured()) {
            return $this->resolvedZipCodes[$zipCode] = null;
        }

        $coordinates = $this->requestCoordinates($zipCode);

        $this->remember($zipCode, $coordinates);

        return $coordinates;
    }

    public function preload(iterable $zipCodes): void
    {
        $pending = [];

        foreach ($zipCodes as $zipCode) {
            $zipCode = $this->normalize($zipCode);

            if (
                $zipCode === null
                || array_key_exists($zipCode, $this->resolvedZipCodes)
                || $this->restoreFromCache($zipCode)
            ) {
                continue;
            }

            $pending[$zipCode] = $zipCode;
        }

        if ($pending === []) {
            return;
        }

        if (! $this->googleGeocodingIsConfigured()) {
            foreach ($pending as $zipCode) {
                $this->resolvedZipCodes[$zipCode] = null;
            }

            return;
        }

        try {
            Log::channel('google_geocoding')->info('Requisição em lote ao Google Geocoding', [
                'request_count'    => count($pending),
                'zip_codes_sample' => array_slice(array_values($pending), 0, 20),
            ]);

            $baseUrl = rtrim((string) config('services.google_geocoding.base_url'), '/');

            $responses = Http::pool(fn (Pool $pool) => array_map(
                fn (string $zipCode) => $pool
                    ->as($zipCode)
                    ->acceptJson()
                    ->connectTimeout((int) config('services.google_geocoding.connect_timeout', 2))
                    ->timeout((int) config('services.google_geocoding.timeout', 5))
                    ->withOptions($this->curlForceIpv4Options())
                    ->get("{$baseUrl}/maps/api/geocode/json", $this->googleQuery($zipCode)),
                $pending,
            ));
        } catch (Throwable) {
            $responses = [];
        }

        foreach ($pending as $zipCode) {
            $response = data_get($responses, $zipCode);

            $coordinates = $response instanceof Response
                ? $this->coordinatesFromResponse($response)
                : null;

            $this->remember($zipCode, $coordinates);
        }
    }

    //

    private function coordinatesFromResponse(Response $response): ?array
    {
        if (! $response->successful()) {
            return null;
        }

        if ($response->json('status') !== 'OK') {
            return null;
        }

        $latitude  = $response->json('results.0.geometry.location.lat');
        $longitude = $response->json('results.0.geometry.location.lng');

        if (! is_numeric($latitude) || ! is_numeric($longitude)) {
            return null;
        }

        $latitude  = (float) $latitude;
        $longitude = (float) $longitude;

        if ($latitude < -90 || $latitude > 90 || $longitude < -180 || $longitude > 180) {
            return null;
        }

        return compact('latitude', 'longitude');
    }

    private function curlForceIpv4Options(): array
    {
        if (! defined('CURLOPT_IPRESOLVE') || ! defined('CURL_IPRESOLVE_V4')) {
            return [];
        }

        return [
            'curl' => [
                CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
            ],
        ];
    }

    private function googleGeocodingIsConfigured(): bool
    {
        return trim((string) config('services.google_geocoding.api_key')) !== '';
    }

    private function googleQuery(string $zipCode): array
    {
        return [
            'components' => "postal_code:{$zipCode}|country:BR",
            'language'   => 'pt-BR',
            'region'     => 'br',
            'key'        => (string) config('services.google_geocoding.api_key'),
        ];
    }

    private function normalize(?string $zipCode): ?string
    {
        $zipCode = preg_replace('/\D/', '', $zipCode ?? '');

        return strlen($zipCode) === 8 ? $zipCode : null;
    }

    private function persistProviderCoordinates(string $zipCode, array $coordinates): void
    {
        $formattedZipCode = substr($zipCode, 0, 5).'-'.substr($zipCode, 5);

        Address::query()
            ->where('source', 'provider')
            ->whereIn('zip_code', [$zipCode, $formattedZipCode])
            ->where(function ($query) {
                $query->whereNull('latitude')->orWhereNull('longitude');
            })
            ->update([
                'latitude'  => data_get($coordinates, 'latitude'),
                'longitude' => data_get($coordinates, 'longitude'),
            ]);
    }

    private function remember(string $zipCode, ?array $coordinates): void
    {
        $this->resolvedZipCodes[$zipCode] = $coordinates;

        if ($coordinates === null) {
            return;
        }

        Cache::put(
            "zip-code-coordinates:{$zipCode}",
            [
                'found'       => $coordinates !== null,
                'coordinates' => $coordinates,
            ],
            now()->addSeconds(
                (int) config('services.google_geocoding.cache_ttl', 2592000)
            )
        );

        $this->persistProviderCoordinates($zipCode, $coordinates);
    }

    private function requestCoordinates(string $zipCode): ?array
    {
        try {
            Log::channel('google_geocoding')->info('Requisição ao Google Geocoding', [
                'zip_code' => $zipCode,
            ]);

            $baseUrl = rtrim((string) config('services.google_geocoding.base_url'), '/');

            $response = Http::acceptJson()
                ->withOptions($this->curlForceIpv4Options())
                ->connectTimeout((int) config('services.google_geocoding.connect_timeout', 2))
                ->timeout((int) config('services.google_geocoding.timeout', 5))
                ->get("{$baseUrl}/maps/api/geocode/json", $this->googleQuery($zipCode));

            return $this->coordinatesFromResponse($response);
        } catch (Throwable) {
            return null;
        }
    }

    private function restoreFromCache(string $zipCode): bool
    {
        $cacheKey = "zip-code-coordinates:{$zipCode}";

        $cached = Cache::get($cacheKey);

        if (! is_array($cached) || ! array_key_exists('found', $cached)) {
            return false;
        }

        if (! data_get($cached, 'found')) {
            Cache::forget($cacheKey);

            return false;
        }

        $this->resolvedZipCodes[$zipCode] = data_get($cached, 'found')
            ? data_get($cached, 'coordinates')
            : null;

        return true;
    }
}
