using LSFE.MobApp.DTO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace LSFE.MobApp.Services
{
    public class AddressService
    {
        private readonly ILogger<AddressService> _logger;
        private readonly Repository<Entities.AddressCache> _addressCache;

        // Known location overrides for areas with incorrect OSM data
        private static readonly List<LocationOverride> _locationOverrides = new()
        {
            // Lloyd Laboratories - FBIC area (Tikay, not Sumapang Matanda)
            new LocationOverride
            {
                Name = "First Bulacan Industrial City (FBIC)",
                MinLat = 14.835, MaxLat = 14.842,
                MinLng = 120.850, MaxLng = 120.860,
                Street = "Lloyd Avenue",
                Subdivision = "First Bulacan Industrial City",
                Barangay = "Tikay",
                City = "Malolos",
                Province = "Bulacan",
                Postcode = "3000"
            },
            // Add more known areas here as needed
        };

        public AddressService(ILogger<AddressService> logger, Repository<Entities.AddressCache> addressCache)
        {
            _logger = logger;
            _addressCache = addressCache;
        }

        public async Task<(LocationResult result, bool isHumanReadable)> GetCurrentLocationAsync()
        {
            await EnsurePermissionsAsync();

            var location =
             await Geolocation.Default.GetLastKnownLocationAsync()
             ?? await Geolocation.Default.GetLocationAsync(
                 new GeolocationRequest(
                     GeolocationAccuracy.High,
                     TimeSpan.FromSeconds(30)));

            if (location == null)
                throw new Exception("Unable to get location.");

            double lat = location.Latitude;
            double lng = location.Longitude;

            var (address, isHumanReadable) = await GetAddressAsync(lat, lng);

            return (new LocationResult
            {
                Latitude = lat,
                Longitude = lng,
                Address = address
            }, isHumanReadable);
        }

        // ================== PERMISSIONS ==================

        private async Task EnsurePermissionsAsync()
        {
            var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();

            if (status != PermissionStatus.Granted)
                status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();

            if (status != PermissionStatus.Granted)
                throw new Exception("Location permission is required.");
        }

        // ================== RETRY GEOCODING ==================

        /// <summary>
        /// Retries geocoding for a coordinate-only address when internet becomes available.
        /// Use this to convert "Lat: X, Lng: Y" back to human-readable addresses.
        /// </summary>
        /// <param name="lat">Latitude</param>
        /// <param name="lng">Longitude</param>
        /// <returns>Tuple of (address, isHumanReadable)</returns>
        public async Task<(string address, bool isHumanReadable)> RetryGeocodingAsync(double lat, double lng)
        {
            _logger.LogInformation($"Retrying geocoding for coordinates: {lat}, {lng}");

            // Clear cache for this location to force fresh geocoding attempt
            await ClearCacheForLocationAsync(lat, lng);

            // Try to geocode again
            return await GetAddressAsync(lat, lng);
        }

        /// <summary>
        /// Clears cached address for a specific location
        /// </summary>
        private async Task ClearCacheForLocationAsync(double lat, double lng)
        {
            const double radius = 0.0002; // ~20 meters
            double latMin = lat - radius;
            double latMax = lat + radius;
            double lngMin = lng - radius;
            double lngMax = lng + radius;

            var cachedEntries = await _addressCache.GetAllAsync();
                cachedEntries.Where(x =>
                x.Latitude >= latMin &&
                x.Latitude <= latMax &&
                x.Longitude >= lngMin &&
                x.Longitude <= lngMax
            );

            foreach (var entry in cachedEntries)
            {
                await _addressCache.DeleteAsync(entry);
            }
        }

        // ================== ADDRESS WITH OVERRIDE ==================

        private async Task<Entities.AddressCache?> GetNearbyAsync(double lat, double lng)
        {
            const double radius = 0.0025; // mobile-safe radius
            var cutoff = DateTime.UtcNow.AddHours(-24);

            double latMin = lat - radius;
            double latMax = lat + radius;
            double lngMin = lng - radius;
            double lngMax = lng + radius;

            var data = await _addressCache.GetAllAsync();
            return await _addressCache.FirstOrDefaultAsync(x =>
                x.Latitude >= latMin &&
                x.Latitude <= latMax &&
                x.Longitude >= lngMin &&
                x.Longitude <= lngMax &&
                x.CachedAt >= cutoff
            );
        }

        public async Task<(string address, bool isHumanReadable)> GetAddressAsync(double lat, double lng)
        {
            // 1️⃣ Check cache first
            var cached = await GetNearbyAsync(lat, lng);
            if (cached != null)
                return (cached.Address, IsAddressHumanReadable(cached.Address));

            // 2️⃣ Check for location override (for areas with known bad OSM data)
            var locationOverride = GetLocationOverride(lat, lng);

            // 3️⃣ Get address from geocoding APIs
            var geoAddress = await GetGeocodedAddressAsync(lat, lng);

            // 4️⃣ Apply override corrections if found
            string finalAddress;
            bool isHumanReadable;

            if (locationOverride != null)
            {
                finalAddress = ApplyLocationOverride(geoAddress, locationOverride);
                isHumanReadable = true; // Override always provides human-readable address
                _logger.LogInformation($"Applied location override for {locationOverride.Name}");
            }
            else if (!string.IsNullOrWhiteSpace(geoAddress))
            {
                // 5️⃣ Verify and enhance with nearby POI search
                finalAddress = await EnhanceWithNearbyPOI(lat, lng, geoAddress);
                isHumanReadable = true; // Got address from geocoding API
            }
            else
            {
                // 6️⃣ No internet or geocoding failed - return coordinates
                finalAddress = $"Lat: {lat:F6}, Lng: {lng:F6}";
                isHumanReadable = false;
                _logger.LogWarning($"Failed to geocode address, returning coordinates: {finalAddress}");
            }

            // Save to cache (even if it's just coordinates)
            await SaveCache(lat, lng, finalAddress);

            return (finalAddress, isHumanReadable);
        }

        /// <summary>
        /// Checks if an address is human-readable or just coordinates
        /// </summary>
        private bool IsAddressHumanReadable(string address)
        {
            if (string.IsNullOrWhiteSpace(address))
                return false;

            // If address starts with "Lat:" or contains only coordinates format, it's not human-readable
            if (address.StartsWith("Lat:", StringComparison.OrdinalIgnoreCase) ||
                address.StartsWith("Latitude:", StringComparison.OrdinalIgnoreCase))
                return false;

            // Check if it's just a coordinate pattern (numbers, dots, commas, spaces)
            var cleanAddress = address.Replace("Lat:", "").Replace("Lng:", "").Replace("Long:", "")
                .Replace("Latitude:", "").Replace("Longitude:", "").Trim();

            // If after removing coordinate keywords, we only have numbers/dots/commas, it's not human-readable
            if (cleanAddress.All(c => char.IsDigit(c) || c == '.' || c == ',' || c == ' ' || c == ':' || c == '-'))
                return false;

            // If we have actual text (city names, street names, etc.), it's human-readable
            return true;
        }

        /// <summary>
        /// Checks if coordinates fall within a known override area
        /// </summary>
        private LocationOverride? GetLocationOverride(double lat, double lng)
        {
            return _locationOverrides.FirstOrDefault(o =>
                lat >= o.MinLat && lat <= o.MaxLat &&
                lng >= o.MinLng && lng <= o.MaxLng);
        }

        /// <summary>
        /// Applies override data to correct wrong API results
        /// </summary>
        private string ApplyLocationOverride(string? apiAddress, LocationOverride ovr)
        {
            var parts = new List<string>();

            // Try to extract establishment name from API result
            if (!string.IsNullOrWhiteSpace(apiAddress))
            {
                var apiParts = apiAddress.Split(',').Select(p => p.Trim()).ToArray();
                if (apiParts.Length > 0 && !apiParts[0].Contains("Highway") && !apiParts[0].Contains("Road"))
                {
                    parts.Add(apiParts[0]); // Keep establishment name from API
                }
            }

            // Use override data for everything else
            if (!string.IsNullOrWhiteSpace(ovr.Street))
                parts.Add(ovr.Street);

            if (!string.IsNullOrWhiteSpace(ovr.Subdivision))
                parts.Add(ovr.Subdivision);

            if (!string.IsNullOrWhiteSpace(ovr.Barangay))
                parts.Add($"Brgy. {ovr.Barangay}");

            if (!string.IsNullOrWhiteSpace(ovr.City))
                parts.Add($"City of {ovr.City}");

            if (!string.IsNullOrWhiteSpace(ovr.Province))
                parts.Add(ovr.Province);

            if (!string.IsNullOrWhiteSpace(ovr.Postcode))
                parts.Add(ovr.Postcode);

            parts.Add("Philippines");

            return string.Join(", ", parts);
        }

        /// <summary>
        /// Enhances address with nearby POI to get building numbers and names
        /// </summary>
        private async Task<string> EnhanceWithNearbyPOI(double lat, double lng, string baseAddress)
        {
            try
            {
                // Use Overpass API to find exact building/POI at this location
                var poi = await GetNearbyBuildingAsync(lat, lng, radiusMeters: 10);

                if (poi != null && !string.IsNullOrWhiteSpace(poi.Name))
                {
                    // Check if POI name is already in address
                    if (!baseAddress.Contains(poi.Name, StringComparison.OrdinalIgnoreCase))
                    {
                        // Insert POI at the beginning
                        var parts = baseAddress.Split(',').Select(p => p.Trim()).ToList();

                        // Build enhanced address with POI
                        var enhanced = new List<string> { poi.Name };

                        // Add house number if available
                        if (!string.IsNullOrWhiteSpace(poi.HouseNumber))
                        {
                            enhanced[0] = $"{poi.Name}, {poi.HouseNumber}";
                        }

                        enhanced.AddRange(parts);
                        return string.Join(", ", RemoveDuplicatesAndSimilar(enhanced));
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Failed to enhance address with POI");
            }

            return baseAddress;
        }

        /// <summary>
        /// Gets exact building/POI at coordinates using Overpass API
        /// </summary>
        private async Task<POIInfo?> GetNearbyBuildingAsync(double lat, double lng, int radiusMeters = 10)
        {
            try
            {
                using var client = new HttpClient();
                client.Timeout = TimeSpan.FromSeconds(10);

                // Overpass query to find buildings/POI at exact location
                var query = $@"
                    [out:json][timeout:5];
                    (
                        node[""name""](around:{radiusMeters},{lat},{lng});
                        way[""name""](around:{radiusMeters},{lat},{lng});
                        node[""building""](around:{radiusMeters},{lat},{lng});
                        way[""building""](around:{radiusMeters},{lat},{lng});
                    );
                    out body;
                ";

                var content = new StringContent($"data={Uri.EscapeDataString(query)}");
                content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-www-form-urlencoded");

                var response = await client.PostAsync("https://overpass-api.de/api/interpreter", content);
                var json = await response.Content.ReadAsStringAsync();

                using var doc = JsonDocument.Parse(json);

                if (!doc.RootElement.TryGetProperty("elements", out var elements))
                    return null;

                // Find the closest POI with a name
                POIInfo? bestPOI = null;
                double closestDistance = double.MaxValue;

                foreach (var element in elements.EnumerateArray())
                {
                    if (!element.TryGetProperty("tags", out var tags))
                        continue;

                    var name = GetJsonString(tags, "name");
                    if (string.IsNullOrWhiteSpace(name))
                        continue;

                    // Get element coordinates
                    double elemLat = lat, elemLng = lng;
                    if (element.TryGetProperty("lat", out var latProp) &&
                        element.TryGetProperty("lon", out var lonProp))
                    {
                        elemLat = latProp.GetDouble();
                        elemLng = lonProp.GetDouble();
                    }

                    // Calculate distance
                    var distance = CalculateDistance(lat, lng, elemLat, elemLng);

                    if (distance < closestDistance)
                    {
                        closestDistance = distance;
                        bestPOI = new POIInfo
                        {
                            Name = name,
                            HouseNumber = GetJsonString(tags, "addr:housenumber"),
                            Street = GetJsonString(tags, "addr:street"),
                            Distance = distance
                        };
                    }
                }

                return bestPOI;
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Overpass API query failed");
                return null;
            }
        }

        /// <summary>
        /// Calculate distance between two coordinates in meters
        /// </summary>
        private double CalculateDistance(double lat1, double lng1, double lat2, double lng2)
        {
            const double R = 6371000; // Earth radius in meters
            var dLat = (lat2 - lat1) * Math.PI / 180;
            var dLng = (lng2 - lng1) * Math.PI / 180;
            var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
                    Math.Cos(lat1 * Math.PI / 180) * Math.Cos(lat2 * Math.PI / 180) *
                    Math.Sin(dLng / 2) * Math.Sin(dLng / 2);
            var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
            return R * c;
        }

        // ================== GEOCODING (BEST EFFORT) ==================

        private async Task<string?> GetGeocodedAddressAsync(double lat, double lng)
        {
            // Try Photon first (unlimited, good for PH)
            var photon = await GetPhotonAddressAsync(lat, lng);
            if (!string.IsNullOrWhiteSpace(photon))
                return photon;

            // Fallback to Nominatim
            var nominatim = await GetNominatimAddressAsync(lat, lng);
            if (!string.IsNullOrWhiteSpace(nominatim))
                return nominatim;

            return null;
        }

        private async Task<string?> GetPhotonAddressAsync(double lat, double lng)
        {
            try
            {
                using var client = new HttpClient();
                client.Timeout = TimeSpan.FromSeconds(10);

                var url = $"https://photon.komoot.io/reverse?lon={lng}&lat={lat}";
                var json = await client.GetStringAsync(url);
                using var doc = JsonDocument.Parse(json);

                if (!doc.RootElement.TryGetProperty("features", out var features) ||
                    features.GetArrayLength() == 0)
                    return null;

                var props = features[0].GetProperty("properties");
                return FormatPhotonAddress(props);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Photon geocoding failed");
                return null;
            }
        }

        private async Task<string?> GetNominatimAddressAsync(double lat, double lng)
        {
            try
            {
                using var client = new HttpClient();
                client.Timeout = TimeSpan.FromSeconds(10);
                client.DefaultRequestHeaders.Add("User-Agent", "LSFE.MobApp/1.0");

                var url = $"https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lng}&format=json&addressdetails=1";
                var json = await client.GetStringAsync(url);
                using var doc = JsonDocument.Parse(json);

                if (!doc.RootElement.TryGetProperty("address", out var address))
                    return null;

                return FormatNominatimAddress(address);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Nominatim geocoding failed");
                return null;
            }
        }

        private string FormatPhotonAddress(JsonElement props)
        {
            var parts = new List<string>();

            var name = GetJsonString(props, "name");
            var street = GetJsonString(props, "street");
            var district = GetJsonString(props, "district") ?? GetJsonString(props, "suburb");
            var city = GetJsonString(props, "city");
            var state = GetJsonString(props, "state");
            var postcode = GetJsonString(props, "postcode");
            var country = GetJsonString(props, "country");

            if (!string.IsNullOrWhiteSpace(name) && name != street)
                parts.Add(name);
            if (!string.IsNullOrWhiteSpace(street))
                parts.Add(street);
            if (!string.IsNullOrWhiteSpace(district))
                parts.Add($"Brgy. {district}");
            if (!string.IsNullOrWhiteSpace(city))
                parts.Add($"City of {city}");
            if (!string.IsNullOrWhiteSpace(state))
                parts.Add(state);
            if (!string.IsNullOrWhiteSpace(postcode))
                parts.Add(postcode);
            if (!string.IsNullOrWhiteSpace(country))
                parts.Add(country);

            return string.Join(", ", parts.Where(p => !string.IsNullOrWhiteSpace(p)));
        }

        private string FormatNominatimAddress(JsonElement address)
        {
            var parts = new List<string>();

            var industrial = GetJsonString(address, "industrial");
            var road = GetJsonString(address, "road");
            var suburb = GetJsonString(address, "suburb") ?? GetJsonString(address, "village");
            var city = GetJsonString(address, "city") ?? GetJsonString(address, "town");
            var state = GetJsonString(address, "state");
            var postcode = GetJsonString(address, "postcode");
            var country = GetJsonString(address, "country");

            if (!string.IsNullOrWhiteSpace(industrial))
                parts.Add(industrial);
            if (!string.IsNullOrWhiteSpace(road))
                parts.Add(road);
            if (!string.IsNullOrWhiteSpace(suburb))
                parts.Add($"Brgy. {suburb}");
            if (!string.IsNullOrWhiteSpace(city))
                parts.Add($"City of {city}");
            if (!string.IsNullOrWhiteSpace(state))
                parts.Add(state);
            if (!string.IsNullOrWhiteSpace(postcode))
                parts.Add(postcode);
            if (!string.IsNullOrWhiteSpace(country))
                parts.Add(country);

            return string.Join(", ", parts.Where(p => !string.IsNullOrWhiteSpace(p)));
        }

        // ================== HELPERS ==================

        private string? GetJsonString(JsonElement element, string property)
        {
            if (element.TryGetProperty(property, out var value) &&
                value.ValueKind == JsonValueKind.String)
            {
                return value.GetString();
            }
            return null;
        }

        private List<string> RemoveDuplicatesAndSimilar(List<string> parts)
        {
            var result = new List<string>();

            foreach (var part in parts)
            {
                if (string.IsNullOrWhiteSpace(part))
                    continue;

                bool isSimilar = false;
                foreach (var existing in result)
                {
                    if (IsSimilar(part, existing))
                    {
                        isSimilar = true;
                        if (part.Length > existing.Length)
                        {
                            result.Remove(existing);
                            result.Add(part);
                        }
                        break;
                    }
                }

                if (!isSimilar)
                    result.Add(part);
            }

            return result;
        }

        private bool IsSimilar(string a, string b)
        {
            if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
                return false;

            var aLower = a.ToLowerInvariant().Trim();
            var bLower = b.ToLowerInvariant().Trim();

            if (aLower == bLower)
                return true;

            var aClean = aLower.Replace(", inc.", "").Replace(" inc.", "").Replace(" corporation", "").Replace(" corp", "").Trim();
            var bClean = bLower.Replace(", inc.", "").Replace(" inc.", "").Replace(" corporation", "").Replace(" corp", "").Trim();

            if (aClean.Contains(bClean) || bClean.Contains(aClean))
            {
                var minLength = Math.Min(aClean.Length, bClean.Length);
                var maxLength = Math.Max(aClean.Length, bClean.Length);
                return (double)minLength / maxLength >= 0.6;
            }

            return false;
        }

        private async Task SaveCache(double lat, double lng, string address)
        {
            var cacheEntry = new Entities.AddressCache()
            {
                Latitude = lat,
                Longitude = lng,
                Address = address,
                CachedAt = DateTime.UtcNow
            };
            var isSuccess = await _addressCache.SaveAsync(cacheEntry);
        }

        private async Task<string> GetFromMauiGeocoderAsync(double lat, double lng)
        {
            try
            {
                var placemark = (await Geocoding.Default.GetPlacemarksAsync(lat, lng))?.FirstOrDefault();
                if (placemark == null)
                    return $"Lat: {lat:F6}, Long: {lng:F6}";

                return string.Join(", ", new[]
                {
                    placemark.FeatureName,
                    placemark.Thoroughfare,
                    placemark.SubLocality,
                    placemark.Locality,
                    placemark.AdminArea,
                    placemark.CountryName
                }.Where(x => !string.IsNullOrWhiteSpace(x)));
            }
            catch
            {
                return $"Lat: {lat:F6}, Long: {lng:F6}";
            }
        }
    }

    // ================== MODELS ==================

    internal class LocationOverride
    {
        public string Name { get; set; } = "";
        public double MinLat { get; set; }
        public double MaxLat { get; set; }
        public double MinLng { get; set; }
        public double MaxLng { get; set; }
        public string? Street { get; set; }
        public string? Subdivision { get; set; }
        public string? Barangay { get; set; }
        public string? City { get; set; }
        public string? Province { get; set; }
        public string? Postcode { get; set; }
    }

    internal class POIInfo
    {
        public string Name { get; set; } = "";
        public string? HouseNumber { get; set; }
        public string? Street { get; set; }
        public double Distance { get; set; }
    }
}