using System.Globalization;
using System.Text.Json;
using Aop.Api;
using Aop.Api.Domain;
using Aop.Api.Request;
using Aop.Api.Util;
using Tiku.Application.Commerce;

namespace Tiku.Infrastructure.Commerce;

internal sealed class AlipayProvider : IPaymentProvider
{
    public string Provider => PaymentProviders.Alipay;

    public Task<CreatePaymentProviderResult> CreatePaymentAsync(
        PaymentProviderAccount account,
        CreatePaymentProviderRequest request,
        CancellationToken cancellationToken = default)
    {
        _ = cancellationToken;
        var client = BuildClient(account);
        var alipayRequest = new AlipayTradeWapPayRequest();
        alipayRequest.SetNotifyUrl(request.NotifyUrl);
        if (!string.IsNullOrWhiteSpace(request.ReturnUrl)) alipayRequest.SetReturnUrl(request.ReturnUrl);

        alipayRequest.SetBizModel(new AlipayTradeWapPayModel
        {
            OutTradeNo = request.OrderNo,
            Subject = request.Subject,
            TotalAmount = FormatYuan(request.AmountCents),
            ProductCode = "QUICK_WAP_WAY",
            QuitUrl = request.QuitUrl
        });
        var response = client.pageExecute(alipayRequest);
        if (string.IsNullOrWhiteSpace(response.Body))
            throw new PaymentProviderException("Alipay create payment returned an empty body.", "alipay_create_failed");

        var clientPayload = JsonSerializer.SerializeToElement(new
        {
            formHtml = response.Body
        });
        var rawPayload = JsonSerializer.SerializeToElement(new
        {
            response.Body,
            request.OrderNo,
            request.AmountCents
        });

        return Task.FromResult(new CreatePaymentProviderResult(
            Provider,
            request.Method,
            "pending",
            null,
            clientPayload,
            rawPayload));
    }

    public Task<PaymentNotificationResult> ParsePaymentNotificationAsync(
        PaymentProviderAccount account,
        PaymentNotificationRequest request,
        CancellationToken cancellationToken = default)
    {
        _ = BuildClient(account);
        var values = ToDictionary(request.Body);
        var signatureValid = AlipaySignature.RSACheckV1(
            values,
            Required(account.ConfigPublic, "alipayPublicKey"),
            GetString(account.ConfigPublic, "charset") ?? "UTF-8",
            GetString(account.ConfigPublic, "signType") ?? "RSA2",
            false);
        var eventId = GetValue(values, "notify_id") ?? GetValue(values, "trade_no") ?? Guid.NewGuid().ToString("N");
        var orderNo = GetValue(values, "out_trade_no")
                      ?? throw new PaymentProviderException("Alipay notification order number is missing.",
                          "alipay_notify_order_missing");
        var tradeStatus = GetValue(values, "trade_status");
        var amountCents = YuanToCents(GetValue(values, "total_amount") ?? GetValue(values, "receipt_amount"));
        DateTimeOffset? paidAt = DateTimeOffset.TryParse(GetValue(values, "gmt_payment"), out var parsedPaidAt)
            ? parsedPaidAt
            : null;

        return Task.FromResult(new PaymentNotificationResult(
            Provider,
            "payment_notify",
            eventId,
            orderNo,
            GetValue(values, "trade_no"),
            amountCents,
            string.Equals(tradeStatus, "TRADE_SUCCESS", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(tradeStatus, "TRADE_FINISHED", StringComparison.OrdinalIgnoreCase),
            signatureValid,
            paidAt,
            request.Body));
    }

    private static DefaultAopClient BuildClient(PaymentProviderAccount account)
    {
        return new DefaultAopClient(
            Required(account.ConfigPublic, "gatewayUrl", "gateway"),
            Required(account.ConfigPublic, "appId"),
            Required(account.SecretPayload, "privateKey", "appPrivateKey"),
            "json",
            "1.0",
            "RSA2",
            Required(account.ConfigPublic, "alipayPublicKey"),
            "UTF-8",
            false);
    }

    private static string Required(JsonElement element, params string[] keys)
    {
        if (element.ValueKind == JsonValueKind.Object)
            foreach (var key in keys)
                if (element.TryGetProperty(key, out var property) &&
                    property.ValueKind == JsonValueKind.String &&
                    !string.IsNullOrWhiteSpace(property.GetString()))
                    return property.GetString()!;

        throw new PaymentProviderException(
            "Alipay provider configuration is incomplete.",
            "alipay_config_incomplete");
    }

    private static Dictionary<string, string> ToDictionary(JsonElement element)
    {
        var values = new Dictionary<string, string>(StringComparer.Ordinal);
        if (element.ValueKind != JsonValueKind.Object) return values;

        foreach (var property in element.EnumerateObject())
            values[property.Name] = property.Value.ValueKind == JsonValueKind.String
                ? property.Value.GetString() ?? string.Empty
                : property.Value.GetRawText();

        return values;
    }

    private static string? GetString(JsonElement element, params string[] keys)
    {
        if (element.ValueKind != JsonValueKind.Object) return null;

        foreach (var key in keys)
            if (element.TryGetProperty(key, out var property) &&
                property.ValueKind == JsonValueKind.String)
                return property.GetString();

        return null;
    }

    private static string? GetValue(IReadOnlyDictionary<string, string> values, string key)
    {
        return values.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)
            ? value
            : null;
    }

    private static int YuanToCents(string? value)
    {
        return decimal.TryParse(value, out var amount)
            ? (int)Math.Round(amount * 100, MidpointRounding.AwayFromZero)
            : 0;
    }

    private static string FormatYuan(int cents)
    {
        return (cents / 100m).ToString("0.00", CultureInfo.InvariantCulture);
    }
}