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

namespace Baya.Infrastructure.CrossCutting.Seams;

/// <summary>
/// Deterministic, network-free <see cref="IGeocoder"/> — the mock seam. It resolves an address to a
/// stable point jittered around the known city centroid (an unknown city falls back to Iran's centroid),
/// so the same address always yields the same coordinates without any external call. A configured global
/// switch or a per-address marker forces the null-coordinate path so the "saved without a map pin" state
/// is testable. The real implementation swaps to a Neshan/Google geocoding client behind this contract.
/// </summary>
public sealed class MockGeocoder(IOptions<SeamOptions> options) : IGeocoder
{
    private readonly GeocodingOptions _options = options.Value.Geocoding;

    // A few real city centroids for plausible pins; everything else falls back to Iran's centroid.
    private static readonly IReadOnlyDictionary<string, (decimal Lat, decimal Lng)> Centroids =
        new Dictionary<string, (decimal, decimal)>(StringComparer.OrdinalIgnoreCase)
        {
            ["Tehran"] = (35.6892m, 51.3890m),
            ["Karaj"] = (35.8400m, 50.9391m),
            ["Mashhad"] = (36.2605m, 59.6168m),
            ["Isfahan"] = (32.6539m, 51.6660m),
            ["Shiraz"] = (29.5918m, 52.5837m),
            ["Tabriz"] = (38.0800m, 46.2919m),
            ["Ahvaz"] = (31.3183m, 48.6706m),
            ["Qom"] = (34.6416m, 50.8746m),
        };

    private static readonly (decimal Lat, decimal Lng) IranCentroid = (32.4279m, 53.6880m);

    public ValueTask<GeocodeResult> GeocodeAsync(
        string addressText,
        string cityName,
        string? districtName,
        CancellationToken cancellationToken = default)
    {
        var formatted = FormatAddress(addressText, cityName, districtName);

        var unresolved =
            _options.ReturnNullCoordinates ||
            (!string.IsNullOrEmpty(_options.LowConfidenceMarker) &&
             addressText is not null &&
             addressText.Contains(_options.LowConfidenceMarker, StringComparison.OrdinalIgnoreCase));

        if (unresolved)
            return ValueTask.FromResult(new GeocodeResult(null, null, formatted, 0.2));

        var centroid = Centroids.TryGetValue(cityName ?? string.Empty, out var c) ? c : IranCentroid;

        // Deterministic ±~0.045° jitter (~5 km) derived from a stable FNV-1a hash of the full text —
        // never string.GetHashCode(), which is randomized per process and would break test determinism.
        var seed = StableHash($"{cityName}|{districtName}|{addressText}");
        var latOffset = Offset(seed);
        var lngOffset = Offset(seed >> 16 ^ seed);

        var lat = decimal.Round(centroid.Lat + latOffset, 6);
        var lng = decimal.Round(centroid.Lng + lngOffset, 6);

        return ValueTask.FromResult(new GeocodeResult(lat, lng, formatted, _options.ResolvedConfidence));
    }

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

    // Maps the low 16 bits of the seed to a signed offset in [-0.045, +0.045] degrees.
    private static decimal Offset(uint seed) => ((seed & 0xFFFF) / 65535m - 0.5m) * 0.09m;

    private static uint StableHash(string value)
    {
        const uint offsetBasis = 2166136261;
        const uint prime = 16777619;

        var hash = offsetBasis;
        foreach (var b in Encoding.UTF8.GetBytes(value))
        {
            hash ^= b;
            hash *= prime;
        }

        return hash;
    }
}
