#nullable enable
using System.Globalization;
using System.Text.Json;
using Baya.Application.Contracts.Common;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Baya.Infrastructure.CrossCutting.Seams.Real;

/// <summary>
/// Real <see cref="IGeocoder"/> over the <b>Neshan</b> geocoding API (refinement-phase-8, 5.4) — turns a typed
/// address into coordinates for the b9 EVV distance check. Selected by <c>Seams:Geocoding:Provider = neshan</c>.
/// Coordinates are parsed to <see cref="decimal"/> (never float) so the downstream haversine stays exact. An
/// address Neshan can't resolve yields null coordinates (the "saved without a map pin" state) rather than an
/// error — the address is still saved. REQ-008's user-supplied pin reduces, but doesn't remove, the need for this.
/// </summary>
public sealed class NeshanGeocoder(
    HttpClient httpClient,
    IOptions<SeamOptions> options,
    ILogger<NeshanGeocoder> logger) : IGeocoder
{
    private const string DefaultBaseUrl = "https://api.neshan.org";
    private readonly GeocodingOptions _options = options.Value.Geocoding;

    public async ValueTask<GeocodeResult> GeocodeAsync(
        string addressText,
        string cityName,
        string? districtName,
        CancellationToken cancellationToken = default)
    {
        var formatted = FormatAddress(addressText, cityName, districtName);
        var baseUrl = string.IsNullOrWhiteSpace(_options.BaseUrl) ? DefaultBaseUrl : _options.BaseUrl.TrimEnd('/');

        try
        {
            using var request = new HttpRequestMessage(HttpMethod.Get,
                $"{baseUrl}/v1/geocoding?address={Uri.EscapeDataString(formatted)}");
            request.Headers.Add("Api-Key", _options.ApiKey);

            using var response = await httpClient.SendAsync(request, cancellationToken);
            var raw = await response.Content.ReadAsStringAsync(cancellationToken);
            response.EnsureSuccessStatusCode();

            using var doc = JsonDocument.Parse(raw);
            var root = doc.RootElement;

            var ok = root.TryGetProperty("status", out var status)
                     && string.Equals(status.GetString(), "OK", StringComparison.OrdinalIgnoreCase);

            if (ok && root.TryGetProperty("location", out var location)
                   && location.TryGetProperty("y", out var y)
                   && location.TryGetProperty("x", out var x)
                   && TryDecimal(y, out var lat) && TryDecimal(x, out var lng))
            {
                // Neshan: x = longitude, y = latitude.
                return new GeocodeResult(decimal.Round(lat, 6), decimal.Round(lng, 6), formatted, _options.ResolvedConfidence);
            }

            logger.LogWarning("Neshan could not resolve an address to coordinates (status {Status})",
                ok ? "OK-no-location" : "not-OK");
        }
        catch (Exception ex) when (ex is HttpRequestException or JsonException or TaskCanceledException)
        {
            // A geocoder outage must never block saving an address — degrade to the null-pin state.
            logger.LogWarning(ex, "Neshan geocoding failed; saving the address without a map pin");
        }

        return new GeocodeResult(null, null, formatted, 0.2);
    }

    private static bool TryDecimal(JsonElement element, out decimal value)
    {
        if (element.ValueKind == JsonValueKind.Number && element.TryGetDecimal(out value))
            return true;

        if (element.ValueKind == JsonValueKind.String
            && decimal.TryParse(element.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out value))
            return true;

        value = 0m;
        return false;
    }

    private static string FormatAddress(string addressText, string cityName, string? districtName) =>
        string.Join("، ", new[] { cityName, districtName, addressText }
            .Where(part => !string.IsNullOrWhiteSpace(part)));
}
