<?php

namespace App\Services\Location;

use \App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class Geocoder
{
    private bool $missingKeyWarned = false;

    public function __construct(private array $cfg = [])
    {
        $this->cfg = config("services.geocoding");
    }

    /**
     * forward geocode with optional per-user bias
     */
    public function forward(string $query, ?User $user = null): ?array
    {
        if (mb_strlen(trim($query)) < 4) {
            return null;
        }

        return match ($this->cfg['provider'] ?? 'arcgis') {
            'arcgis' => $this->forwardArcgis($query, $this->biasForUser($user)),
            default  => null,
        };
    }

    /**
     * reverse geocode lon/lat → address. (Optional)
     */
    public function reverse(float $lat, float $lon): ?array
    {
        return match ($this->cfg['provider'] ?? 'arcgis') {
            'arcgis' => $this->reverseArcgis($lat, $lon),
            default  => null,
        };
    }

    /*
     *
     * ArcGIS World Geocoding
     */

    /**
     * request
     */
    private function http()
    {
        return Http::retry(3, 500)
            ->timeout((int) ($this->cfg['timeout'] ?? 20))
            ->withHeaders([
                'User-Agent' => $this->cfg['user_agent'] ?? 'Kithkin/Geocoder',
            ]);
    }

    /**
     * get base url
     */
    private function arcgisBase(): string
    {
        return rtrim($this->cfg["arcgis"]["endpoint"], "/");
    }

    /**
     * fetch arcgis api key (log once if missing)
     */
    private function arcgisKey(): ?string
    {
        $key = $this->cfg['arcgis']['api_key'] ?? null;

        if (!$key) {
            $this->warnMissingKey();
            return null;
        }

        return $key;
    }

    private function warnMissingKey(): void
    {
        if ($this->missingKeyWarned) {
            return;
        }

        $this->missingKeyWarned = true;
        Log::warning('arcgis api key missing; geocoding disabled');
    }

    private function arcgisDebugEnabled(): bool
    {
        return (bool) ($this->cfg['arcgis']['debug'] ?? false);
    }

    private function logArcgisResponse(string $label, array $params, $res): void
    {
        if (! $this->arcgisDebugEnabled()) {
            return;
        }

        $safeParams = $params;
        if (isset($safeParams['token'])) {
            $safeParams['token'] = '***';
        }

        Log::info("arcgis {$label} response", [
            'status' => $res->status(),
            'params' => $safeParams,
            'body'   => $res->body(),
        ]);
    }

    /**
     * pull a bias from the user (zip -> centroid) and cache it
     */
    private function biasForUser(?User $user): ?array
    {
        if (!$user) {
            return null;
        }

        // if you store home_lat/home_lon on users, prefer those
        if (!empty($user->home_lat) && !empty($user->home_lon)) {
            return ['lat' => (float)$user->home_lat, 'lon' => (float)$user->home_lon, 'radius_km' => 120.0];
        }

        // else try user zip/postal
        $zip = $user->postal_code ?? $user->zip ?? null;
        if (!$zip) {
            return null;
        }

        $key = $this->arcgisKey();
        if (!$key) {
            return null;
        }

        $cacheKey = "geo:bias:zip:{$zip}";
        $bias = Cache::remember($cacheKey, now()->addDays(7), function () use ($zip, $key) {
            $a = $this->cfg['arcgis'];

            $params = [
                'singleLine'   => $zip,
                'category'     => 'Postal',
                'maxLocations' => 1,
                'f'            => 'pjson',
                'token'        => $key,
                'countryCode'  => $a['country_code'] ?? null,
            ];

            $res = $this->http()->get($this->arcgisBase().'/findAddressCandidates', $params);
            $this->logArcgisResponse('zip-bias', $params, $res);
            if (!$res->ok()) {
                Log::info('arcgis zip bias lookup failed', ['zip' => $zip, 'status' => $res->status()]);
                return null;
            }

            $cand = Arr::first($res->json('candidates', []));
            if (!$cand || empty($cand['location'])) {
                return null;
            }

            return [
                'lat'        => (float)($cand['location']['y'] ?? 0),
                'lon'        => (float)($cand['location']['x'] ?? 0),
                'radius_km'  => 120.0, // reasonable city-scale radius
            ];
        });

        return $bias ?: null;
    }

    /**
     * compute a bounding box string from center + radius (km)
     */
    private function bboxFromBias(float $lat, float $lon, float $radiusKm): string
    {
        $latDelta = $radiusKm / 111.0;
        $lonDelta = $radiusKm / (111.0 * max(cos(deg2rad($lat)), 0.01));
        $minx = $lon - $lonDelta;
        $miny = $lat - $latDelta;
        $maxx = $lon + $lonDelta;
        $maxy = $lat + $latDelta;
        return "{$minx},{$miny},{$maxx},{$maxy}";
    }

    /**
     * handle location request with optional bias array
     */
    private function forwardArcgis(string $query, ?array $bias): ?array
    {
        $a = $this->cfg['arcgis'];
        $key = $this->arcgisKey();
        if (!$key) {
            return null;
        }

        $params = [
            'singleLine'   => $query,
            'outFields'    => $a['out_fields'] ?? '*',
            'maxLocations' => (int)($a['max_results'] ?? 5),
            'f'            => 'pjson',
            'token'        => $key,
            'category'     => $a['categories'] ?? 'POI,Address',
            'countryCode'  => $a['country_code'] ?? null,
        ];

        if (!empty($a['store'])) {
            $params['forStorage'] = 'true';
        }

        if ($bias && $bias['lat'] && $bias['lon']) {
            $params['location'] = $bias['lon'].','.$bias['lat'];
            if (!empty($bias['radius_km'])) {
                $params['searchExtent'] = $this->bboxFromBias($bias['lat'], $bias['lon'], (float)$bias['radius_km']);
            }
        }

        $res = $this->http()->get($this->arcgisBase().'/findAddressCandidates', $params);
        $this->logArcgisResponse('forward', $params, $res);
        if (!$res->ok()) {
            Log::warning('arcgis forward geocode failed', ['status' => $res->status(), 'q' => $query]);
            return null;
        }

        $cands = $res->json('candidates', []);
        if (!$cands) {
            if ($this->arcgisDebugEnabled()) {
                Log::info('arcgis forward geocode empty', ['q' => $query]);
            }
            return null;
        }

        // simplest “best”: highest arcgis score
        usort($cands, fn($a,$b) => ($b['score'] <=> $a['score']));
        $best = $cands[0];

        return $this->normalizeArcgisCandidate($best, $query);
    }

    /**
     * lookup based on lat/lon
     */
    private function reverseArcgis(float $lat, float $lon): ?array
    {
        $a = $this->cfg['arcgis'];
        $key = $this->arcgisKey();
        if (!$key) {
            return null;
        }

        $params = [
            'location' => "{$lon},{$lat}",
            'f'        => 'pjson',
            'token'    => $key,
        ];

        if (!empty($a['store'])) {
            $params['forStorage'] = 'true';
        }

        $res = $this->http()->get($this->arcgisBase().'/reverseGeocode', $params);
        $this->logArcgisResponse('reverse', $params, $res);
        if (!$res->ok()) {
            Log::warning('arcgis reverse geocode failed', ['status' => $res->status()]);
            return null;
        }

        $j = $res->json();
        $addr = $j['address'] ?? null;
        if (!$addr) {
            return null;
        }

        return [
            'display_name' => $addr['LongLabel'] ?? $addr['Match_addr'] ?? null,
            'raw_address'  => $addr['Match_addr'] ?? null,
            'street'       => $addr['Address']     ?? null,
            'city'         => $addr['City']        ?? null,
            'state'        => $addr['Region']      ?? null,
            'postal'       => $addr['Postal']      ?? null,
            'country'      => $addr['CountryCode'] ?? null,
            'lat'          => Arr::get($j, 'location.y'),
            'lon'          => Arr::get($j, 'location.x'),
        ];
    }

    /**
     * format the returned data
     */
    private function normalizeArcgisCandidate(array $c, string $query): array
    {
        $loc  = $c['location']   ?? [];
        $attr = $c['attributes'] ?? [];

        $display = $attr['LongLabel'] ?? $c['address'] ?? $attr['Place_addr'] ?? $query;
        $street  = $attr['Address']   ?? $attr['Place_addr'] ?? null;

        return [
            'display_name' => $display,
            'raw_address'  => $query,
            'street'       => $street,
            'city'         => $attr['City']        ?? null,
            'state'        => $attr['Region']      ?? null,
            'postal'       => $attr['Postal']      ?? null,
            'country'      => $attr['CountryCode'] ?? null,
            'lat'          => $loc['y'] ?? null,
            'lon'          => $loc['x'] ?? null,
        ];
    }

    /**
     * get top-n suggestions for a free-form query
     */
    public function suggestions(string $query, int $limit = 5, ?User $user = null): array
    {
        $provider = $this->cfg["provider"] ?? "arcgis";

        if (mb_strlen(trim($query)) < 3) {
            return [];
        }

        $bias = $this->biasForUser($user);

        return match ($provider) {
            "arcgis" => $this->arcgisSuggestions($query, $limit, $bias),
            default => [],
        };
    }

    /**
     * get the suggestions from arcgis
     */
    private function arcgisSuggestions(string $query, int $limit, ?array $bias = null): array
    {
        $key = $this->arcgisKey();
        if (!$key) {
            return [];
        }

        $params = [
            "singleLine" => $query,
            "outFields" => $this->cfg["arcgis"]["out_fields"] ?? "*",
            "maxLocations" => $limit,
            "category" => $this->cfg["arcgis"]["categories"] ?? "POI,Address",
            "countryCode" => $this->cfg["arcgis"]["country_code"] ?? null,
            "f" => "pjson",
            "token" => $key,
        ];

        if ($bias && $bias['lat'] && $bias['lon']) {
            $params['location'] = $bias['lon'] . ',' . $bias['lat'];
            if (!empty($bias['radius_km'])) {
                $params['searchExtent'] = $this->bboxFromBias(
                    (float) $bias['lat'],
                    (float) $bias['lon'],
                    (float) $bias['radius_km']
                );
            }
        }

        if (!empty($this->cfg["arcgis"]["store"])) {
            $params["forStorage"] = "true";
        }

        $res = $this->http()->get(
            $this->arcgisBase() . "/findAddressCandidates",
            $params,
        );
        $this->logArcgisResponse('suggestions', $params, $res);
        if (!$res->ok()) {
            return [];
        }

        $json = $res->json();
        $cands = array_slice($json["candidates"] ?? [], 0, $limit);

        // normalize to the same structure used elsewhere
        return array_values(
            array_filter(
                array_map(
                    fn($c) => $this->normalizeArcgisCandidate($c, $query),
                    $cands,
                ),
            ),
        );
    }
}
