<?php

namespace ChurchCRM\Utils;

use ChurchCRM\Bootstrapper;
use ChurchCRM\dto\SystemConfig;

class GeoUtils
{
    /**
     * Geocode an address to latitude/longitude using OpenStreetMap's Nominatim service.
     *
     * Nominatim is free and requires no API key. No admin configuration needed.
     * Supports both concatenated address strings and structured components.
     *
     * @param string $address The address to geocode (can be full address or just street)
     * @param string|null $city City name (improves accuracy when provided)
     * @param string|null $state State/province (improves accuracy when provided)
     * @param string|null $zip Postal code (improves accuracy when provided)
     * @param string|null $country Country name (improves accuracy when provided)
     * @return array{Latitude: float, Longitude: float} Latitude and longitude, or [0, 0] if not found
     */
    public static function getLatLong(
        string $address,
        ?string $city = null,
        ?string $state = null,
        ?string $zip = null,
        ?string $country = null
    ): array {
        $logger = LoggerUtils::getAppLogger();
        $localeInfo = Bootstrapper::getCurrentLocale();

        $lat = 0;
        $long = 0;

        if (empty(trim($address))) {
            $logger->warning('Geocoding: empty address provided');
            return ['Latitude' => $lat, 'Longitude' => $long];
        }

        try {
            $logger->debug('Using: Geo Provider - Nominatim (OpenStreetMap)');

            // Build structured query for better Nominatim accuracy
            $params = [
                'format' => 'json',
                'limit' => 1,
                'accept-language' => $localeInfo->getShortLocale(),
            ];

            // Build address for simple query with proper formatting
            $simplifiedAddress = trim($address);

            // Use structured query if components provided
            if (!empty($city) || !empty($state) || !empty($zip)) {
                $params['street'] = trim($address);
                if (!empty($city)) {
                    $params['city'] = trim($city);
                }
                if (!empty($state)) {
                    $params['state'] = trim($state);
                }
                if (!empty($zip)) {
                    $params['postalcode'] = trim($zip);
                }
                // Only add country if it's actually provided (not empty/null)
                if (!empty($country)) {
                    $params['country'] = trim($country);
                }
            } else {
                // Fallback: construct a clean address from available components
                $parts = [];
                $parts[] = $simplifiedAddress;
                if (!empty($city)) {
                    $parts[] = trim($city);
                }
                if (!empty($state)) {
                    $parts[] = trim($state);
                }
                if (!empty($zip)) {
                    $parts[] = trim($zip);
                }
                // Don't add country to simple query - it often causes matching to fail
                $params['q'] = implode(', ', $parts);
            }

            $url = 'https://nominatim.openstreetmap.org/search?' . http_build_query($params);

            // Nominatim ToS requires a User-Agent header; add timeout to avoid hanging PHP workers
            $context = stream_context_create([
                'http' => [
                    'method'  => 'GET',
                    'header'  => "User-Agent: ChurchCRM/7.0 (+https://churchcrm.io)\r\n",
                    'timeout' => 10,
                ],
            ]);

            $response = file_get_contents($url, false, $context);
            if ($response === false) {
                $logger->warning('Geocoding failed: Nominatim API request failed');
                return ['Latitude' => $lat, 'Longitude' => $long];
            }

            $results = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
            if (empty($results) || !\is_array($results)) {
                $logger->warning('Geocoding: No results found for address (see service log for familyId)');
                return ['Latitude' => $lat, 'Longitude' => $long];
            }

            $firstResult = $results[0];
            $lat = (float) $firstResult['lat'];
            $long = (float) $firstResult['lon'];

            $logger->debug('Geocoding successful: lat=' . $lat . ', lng=' . $long);
        } catch (\Throwable $exception) {
            $logger->warning('Geocoding error: ' . $exception->getMessage());
        }

        return [
            'Latitude'  => $lat,
            'Longitude' => $long,
        ];
    }

    /**
     * Returns a Google Maps directions deep-link URL.
     *
     * Prefer lat/lng when available — avoids client-side geocoding and gives a
     * precise pin. Falls back to an address string otherwise.
     * Returns an empty string when no destination can be determined.
     */
    public static function buildDirectionsUrl(string $address = '', float $lat = 0.0, float $lng = 0.0): string
    {
        $base = 'https://www.google.com/maps/dir/?api=1&destination=';
        if ($lat !== 0.0 && $lng !== 0.0) {
            return $base . $lat . ',' . $lng;
        }
        if (!empty($address)) {
            return $base . urlencode($address);
        }
        return '';
    }

    public static function drivingDistanceMatrix($address1, $address2): array
    {
        $logger = LoggerUtils::getAppLogger();
        $localeInfo = Bootstrapper::getCurrentLocale();
        $url = 'https://maps.googleapis.com/maps/api/distancematrix/json?';
        $url = $url . 'language=' . $localeInfo->getShortLocale();
        $url = $url . '&origins=' . urlencode($address1);
        $url = $url . '&destinations=' . urlencode($address2);
        $logger->debug($url);
        $gMapsResponse = file_get_contents($url);
        $details = json_decode($gMapsResponse, true, 512, JSON_THROW_ON_ERROR);
        $matrixElements = $details['rows'][0]['elements'][0];

        return [
            'distance' => $matrixElements['distance']['text'],
            'duration' => $matrixElements['duration']['text'],
        ];
    }

    // Function takes latitude and longitude
    // of two places as input and returns the
    // distance in miles.
    public static function latLonDistance($lat1, $lon1, $lat2, $lon2): string
    {
        // Formula for calculating radians between
        // latitude and longitude pairs.

        // Uses the Spherical Law of Cosines to find great circle distance.
        // Length of arc on surface of sphere

        // convert to radians to work with trig functions

        $lat1 = deg2rad($lat1);
        $lon1 = deg2rad($lon1);
        $lat2 = deg2rad($lat2);
        $lon2 = deg2rad($lon2);

        // determine angle between between points in radians
        $radians = acos(sin($lat1) * sin($lat2) + cos($lat1) * cos($lat2) * cos($lon1 - $lon2));

        // mean radius of Earth in kilometers
        $radius = 6371.0;

        // distance in kilometers is $radians times $radius
        $distance = $radians * $radius;

        // convert to miles
        if (strtoupper(SystemConfig::getValue('sDistanceUnit')) === 'MILES') {
            $distance = 0.6213712 * $distance;
        }

        // Return distance to three figures
        if ($distance < 10.0) {
            $distance_f = sprintf('%0.2f', $distance);
        } elseif ($distance < 100.0) {
            $distance_f = sprintf('%0.1f', $distance);
        } else {
            $distance_f = sprintf('%0.0f', $distance);
        }

        return $distance_f;
    }

    public static function latLonBearing($lat1, $lon1, $lat2, $lon2): string
    {
        // Formula for determining the bearing from ($lat1,$lon1) to ($lat2,$lon2)

        // This is the initial bearing which if followed in a straight line will take
        // you from the start point to the end point; in general, the bearing you are
        // following will have varied by the time you get to the end point (if you were
        // to go from say 35°N,45°E (Baghdad) to 35°N,135°E (Osaka), you would start on
        // a bearing of 60° and end up on a bearing of 120°!).

        // If you are standing at ($lat1,$lon1) and pointing the shortest distance to
        // ($lat2,$lon2) this function tells you which direction you are pointing.
        // Returns one of the following 16 directions.
        // N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW

        // convert to radians to work with trig functions
        $lat1 = deg2rad($lat1);
        $lon1 = deg2rad($lon1);
        $lat2 = deg2rad($lat2);
        $lon2 = deg2rad($lon2);

        $y = sin($lon2 - $lon1) * cos($lat2);
        $x = cos($lat1) * sin($lat2) - sin($lat1) * cos($lat2) * cos($lon2 - $lon1);
        $bearing = atan2($y, $x);

        // Convert from radians to degrees
        $bearing = sprintf('%5.1f', rad2deg($bearing));

        // Convert to directions
        // -180=S   -135=SW   -90=W   -45=NW   0=N   45=NE   90=E   135=SE   180=S
        if ($bearing < -191.25) {
            $direction = '---';
        } elseif ($bearing < -168.75) {
            $direction = gettext('S');
        } elseif ($bearing < -146.25) {
            $direction = gettext('SSW');
        } elseif ($bearing < -123.75) {
            $direction = gettext('SW');
        } elseif ($bearing < -101.25) {
            $direction = gettext('WSW');
        } elseif ($bearing < -78.75) {
            $direction = gettext('W');
        } elseif ($bearing < -56.25) {
            $direction = gettext('WNW');
        } elseif ($bearing < -33.75) {
            $direction = gettext('NW');
        } elseif ($bearing < -11.25) {
            $direction = gettext('NNW');
        } elseif ($bearing < 11.25) {
            $direction = gettext('N');
        } elseif ($bearing < 33.75) {
            $direction = gettext('NNE');
        } elseif ($bearing < 56.25) {
            $direction = gettext('NE');
        } elseif ($bearing < 78.75) {
            $direction = gettext('ENE');
        } elseif ($bearing < 101.25) {
            $direction = gettext('E');
        } elseif ($bearing < 123.75) {
            $direction = gettext('ESE');
        } elseif ($bearing < 146.25) {
            $direction = gettext('SE');
        } elseif ($bearing < 168.75) {
            $direction = gettext('SSE');
        } elseif ($bearing < 191.25) {
            $direction = gettext('S');
        } else {
            $direction = '+++';
        }

//    $direction  = $bearing . " " . $direction;

        return $direction;
    }
}
