<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;

class PostcodeService
{
    private const string BASE_URL = 'https://api.postcodes.io';

    private const int CACHE_TTL = 60 * 60 * 24 * 30; // 30 days

    public function __construct(
        private readonly ApiLogger $apiLogger,
    ) {}

    /**
     * Resolve a UK location query (full postcode, outcode, or place name) to coordinates.
     * Returns null if the location cannot be resolved.
     */
    public function resolve(string $query): ?LocationResult
    {
        $query = trim($query);
        $cacheKey = 'postcode:'.strtolower(preg_replace('/\s+/', '', $query));

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

        if ($cached instanceof LocationResult) {
            return $cached;
        }

        $result = match (true) {
            $this->isFullPostcode($query) => $this->lookupPostcode($query),
            $this->isOutcode($query) => $this->lookupOutcode($query),
            default => $this->lookupPlace($query),
        };

        if ($result !== null) {
            Cache::put($cacheKey, $result, self::CACHE_TTL);
        }

        return $result;
    }

    private function isFullPostcode(string $query): bool
    {
        return (bool) preg_match('/^[A-Z]{1,2}[0-9][0-9A-Z]?\s*[0-9][A-Z]{2}$/i', $query);
    }

    private function isOutcode(string $query): bool
    {
        return (bool) preg_match('/^[A-Z]{1,2}[0-9][0-9A-Z]?$/i', $query);
    }

    private function lookupPostcode(string $postcode): ?LocationResult
    {
        $normalised = strtoupper(preg_replace('/\s+/', '', $postcode));
        $url = self::BASE_URL.'/postcodes/'.$normalised;

        try {
            $response = $this->apiLogger->send('postcodes_io', 'GET', $url, fn () => Http::timeout(10)->get($url));

            if (! $response->successful()) {
                return null;
            }

            $data = $response->json('result');

            return new LocationResult(
                query: $postcode,
                displayName: $data['postcode'],
                lat: $data['latitude'],
                lng: $data['longitude'],
            );
        } catch (Throwable $e) {
            Log::error('PostcodeService: postcode lookup failed', [
                'postcode' => $postcode,
                'error' => $e->getMessage(),
            ]);

            return null;
        }
    }

    private function lookupOutcode(string $outcode): ?LocationResult
    {
        $normalised = strtoupper(trim($outcode));
        $url = self::BASE_URL.'/outcodes/'.$normalised;

        try {
            $response = $this->apiLogger->send('postcodes_io', 'GET', $url, fn () => Http::timeout(10)->get($url));

            if (! $response->successful()) {
                return null;
            }

            $data = $response->json('result');

            return new LocationResult(
                query: $outcode,
                displayName: $data['outcode'],
                lat: $data['latitude'],
                lng: $data['longitude'],
            );
        } catch (Throwable $e) {
            Log::error('PostcodeService: outcode lookup failed', [
                'outcode' => $outcode,
                'error' => $e->getMessage(),
            ]);

            return null;
        }
    }

    private function lookupPlace(string $place): ?LocationResult
    {
        $url = self::BASE_URL.'/places';
        $logUrl = $url.'?q='.urlencode($place).'&limit=1';

        try {
            $response = $this->apiLogger->send('postcodes_io', 'GET', $logUrl, fn () => Http::timeout(10)
                ->get($url, ['q' => $place, 'limit' => 1]));

            if (! $response->successful()) {
                return null;
            }

            $results = $response->json('result');

            if (empty($results)) {
                return null;
            }

            $data = $results[0];

            return new LocationResult(
                query: $place,
                displayName: $data['name_1'],
                lat: $data['latitude'],
                lng: $data['longitude'],
            );
        } catch (Throwable $e) {
            Log::error('PostcodeService: place lookup failed', [
                'place' => $place,
                'error' => $e->getMessage(),
            ]);

            return null;
        }
    }
}
