<?php

namespace App\Services;

use App\Exceptions\GeocodingException;
use App\Exceptions\ValidationException;
use Illuminate\Http\Client\Factory as HttpClient;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class GeocodeService
{
    private const BASE_URL = 'https://nominatim.openstreetmap.org';
    private const CACHE_TTL = 86400; // 24 hours
    private const USER_AGENT = 'ThrillWiki Geocoder';
    private const RATE_LIMIT = 1; // 1 request per second

    private HttpClient $http;
    private ?float $lastRequestTime = null;

    public function __construct(HttpClient $http)
    {
        $this->http = $http;
    }

    /**
     * Convert an address to coordinates
     *
     * @param string $address
     * @return array|null
     * @throws GeocodingException|ValidationException
     */
    public function geocode(string $address): ?array
    {
        $this->validateAddress($address);

        $cacheKey = "geocode:" . md5($address);
        return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($address) {
            try {
                $this->enforceRateLimit();

                $response = $this->http->withHeaders([
                    'User-Agent' => self::USER_AGENT
                ])->get(self::BASE_URL . '/search', [
                    'q' => $address,
                    'format' => 'json',
                    'addressdetails' => 1,
                    'limit' => 1
                ]);

                if (!$response->successful()) {
                    throw new GeocodingException('Geocoding API request failed: ' . $response->status());
                }

                $data = $response->json();
                if (empty($data)) {
                    return null;
                }

                return [
                    'lat' => (float) $data[0]['lat'],
                    'lon' => (float) $data[0]['lon'],
                    'display_name' => $data[0]['display_name'],
                    'address' => $data[0]['address'] ?? [],
                    'type' => $data[0]['type'] ?? null,
                ];
            } catch (\Exception $e) {
                Log::error('Geocoding error', [
                    'address' => $address,
                    'error' => $e->getMessage()
                ]);
                throw new GeocodingException('Failed to geocode address: ' . $e->getMessage());
            }
        });
    }

    /**
     * Convert coordinates to an address
     *
     * @param float $lat
     * @param float $lon
     * @return array|null
     * @throws GeocodingException|ValidationException
     */
    public function reverseGeocode(float $lat, float $lon): ?array
    {
        $this->validateCoordinates($lat, $lon);

        $cacheKey = "reverse:{$lat},{$lon}";
        return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($lat, $lon) {
            try {
                $this->enforceRateLimit();

                $response = $this->http->withHeaders([
                    'User-Agent' => self::USER_AGENT
                ])->get(self::BASE_URL . '/reverse', [
                    'lat' => $lat,
                    'lon' => $lon,
                    'format' => 'json',
                    'addressdetails' => 1
                ]);

                if (!$response->successful()) {
                    throw new GeocodingException('Reverse geocoding API request failed: ' . $response->status());
                }

                $data = $response->json();
                if (empty($data)) {
                    return null;
                }

                return [
                    'display_name' => $data['display_name'],
                    'address' => $data['address'] ?? [],
                    'type' => $data['type'] ?? null,
                ];
            } catch (\Exception $e) {
                Log::error('Reverse geocoding error', [
                    'lat' => $lat,
                    'lon' => $lon,
                    'error' => $e->getMessage()
                ]);
                throw new GeocodingException('Failed to reverse geocode coordinates: ' . $e->getMessage());
            }
        });
    }

    /**
     * Geocode multiple addresses
     *
     * @param array $addresses
     * @return array
     */
    public function batchGeocode(array $addresses): array
    {
        $results = [];
        foreach ($addresses as $address) {
            try {
                $results[$address] = $this->geocode($address);
            } catch (\Exception $e) {
                $results[$address] = null;
                Log::error('Batch geocoding error', [
                    'address' => $address,
                    'error' => $e->getMessage()
                ]);
            }
        }
        return $results;
    }

    /**
     * Validate coordinates
     *
     * @param float $lat
     * @param float $lon
     * @return bool
     * @throws ValidationException
     */
    public function validateCoordinates(float $lat, float $lon): bool
    {
        if ($lat < -90 || $lat > 90) {
            throw new ValidationException('Invalid latitude. Must be between -90 and 90');
        }

        if ($lon < -180 || $lon > 180) {
            throw new ValidationException('Invalid longitude. Must be between -180 and 180');
        }

        return true;
    }

    /**
     * Clear geocoding cache
     *
     * @param string|null $key
     * @return void
     */
    public function clearCache(?string $key = null): void
    {
        if ($key) {
            Cache::forget($key);
        } else {
            // Clear all geocoding related cache
            $keys = Cache::get('geocoding_cache_keys', []);
            foreach ($keys as $cacheKey) {
                Cache::forget($cacheKey);
            }
            Cache::forget('geocoding_cache_keys');
        }
    }

    /**
     * Warm up cache with common addresses
     *
     * @param array $addresses
     * @return void
     */
    public function warmCache(array $addresses): void
    {
        foreach ($addresses as $address) {
            try {
                $this->geocode($address);
            } catch (\Exception $e) {
                Log::error('Cache warming error', [
                    'address' => $address,
                    'error' => $e->getMessage()
                ]);
            }
        }
    }

    /**
     * Validate address string
     *
     * @param string $address
     * @return bool
     * @throws ValidationException
     */
    private function validateAddress(string $address): bool
    {
        if (strlen($address) < 5) {
            throw new ValidationException('Address must be at least 5 characters long');
        }

        if (strlen($address) > 200) {
            throw new ValidationException('Address must not exceed 200 characters');
        }

        return true;
    }

    /**
     * Enforce rate limiting for API requests
     *
     * @return void
     */
    private function enforceRateLimit(): void
    {
        if ($this->lastRequestTime !== null) {
            $timeSinceLastRequest = microtime(true) - $this->lastRequestTime;
            if ($timeSinceLastRequest < self::RATE_LIMIT) {
                usleep(($this->RATE_LIMIT - $timeSinceLastRequest) * 1000000);
            }
        }
        $this->lastRequestTime = microtime(true);
    }
}