using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Options;
using TakeoutSaaS.Application.App.Common.Geo;
using TakeoutSaaS.TenantApi.Options;

namespace TakeoutSaaS.TenantApi.Services;

/// <summary>
/// 腾讯地图地理编码服务（服务端签名版）。
/// </summary>
public sealed class TencentMapGeocodingService(
    IHttpClientFactory httpClientFactory,
    IOptionsMonitor<TencentMapOptions> optionsMonitor,
    ILogger<TencentMapGeocodingService> logger) : IAddressGeocodingService
{
    /// <summary>
    /// HttpClient 名称。
    /// </summary>
    public const string HttpClientName = "TencentMapWebService";

    /// <summary>
    /// 根据地址解析经纬度。
    /// </summary>
    /// <param name="rawAddress">地址文本。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>解析结果；失败时返回 <c>null</c>。</returns>
    public async Task<(decimal Latitude, decimal Longitude)?> GeocodeAsync(
        string rawAddress,
        CancellationToken cancellationToken)
    {
        var result = await GeocodeWithDetailsAsync(rawAddress, cancellationToken);
        return result.Succeeded && result.Latitude is not null && result.Longitude is not null
            ? (result.Latitude.Value, result.Longitude.Value)
            : null;
    }

    /// <summary>
    /// 根据地址解析经纬度（含失败原因）。
    /// </summary>
    /// <param name="rawAddress">地址文本。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>解析结果。</returns>
    public async Task<AddressGeocodingResult> GeocodeWithDetailsAsync(
        string rawAddress,
        CancellationToken cancellationToken)
    {
        // 1. 预处理地址文本，空值直接返回。
        var address = rawAddress?.Trim() ?? string.Empty;
        if (string.IsNullOrWhiteSpace(address))
        {
            return AddressGeocodingResult.Failed("地址为空，无法定位");
        }

        // 2. 读取腾讯地图配置并做兜底校验。
        var options = optionsMonitor.CurrentValue;
        var key = options.WebServiceKey?.Trim() ?? string.Empty;
        var secret = options.WebServiceSecret?.Trim() ?? string.Empty;
        if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(secret))
        {
            logger.LogWarning("腾讯地图 WebService 未配置 key/sk，已跳过地址解析。");
            return AddressGeocodingResult.Failed("地图服务未配置");
        }

        // 3. 同时尝试两种签名方式，兼容不同编码策略。
        var baseUrl = NormalizeBaseUrl(options.BaseUrl);
        var geocoderPath = NormalizePath(options.GeocoderPath);
        string? lastMessage = null;
        foreach (var useEncodedValueInSignature in new[] { false, true })
        {
            var query = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                ["address"] = address,
                ["key"] = key
            };
            query["sig"] = BuildSignature(
                geocoderPath,
                query,
                secret,
                useEncodedValueInSignature);

            var requestUri = BuildRequestUri(baseUrl, geocoderPath, query);
            var response = await RequestAsync(requestUri, cancellationToken);
            if (response is null)
            {
                lastMessage = "地图服务响应无法解析";
                continue;
            }

            // 4. 成功状态直接返回经纬度。
            if (response.Status == 0 &&
                response.Latitude is not null &&
                response.Longitude is not null)
            {
                return AddressGeocodingResult.Success(response.Latitude.Value, response.Longitude.Value);
            }

            lastMessage = string.IsNullOrWhiteSpace(response.Message)
                ? $"地理编码失败，状态码：{response.Status}"
                : response.Message;

            // 5. 仅在签名错误时继续下一轮重试，其他状态直接终止。
            if (response.Status != 111)
            {
                break;
            }
        }

        return AddressGeocodingResult.Failed(lastMessage ?? "地理编码失败");
    }

    Task<AddressGeocodingResult> IAddressGeocodingService.GeocodeAsync(
        string address,
        CancellationToken cancellationToken)
        => GeocodeWithDetailsAsync(address, cancellationToken);

    private async Task<TencentGeocodeResponse?> RequestAsync(
        string requestUri,
        CancellationToken cancellationToken)
    {
        // 1. 发起请求并获取响应文本。
        using var httpClient = httpClientFactory.CreateClient(HttpClientName);
        using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
        using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
        var responseBody = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);

        // 2. 解析响应，失败时返回 null。
        if (!TryParseGeocodeResponse(responseBody, out var parsed))
        {
            logger.LogWarning("腾讯地图地理编码响应无法解析。");
            return null;
        }

        // 3. 非 0 状态记录诊断信息（不包含敏感密钥）。
        if (parsed.Status != 0)
        {
            logger.LogWarning(
                "腾讯地图地理编码失败，Status={Status}, Message={Message}",
                parsed.Status,
                parsed.Message);
        }

        return parsed;
    }

    private static string BuildRequestUri(
        string baseUrl,
        string geocoderPath,
        IReadOnlyDictionary<string, string> query)
    {
        var canonicalQuery = string.Join(
            "&",
            query
                .OrderBy(x => x.Key, StringComparer.Ordinal)
                .Select(x => $"{x.Key}={Uri.EscapeDataString(x.Value)}"));

        var requestUri = new Uri(new Uri(baseUrl, UriKind.Absolute), geocoderPath);
        return $"{requestUri}?{canonicalQuery}";
    }

    private static string BuildSignature(
        string geocoderPath,
        IReadOnlyDictionary<string, string> query,
        string secret,
        bool useEncodedValue)
    {
        var canonicalQuery = string.Join(
            "&",
            query
                .OrderBy(x => x.Key, StringComparer.Ordinal)
                .Select(x =>
                {
                    var value = useEncodedValue
                        ? Uri.EscapeDataString(x.Value)
                        : x.Value;
                    return $"{x.Key}={value}";
                }));

        var payload = $"{geocoderPath}?{canonicalQuery}{secret}";
        var hashBytes = MD5.HashData(Encoding.UTF8.GetBytes(payload));
        return Convert.ToHexString(hashBytes).ToLowerInvariant();
    }

    private static bool TryParseGeocodeResponse(
        string responseBody,
        out TencentGeocodeResponse response)
    {
        response = new TencentGeocodeResponse();
        try
        {
            using var document = JsonDocument.Parse(responseBody);
            var root = document.RootElement;

            response.Status = ReadStatus(root);
            response.Message = root.TryGetProperty("message", out var messageElement)
                ? messageElement.GetString() ?? string.Empty
                : string.Empty;

            if (!root.TryGetProperty("result", out var resultElement) ||
                resultElement.ValueKind != JsonValueKind.Object ||
                !resultElement.TryGetProperty("location", out var locationElement) ||
                locationElement.ValueKind != JsonValueKind.Object)
            {
                return true;
            }

            if (!TryReadDecimal(locationElement, "lat", out var latitude) ||
                !TryReadDecimal(locationElement, "lng", out var longitude))
            {
                return true;
            }

            response.Latitude = decimal.Round(latitude, 7);
            response.Longitude = decimal.Round(longitude, 7);
            return true;
        }
        catch (JsonException)
        {
            return false;
        }
    }

    private static int ReadStatus(JsonElement root)
    {
        if (!root.TryGetProperty("status", out var statusElement))
        {
            return -1;
        }

        return statusElement.ValueKind switch
        {
            JsonValueKind.Number when statusElement.TryGetInt32(out var numeric) => numeric,
            JsonValueKind.String when int.TryParse(
                statusElement.GetString(),
                NumberStyles.Integer,
                CultureInfo.InvariantCulture,
                out var parsed) => parsed,
            _ => -1
        };
    }

    private static bool TryReadDecimal(
        JsonElement parent,
        string propertyName,
        out decimal value)
    {
        value = 0m;
        if (!parent.TryGetProperty(propertyName, out var element))
        {
            return false;
        }

        return element.ValueKind switch
        {
            JsonValueKind.Number when element.TryGetDecimal(out var numericValue)
                => Assign(out value, numericValue),
            JsonValueKind.String when decimal.TryParse(
                element.GetString(),
                NumberStyles.Float,
                CultureInfo.InvariantCulture,
                out var parsedValue) => Assign(out value, parsedValue),
            _ => false
        };
    }

    private static bool Assign(out decimal target, decimal value)
    {
        target = value;
        return true;
    }

    private static string NormalizeBaseUrl(string? baseUrl)
    {
        var normalized = string.IsNullOrWhiteSpace(baseUrl)
            ? "https://apis.map.qq.com"
            : baseUrl.Trim();
        return normalized.EndsWith('/') ? normalized : $"{normalized}/";
    }

    private static string NormalizePath(string? path)
    {
        var normalized = string.IsNullOrWhiteSpace(path)
            ? "/ws/geocoder/v1/"
            : path.Trim();

        if (!normalized.StartsWith('/'))
        {
            normalized = $"/{normalized}";
        }

        if (!normalized.EndsWith('/'))
        {
            normalized = $"{normalized}/";
        }

        return normalized;
    }

    private sealed class TencentGeocodeResponse
    {
        public decimal? Latitude { get; set; }

        public decimal? Longitude { get; set; }

        public string Message { get; set; } = string.Empty;

        public int Status { get; set; }
    }
}
