<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Stevebauman\Location\Facades\Location;
use Stevebauman\Location\Position;
use Throwable;

class GeoIpResolver
{
    /**
     * @return array{country?: string, city?: string}
     */
    public function resolve(?string $ip): array
    {
        if (! $this->isPublicIp($ip)) {
            return [];
        }

        return Cache::remember(
            'geoip:'.hash('sha256', $ip),
            now()->addDays((int) config('services.geoip.cache_days', 7)),
            fn (): array => $this->lookup($ip)
        );
    }

    /**
     * @return array{country?: string, city?: string}
     */
    private function lookup(string $ip): array
    {
        return match ((string) config('services.geoip.provider', 'stevebauman')) {
            'disabled' => [],
            'location', 'stevebauman' => $this->lookupWithStevebauman($ip),
            'ip-api' => $this->lookupWithIpApi($ip),
            default => $this->lookupWithIpApiCo($ip),
        };
    }

    /**
     * @return array{country?: string, city?: string}
     */
    private function lookupWithStevebauman(string $ip): array
    {
        try {
            $position = Location::get($ip);

            if (! $position instanceof Position || $position->isEmpty()) {
                return [];
            }

            return $this->normalize(
                $position->countryName,
                $position->cityName
            );
        } catch (Throwable) {
            return [];
        }
    }

    /**
     * @return array{country?: string, city?: string}
     */
    private function lookupWithIpApiCo(string $ip): array
    {
        try {
            $response = Http::timeout((int) config('services.geoip.timeout', 2))
                ->acceptJson()
                ->get("https://ipapi.co/{$ip}/json/");

            if (! $response->ok() || $response->json('error') === true) {
                return [];
            }

            return $this->normalize(
                $response->json('country_name'),
                $response->json('city')
            );
        } catch (Throwable) {
            return [];
        }
    }

    /**
     * @return array{country?: string, city?: string}
     */
    private function lookupWithIpApi(string $ip): array
    {
        try {
            $response = Http::timeout((int) config('services.geoip.timeout', 2))
                ->acceptJson()
                ->get("http://ip-api.com/json/{$ip}", [
                    'fields' => 'status,country,city',
                ]);

            if (! $response->ok() || $response->json('status') !== 'success') {
                return [];
            }

            return $this->normalize(
                $response->json('country'),
                $response->json('city')
            );
        } catch (Throwable) {
            return [];
        }
    }

    /**
     * @return array{country?: string, city?: string}
     */
    private function normalize(mixed $country, mixed $city): array
    {
        return array_filter([
            'country' => is_string($country) && $country !== '' ? $country : null,
            'city' => is_string($city) && $city !== '' ? $city : null,
        ]);
    }

    private function isPublicIp(?string $ip): bool
    {
        return is_string($ip)
            && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
    }
}
