using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Commerce;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
using Tiku.Domain.Commerce;
using Tiku.Domain.Operations;
using Tiku.Domain.Tenancy;
using NotificationSeverity = Tiku.Domain.Commerce.NotificationSeverity;

namespace Tiku.Infrastructure.Commerce;

internal sealed partial class CommerceAdminService
{
    private async Task AssertAdminAsync(CommerceAdminActor actor, CancellationToken cancellationToken)
    {
        var access = await currentAccessContext.GetAsync(cancellationToken);
        if (!access.IsCurrentTenantMember ||
            access.UserId != actor.UserId ||
            access.TenantId != actor.TenantId ||
            !access.HasTenantPermission(BackendPermissions.TenantCommerceOperate))
            throw new CommerceException("Tenant admin access is required.", "tenant_admin_access_denied");
    }

    private async Task<CurrentDataScope> RequireDataScopeAsync(
        CommerceAdminActor actor,
        CancellationToken cancellationToken)
    {
        await AssertAdminAsync(actor, cancellationToken);
        return (await currentAccessContext.GetAsync(cancellationToken)).DataScope;
    }

    private static TenantPaymentProviderItem ToPaymentAccountItem(TenantExternalProviderItem item)
    {
        return new TenantPaymentProviderItem(
            item.Id,
            item.Provider,
            GetJsonString(item.ConfigPublic, "mode") ?? "TenantCollect",
            item.DisplayName,
            item.Status,
            item.SecretRef,
            item.Priority,
            item.ConfigPublic,
            item.CreatedAt,
            item.UpdatedAt);
    }

    private static TenantSecretItem ToSecretItem(TenantSecret item)
    {
        return new TenantSecretItem(item.Id, item.Purpose, item.Provider, item.SecretKey, item.SecretRef,
            item.Status.ToString(),
            item.RotatedAt, item.ExpiresAt, item.UpdatedAt);
    }

    private static CodeBatchItem ToCodeBatchItem(CodeBatch item)
    {
        return new CodeBatchItem(item.Id, item.Name, item.TotalCount, item.Days ?? 0, item.RegionId, item.SaleType,
            item.Channel,
            item.DefaultUnitPriceCents, item.CostPriceCents, item.IssuedAt, item.Remark, item.CreatedAt);
    }

    private static ActivationCodeItem ToActivationCodeItem(ActivationCode item)
    {
        return new ActivationCodeItem(item.Id, item.BatchId, item.Code, item.Days, item.IsUsed, item.UsedBy,
            item.UsedAt, item.SaleType,
            item.SoldTo, item.Remark, item.CreatedAt);
    }

    private static CommerceOrderItem ToOrderItem(Order order)
    {
        return new CommerceOrderItem(order.Id, order.OrderNo, order.Status.ToString(), order.PlanId, order.RegionId,
            order.ProductType,
            order.ProductName, order.AmountCents, FormatCny(order.AmountCents), order.PayMethod, order.PayProvider,
            order.TradeNo, order.Days, order.PaidAt, order.CreatedAt, order.RawPayload);
    }

    private static CommercePaymentItem ToPaymentItem(Payment payment, string orderNo)
    {
        return new CommercePaymentItem(payment.Id, payment.OrderId, orderNo, payment.Provider, payment.Method,
            payment.Status.ToString(),
            payment.AmountCents, FormatCny(payment.AmountCents), payment.ProviderTradeNo, payment.PaidAt,
            JsonSerializer.SerializeToElement(new { }), payment.RawPayload);
    }

    private static OrderStatus ParseOrderStatus(string? status)
    {
        return Enum.TryParse<OrderStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Order status is invalid.", "invalid_order_status");
    }

    private static PaymentStatus ParsePaymentStatus(string? status)
    {
        return Enum.TryParse<PaymentStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Payment status is invalid.", "invalid_payment_status");
    }

    private static PointActivityTaskStatus ParsePointTaskStatus(string? status)
    {
        return Enum.TryParse<PointActivityTaskStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Point task status is invalid.", "invalid_point_task_status");
    }

    private static PointExchangeItemStatus ParsePointExchangeItemStatus(string? status)
    {
        return Enum.TryParse<PointExchangeItemStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Point exchange item status is invalid.",
                "invalid_point_exchange_item_status");
    }

    private static PointExchangeOrderStatus ParsePointExchangeOrderStatus(string? status)
    {
        return Enum.TryParse<PointExchangeOrderStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Point exchange order status is invalid.",
                "invalid_point_exchange_order_status");
    }

    private static CouponRedemptionStatus ParseCouponRedemptionStatus(string? status)
    {
        return Enum.TryParse<CouponRedemptionStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Coupon redemption status is invalid.", "invalid_coupon_redemption_status");
    }

    private static CommerceRefundStatus ParseRefundStatus(string? status)
    {
        return Enum.TryParse<CommerceRefundStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Refund status is invalid.", "invalid_refund_status");
    }

    private static ReconciliationBatchStatus ParseReconciliationBatchStatus(string? status)
    {
        return Enum.TryParse<ReconciliationBatchStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Reconciliation batch status is invalid.",
                "invalid_reconciliation_batch_status");
    }

    private static ReconciliationIssueStatus ParseReconciliationIssueStatus(string? status)
    {
        return Enum.TryParse<ReconciliationIssueStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Reconciliation issue status is invalid.",
                "invalid_reconciliation_issue_status");
    }

    private static CommerceAdjustmentVoucherStatus ParseAdjustmentVoucherStatus(string? status)
    {
        return Enum.TryParse<CommerceAdjustmentVoucherStatus>(NormalizeEnum(status), true, out var parsed)
            ? parsed
            : throw new CommerceException("Adjustment voucher status is invalid.", "invalid_adjustment_voucher_status");
    }

    private async Task AssertOptionalReferenceAsync<TEntity>(
        DbSet<TEntity> set,
        Guid tenantId,
        Guid? id,
        string code,
        CancellationToken cancellationToken)
        where TEntity : class
    {
        if (!id.HasValue) return;

        var exists = await set.AnyAsync(
            item => EF.Property<Guid>(item, "TenantId") == tenantId && EF.Property<Guid>(item, "Id") == id.Value,
            cancellationToken);
        if (!exists) throw new CommerceException("Referenced commerce entity was not found.", code);
    }

    private async Task ApplyRefundToOrderAsync(CommerceRefundRequest refund, CancellationToken cancellationToken)
    {
        var order = await dbContext.Orders.SingleAsync(
            item => item.TenantId == refund.TenantId && item.Id == refund.OrderId,
            cancellationToken);
        if (order.RefundedAmountCents < order.AmountCents)
        {
            order.RefundedAmountCents = Math.Min(order.AmountCents, order.RefundedAmountCents + refund.AmountCents);
            order.Status = order.RefundedAmountCents >= order.AmountCents
                ? OrderStatus.Refunded
                : OrderStatus.PartiallyRefunded;
        }

        if (refund.PaymentId.HasValue)
        {
            var payment = await dbContext.Payments.SingleOrDefaultAsync(
                item => item.TenantId == refund.TenantId && item.Id == refund.PaymentId.Value,
                cancellationToken);
            if (payment is not null)
            {
                payment.RefundedAmountCents =
                    Math.Min(payment.AmountCents, payment.RefundedAmountCents + refund.AmountCents);
                payment.Status = payment.RefundedAmountCents >= payment.AmountCents
                    ? PaymentStatus.Refunded
                    : PaymentStatus.PartiallyRefunded;
            }
        }
    }

    private static bool IsAllowedRefundTransition(CommerceRefundStatus from, CommerceRefundStatus to)
    {
        return from switch
        {
            CommerceRefundStatus.Requested => to is CommerceRefundStatus.Approved or CommerceRefundStatus.Rejected
                or CommerceRefundStatus.Cancelled,
            CommerceRefundStatus.Approved => to is CommerceRefundStatus.Processing or CommerceRefundStatus.Cancelled,
            CommerceRefundStatus.Processing => to is CommerceRefundStatus.Succeeded or CommerceRefundStatus.Failed,
            CommerceRefundStatus.Failed => to is CommerceRefundStatus.Processing or CommerceRefundStatus.Cancelled,
            _ => false
        };
    }

    private static bool IsAllowedAdjustmentTransition(CommerceAdjustmentVoucherStatus from,
        CommerceAdjustmentVoucherStatus to)
    {
        return from switch
        {
            CommerceAdjustmentVoucherStatus.Draft => to is CommerceAdjustmentVoucherStatus.PendingReview
                or CommerceAdjustmentVoucherStatus.Void,
            CommerceAdjustmentVoucherStatus.PendingReview => to is CommerceAdjustmentVoucherStatus.Approved
                or CommerceAdjustmentVoucherStatus.Rejected or CommerceAdjustmentVoucherStatus.Void,
            CommerceAdjustmentVoucherStatus.Approved => to is CommerceAdjustmentVoucherStatus.Closed,
            _ => false
        };
    }

    private void AddRefundEvent(
        CommerceRefundRequest refund,
        CommerceRefundStatus? fromStatus,
        CommerceRefundStatus toStatus,
        string eventType,
        Guid actorUserId,
        object details)
    {
        dbContext.CommerceRefundEvents.Add(new CommerceRefundEvent
        {
            TenantId = refund.TenantId,
            RefundRequestId = refund.Id,
            FromStatus = fromStatus,
            ToStatus = toStatus,
            EventType = eventType,
            ActorUserId = actorUserId,
            Details = JsonSerializer.SerializeToElement(details)
        });
    }

    private Task AddAuditAsync(
        CommerceAdminActor actor,
        string action,
        string targetType,
        Guid targetId,
        object details,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        dbContext.AuditLogs.Add(new AuditLog
        {
            TenantId = actor.TenantId,
            ActorUserId = actor.UserId,
            Action = action,
            TargetType = targetType,
            TargetId = targetId.ToString(),
            Details = JsonSerializer.SerializeToElement(details)
        });
        return Task.CompletedTask;
    }

    private static string NormalizeEnum(string? value)
    {
        return string.Concat((value ?? string.Empty).Split(['_', '-', ' '], StringSplitOptions.RemoveEmptyEntries));
    }

    private static string NormalizeProvider(string? provider)
    {
        var normalized = (provider ?? string.Empty).Trim().ToLowerInvariant()
            .Replace("-", "_", StringComparison.Ordinal);
        return normalized switch
        {
            "wechat" or "wechatpay" or "wxpay" or "wx_pay" => PaymentProviders.WechatPay,
            "ali_pay" => PaymentProviders.Alipay,
            "" => throw new CommerceException("Provider is required.", "provider_required"),
            _ => normalized
        };
    }

    private static JsonElement WithPaymentMode(JsonElement element, string? mode)
    {
        var values = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
        if (element.ValueKind == JsonValueKind.Object)
            foreach (var property in element.EnumerateObject())
                values[property.Name] = property.Value.Clone();

        values["mode"] = JsonSerializer.SerializeToElement(
            string.IsNullOrWhiteSpace(mode) ? "TenantCollect" : mode.Trim());
        return JsonSerializer.SerializeToElement(values);
    }

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

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

        return null;
    }

    private static JsonElement JsonObjectOrDefault(JsonElement element)
    {
        return element.ValueKind == JsonValueKind.Object
            ? element.Clone()
            : JsonSerializer.SerializeToElement(new { });
    }

    private static void AssertNoSecrets(JsonElement element, string path)
    {
        if (element.ValueKind != JsonValueKind.Object) return;

        foreach (var property in element.EnumerateObject())
        {
            var key = property.Name.ToLowerInvariant();
            if (key is "secretref" or "secret_ref") continue;

            if (key.Contains("secret", StringComparison.Ordinal) ||
                key.Contains("privatekey", StringComparison.Ordinal) ||
                key is "appsecret" or "apiv3key" or "api_v3_key" or "accesskeysecret")
                throw new CommerceException($"{path} cannot contain secrets.", "public_config_contains_secret");

            AssertNoSecrets(property.Value, $"{path}.{property.Name}");
        }
    }

    private static ReconciliationImportPreview BuildImportPreview(string provider, JsonElement rows)
    {
        var normalizedProvider = NormalizeProvider(provider);
        var parsedRows = EnumerateImportRows(rows).ToArray();
        var paymentCount = parsedRows.Count(row => row.TransactionType == ReconciliationTransactionType.Payment);
        var refundCount = parsedRows.Count(row => row.TransactionType == ReconciliationTransactionType.Refund);
        var invalidCount = parsedRows.Count(row => row.MatchStatus != ReconciliationMatchStatus.Matched);
        var amountCents = parsedRows.Sum(row => row.AmountCents);
        var refundAmountCents = parsedRows.Sum(row => row.RefundAmountCents);
        var sourceHash = Convert
            .ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes($"{normalizedProvider}:{rows.GetRawText()}")))
            .ToLowerInvariant();
        return new ReconciliationImportPreview(
            parsedRows.Length,
            paymentCount,
            refundCount,
            invalidCount,
            amountCents,
            refundAmountCents,
            sourceHash);
    }

    private static IEnumerable<ReconciliationImportRow> EnumerateImportRows(JsonElement rows)
    {
        if (rows.ValueKind != JsonValueKind.Array)
            throw new CommerceException("Reconciliation rows must be an array.", "invalid_reconciliation_rows");

        foreach (var row in rows.EnumerateArray())
        {
            if (row.ValueKind != JsonValueKind.Object)
            {
                yield return InvalidImportRow("row_not_object", row);
                continue;
            }

            var transactionType = Enum.TryParse<ReconciliationTransactionType>(
                NormalizeEnum(GetJsonString(row, "transactionType", "transaction_type") ?? "payment"),
                true,
                out var parsedTransactionType)
                ? parsedTransactionType
                : ReconciliationTransactionType.Payment;
            var amountCents = GetJsonInt(row, "amountCents", "amount_cents", "amount");
            var refundAmountCents = GetJsonInt(row, "refundAmountCents", "refund_amount_cents", "refundAmount");
            var providerTradeNo = GetJsonString(row, "providerTradeNo", "provider_trade_no", "tradeNo");
            var providerRefundNo = GetJsonString(row, "providerRefundNo", "provider_refund_no");
            var orderNo = GetJsonString(row, "orderNo", "order_no");
            var refundNo = GetJsonString(row, "refundNo", "refund_no");
            var issueCode = GetJsonString(row, "issueCode", "issue_code");
            var matchStatus = Enum.TryParse<ReconciliationMatchStatus>(
                NormalizeEnum(GetJsonString(row, "matchStatus", "match_status") ?? "matched"),
                true,
                out var parsedMatchStatus)
                ? parsedMatchStatus
                : ReconciliationMatchStatus.AmountMismatch;
            if (string.IsNullOrWhiteSpace(providerTradeNo) &&
                string.IsNullOrWhiteSpace(providerRefundNo) &&
                string.IsNullOrWhiteSpace(orderNo) &&
                string.IsNullOrWhiteSpace(refundNo))
            {
                matchStatus = ReconciliationMatchStatus.MissingLocal;
                issueCode ??= "missing_business_identifier";
            }

            yield return new ReconciliationImportRow(
                transactionType,
                providerTradeNo,
                providerRefundNo,
                orderNo,
                refundNo,
                Math.Max(0, amountCents),
                Math.Max(0, refundAmountCents),
                GetJsonString(row, "providerStatus", "provider_status"),
                GetJsonString(row, "localStatus", "local_status"),
                matchStatus,
                issueCode,
                row.Clone());
        }
    }

    private static ReconciliationImportRow InvalidImportRow(string issueCode, JsonElement row)
    {
        return new ReconciliationImportRow(
            ReconciliationTransactionType.Payment,
            null,
            null,
            null,
            null,
            0,
            0,
            null,
            null,
            ReconciliationMatchStatus.AmountMismatch,
            issueCode,
            row.Clone());
    }

    private static CommerceReconciliationItem CreateReconciliationItem(
        Guid tenantId,
        Guid batchId,
        int rowNo,
        string provider,
        ReconciliationImportRow row)
    {
        return new CommerceReconciliationItem
        {
            TenantId = tenantId,
            BatchId = batchId,
            RowNo = rowNo,
            Provider = provider,
            TransactionType = row.TransactionType,
            ProviderTradeNo = row.ProviderTradeNo,
            ProviderRefundNo = row.ProviderRefundNo,
            OrderNo = row.OrderNo,
            RefundNo = row.RefundNo,
            AmountCents = row.AmountCents,
            RefundAmountCents = row.RefundAmountCents,
            ProviderStatus = row.ProviderStatus,
            LocalStatus = row.LocalStatus,
            MatchStatus = row.MatchStatus,
            Severity = row.MatchStatus == ReconciliationMatchStatus.Matched
                ? NotificationSeverity.Info
                : NotificationSeverity.Warning,
            IssueCode = row.IssueCode,
            Details = row.Details
        };
    }

    private static int GetJsonInt(JsonElement element, params string[] keys)
    {
        foreach (var key in keys)
        {
            if (!element.TryGetProperty(key, out var value)) continue;

            if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number)) return number;

            if (value.ValueKind == JsonValueKind.String &&
                int.TryParse(value.GetString(), CultureInfo.InvariantCulture, out var parsed)) return parsed;
        }

        return 0;
    }

    private static string GenerateActivationCode()
    {
        Span<byte> bytes = stackalloc byte[8];
        RandomNumberGenerator.Fill(bytes);
        return $"TKU{Convert.ToHexString(bytes)}";
    }

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

    private sealed record ReconciliationImportRow(
        ReconciliationTransactionType TransactionType,
        string? ProviderTradeNo,
        string? ProviderRefundNo,
        string? OrderNo,
        string? RefundNo,
        int AmountCents,
        int RefundAmountCents,
        string? ProviderStatus,
        string? LocalStatus,
        ReconciliationMatchStatus MatchStatus,
        string? IssueCode,
        JsonElement Details);
}