using System.Globalization;
using System.Text;
using System.Text.Json;
using TakeoutSaaS.Application.App.Members.Dto;
using TakeoutSaaS.Domain.Coupons.Entities;
using TakeoutSaaS.Domain.Coupons.Enums;
using TakeoutSaaS.Domain.Membership.Entities;
using TakeoutSaaS.Domain.Membership.Enums;
using TakeoutSaaS.Domain.Membership.Repositories;
using TakeoutSaaS.Domain.Orders.Entities;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Domain.Orders.Repositories;
using TakeoutSaaS.Domain.Payments.Enums;
using TakeoutSaaS.Shared.Abstractions.Tenancy;

namespace TakeoutSaaS.Application.App.Members.Handlers;

internal static class MemberCenterSupport
{
    private static readonly string[] AvatarColors =
    [
        "#1890ff",
        "#52c41a",
        "#fa8c16",
        "#722ed1",
        "#eb2f96",
        "#13c2c2",
        "#2f54eb",
        "#f56a00"
    ];

    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
    {
        PropertyNameCaseInsensitive = true
    };

    internal static string NormalizePhone(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        var chars = value.Where(char.IsDigit).ToArray();
        return chars.Length == 0 ? string.Empty : new string(chars);
    }

    internal static string MaskPhone(string normalizedPhone)
    {
        if (normalizedPhone.Length >= 11)
        {
            return $"{normalizedPhone[..3]}****{normalizedPhone[^4..]}";
        }

        if (normalizedPhone.Length >= 7)
        {
            return $"{normalizedPhone[..3]}***{normalizedPhone[^2..]}";
        }

        return normalizedPhone;
    }

    internal static string ResolveAvatarColor(string? seed)
    {
        var source = string.IsNullOrWhiteSpace(seed) ? "member" : seed;
        var hash = 0;
        foreach (var ch in source)
        {
            hash = (hash * 31 + ch) & int.MaxValue;
        }

        return AvatarColors[hash % AvatarColors.Length];
    }

    internal static string ResolveAvatarText(string? name, string fallbackPhone)
    {
        var normalizedName = (name ?? string.Empty).Trim();
        if (!string.IsNullOrWhiteSpace(normalizedName))
        {
            return normalizedName[..1];
        }

        return fallbackPhone.Length > 0 ? fallbackPhone[..1] : "会";
    }

    internal static string ResolveDisplayName(MemberProfile profile, MemberOrderMetrics metrics)
    {
        if (!string.IsNullOrWhiteSpace(profile.Nickname))
        {
            return profile.Nickname.Trim();
        }

        if (!string.IsNullOrWhiteSpace(metrics.LatestCustomerName))
        {
            return metrics.LatestCustomerName;
        }

        return profile.Mobile.Length >= 4 ? $"会员{profile.Mobile[^4..]}" : "会员";
    }

    internal static decimal ResolveDisplayAmount(Order order)
    {
        return order.PaidAmount > 0 ? order.PaidAmount : order.PayableAmount;
    }

    internal static string ResolveOrderStatusText(OrderStatus value)
    {
        return value switch
        {
            OrderStatus.PendingPayment => "待付款",
            OrderStatus.AwaitingPreparation => "待接单",
            OrderStatus.InProgress => "制作中",
            OrderStatus.Ready => "待取餐",
            OrderStatus.Completed => "已完成",
            OrderStatus.Cancelled => "已取消",
            _ => "未知"
        };
    }

    internal static string NormalizeRuleType(string? value)
    {
        return (value ?? string.Empty).Trim().ToLowerInvariant() switch
        {
            "amount" => "amount",
            "count" => "count",
            "both" => "both",
            _ => "none"
        };
    }

    internal static MemberTierBenefitsDto NormalizeBenefits(MemberTierBenefitsDto? source)
    {
        var model = source ?? new MemberTierBenefitsDto();

        var discountRate = model.Discount.DiscountRate;
        if (discountRate.HasValue)
        {
            discountRate = Math.Clamp(decimal.Round(discountRate.Value, 2, MidpointRounding.AwayFromZero), 0m, 10m);
        }

        var multiplier = model.PointMultiplier.Multiplier;
        if (multiplier.HasValue)
        {
            multiplier = Math.Clamp(decimal.Round(multiplier.Value, 2, MidpointRounding.AwayFromZero), 0m, 10m);
        }

        var monthlyGrantDay = Math.Clamp(model.MonthlyCoupon.GrantDay <= 0 ? 1 : model.MonthlyCoupon.GrantDay, 1, 28);
        var monthlyFreeTimes = Math.Max(0, model.FreeDelivery.MonthlyFreeTimes);

        return new MemberTierBenefitsDto
        {
            Discount = new MemberTierDiscountBenefitDto
            {
                Enabled = model.Discount.Enabled,
                DiscountRate = discountRate
            },
            PointMultiplier = new MemberTierPointMultiplierBenefitDto
            {
                Enabled = model.PointMultiplier.Enabled,
                Multiplier = multiplier
            },
            Birthday = new MemberTierBirthdayBenefitDto
            {
                Enabled = model.Birthday.Enabled,
                DoublePointsEnabled = model.Birthday.DoublePointsEnabled,
                CouponTemplateIds = NormalizeCouponIds(model.Birthday.CouponTemplateIds)
            },
            MonthlyCoupon = new MemberTierMonthlyCouponBenefitDto
            {
                Enabled = model.MonthlyCoupon.Enabled,
                GrantDay = monthlyGrantDay,
                CouponTemplateIds = NormalizeCouponIds(model.MonthlyCoupon.CouponTemplateIds)
            },
            FreeDelivery = new MemberTierFreeDeliveryBenefitDto
            {
                Enabled = model.FreeDelivery.Enabled,
                MonthlyFreeTimes = monthlyFreeTimes
            },
            PriorityDeliveryEnabled = model.PriorityDeliveryEnabled,
            ExclusiveServiceEnabled = model.ExclusiveServiceEnabled
        };
    }

    internal static MemberTierBenefitsDto DeserializeBenefits(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return new MemberTierBenefitsDto();
        }

        try
        {
            var model = JsonSerializer.Deserialize<MemberTierBenefitsDto>(value, JsonOptions);
            return NormalizeBenefits(model);
        }
        catch
        {
            return new MemberTierBenefitsDto();
        }
    }

    internal static string SerializeBenefits(MemberTierBenefitsDto benefits)
    {
        return JsonSerializer.Serialize(NormalizeBenefits(benefits), JsonOptions);
    }

    internal static string BuildConditionText(MemberTier tier)
    {
        var ruleType = NormalizeRuleType(tier.UpgradeRuleType);
        return ruleType switch
        {
            "amount" => $"累计消费满 {FormatAmount(tier.UpgradeAmountThreshold)}",
            "count" => $"累计消费满 {Math.Max(0, tier.UpgradeOrderCountThreshold ?? 0)} 次",
            "both" => $"累计消费满 {FormatAmount(tier.UpgradeAmountThreshold)} 且累计消费满 {Math.Max(0, tier.UpgradeOrderCountThreshold ?? 0)} 次",
            _ => "注册即享"
        };
    }

    internal static IReadOnlyList<string> BuildPerks(MemberTier tier, MemberTierBenefitsDto benefits)
    {
        var result = new List<string>();

        if (benefits.Discount.Enabled && benefits.Discount.DiscountRate.HasValue)
        {
            result.Add($"全场{benefits.Discount.DiscountRate.Value:0.##}折");
        }

        if (benefits.Birthday.Enabled)
        {
            if (benefits.Birthday.DoublePointsEnabled && benefits.Birthday.CouponTemplateIds.Count > 0)
            {
                result.Add("生日赠券+双倍积分");
            }
            else if (benefits.Birthday.DoublePointsEnabled)
            {
                result.Add("生日双倍积分");
            }
            else if (benefits.Birthday.CouponTemplateIds.Count > 0)
            {
                result.Add("生日赠券");
            }
            else
            {
                result.Add("生日特权");
            }
        }

        if (benefits.MonthlyCoupon.Enabled)
        {
            result.Add("每月赠券");
        }

        if (benefits.PointMultiplier.Enabled && benefits.PointMultiplier.Multiplier.HasValue && benefits.PointMultiplier.Multiplier.Value > 1)
        {
            result.Add($"积分{benefits.PointMultiplier.Multiplier.Value:0.##}倍");
        }

        if (benefits.FreeDelivery.Enabled)
        {
            result.Add("免配送费");
        }

        if (benefits.PriorityDeliveryEnabled)
        {
            result.Add("优先配送");
        }

        if (benefits.ExclusiveServiceEnabled)
        {
            result.Add("专属客服");
        }

        if (result.Count == 0 && tier.IsDefault)
        {
            return ["积分累计", "会员价商品"];
        }

        return result;
    }

    internal static async Task EnsureMemberCenterInitializedAsync(
        IMemberRepository memberRepository,
        IOrderRepository orderRepository,
        ITenantProvider tenantProvider,
        CancellationToken cancellationToken)
    {
        var tenantId = tenantProvider.GetCurrentTenantId();
        if (tenantId <= 0)
        {
            return;
        }

        var tiers = await memberRepository.GetTiersAsync(tenantId, cancellationToken);
        var changed = false;

        if (tiers.Count == 0)
        {
            foreach (var seed in BuildDefaultTierSeeds())
            {
                await memberRepository.AddTierAsync(seed, cancellationToken);
            }

            changed = true;
        }

        var memberDaySetting = await memberRepository.GetMemberDaySettingAsync(tenantId, cancellationToken);
        if (memberDaySetting is null)
        {
            await memberRepository.AddMemberDaySettingAsync(new MemberDaySetting
            {
                IsEnabled = true,
                Weekday = 2,
                ExtraDiscountRate = 9m
            }, cancellationToken);
            changed = true;
        }

        if (changed)
        {
            await memberRepository.SaveChangesAsync(cancellationToken);
            tiers = await memberRepository.GetTiersAsync(tenantId, cancellationToken);
        }

        var allOrders = await orderRepository.SearchAllOrdersAsync(
            tenantId,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            cancellationToken);

        var validOrders = allOrders
            .Where(IsQualifiedOrder)
            .Where(order => !string.IsNullOrWhiteSpace(NormalizePhone(order.CustomerPhone)))
            .ToList();

        if (validOrders.Count == 0)
        {
            return;
        }

        var nowUtc = DateTime.UtcNow;
        var orderMetricsMap = BuildOrderMetrics(validOrders, nowUtc);
        var mobileKeys = orderMetricsMap.Keys.ToList();
        var existedProfiles = await memberRepository.GetProfilesByMobilesAsync(tenantId, mobileKeys, cancellationToken);
        var profileLookup = existedProfiles.ToDictionary(
            item => NormalizePhone(item.Mobile),
            item => item,
            StringComparer.Ordinal);

        var defaultTier = ResolveDefaultTier(tiers);
        var profilesToAdd = new List<MemberProfile>();

        foreach (var (mobile, metrics) in orderMetricsMap)
        {
            if (profileLookup.ContainsKey(mobile))
            {
                continue;
            }

            profilesToAdd.Add(new MemberProfile
            {
                UserId = 0,
                Mobile = mobile,
                Nickname = string.IsNullOrWhiteSpace(metrics.LatestCustomerName) ? null : metrics.LatestCustomerName,
                MemberTierId = defaultTier?.Id,
                StoredBalance = 0,
                StoredRechargeBalance = 0,
                StoredGiftBalance = 0,
                Status = MemberStatus.Active,
                PointsBalance = 0,
                GrowthValue = ToGrowthValue(metrics.TotalAmount),
                JoinedAt = metrics.FirstOrderAt
            });
        }

        if (profilesToAdd.Count > 0)
        {
            await memberRepository.AddProfilesAsync(profilesToAdd, cancellationToken);
            await memberRepository.SaveChangesAsync(cancellationToken);
            existedProfiles = await memberRepository.GetProfilesByMobilesAsync(tenantId, mobileKeys, cancellationToken);
            profileLookup = existedProfiles.ToDictionary(
                item => NormalizePhone(item.Mobile),
                item => item,
                StringComparer.Ordinal);
        }

        if (profileLookup.Count == 0)
        {
            return;
        }

        var tierLookup = tiers.ToDictionary(item => item.Id);
        var orderedTiers = tiers
            .OrderBy(item => item.SortOrder)
            .ThenBy(item => item.Id)
            .ToList();

        var hasProfileChanged = false;
        foreach (var (mobile, profile) in profileLookup)
        {
            if (!orderMetricsMap.TryGetValue(mobile, out var metrics))
            {
                continue;
            }

            var targetTier = ResolveTargetTier(profile, orderedTiers, tierLookup, metrics, nowUtc);
            var targetJoinedAt = profile.JoinedAt == default
                ? metrics.FirstOrderAt
                : (profile.JoinedAt > metrics.FirstOrderAt ? metrics.FirstOrderAt : profile.JoinedAt);
            var targetGrowth = ToGrowthValue(metrics.TotalAmount);
            var targetNickname = string.IsNullOrWhiteSpace(profile.Nickname)
                ? (string.IsNullOrWhiteSpace(metrics.LatestCustomerName) ? profile.Nickname : metrics.LatestCustomerName)
                : profile.Nickname;

            if (profile.MemberTierId != targetTier?.Id ||
                profile.GrowthValue != targetGrowth ||
                profile.JoinedAt != targetJoinedAt ||
                profile.Nickname != targetNickname)
            {
                profile.MemberTierId = targetTier?.Id;
                profile.GrowthValue = targetGrowth;
                profile.JoinedAt = targetJoinedAt;
                profile.Nickname = targetNickname;
                await memberRepository.UpdateProfileAsync(profile, cancellationToken);
                hasProfileChanged = true;
            }
        }

        if (hasProfileChanged)
        {
            await memberRepository.SaveChangesAsync(cancellationToken);
        }
    }

    internal static async Task<MemberContextSnapshot> LoadMemberContextAsync(
        IMemberRepository memberRepository,
        IOrderRepository orderRepository,
        ITenantProvider tenantProvider,
        IReadOnlyCollection<long> visibleStoreIds,
        CancellationToken cancellationToken)
    {
        await EnsureMemberCenterInitializedAsync(memberRepository, orderRepository, tenantProvider, cancellationToken);

        if (visibleStoreIds.Count == 0)
        {
            return new MemberContextSnapshot();
        }

        var tenantId = tenantProvider.GetCurrentTenantId();
        var tiers = await memberRepository.GetTiersAsync(tenantId, cancellationToken);
        var profiles = await memberRepository.GetProfilesAsync(tenantId, cancellationToken);

        if (tiers.Count == 0 || profiles.Count == 0)
        {
            return new MemberContextSnapshot
            {
                TierLookup = tiers.ToDictionary(item => item.Id),
                Aggregates = []
            };
        }

        var orders = await orderRepository.SearchAllOrdersAsync(
            tenantId,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            cancellationToken);

        var visibleStoreSet = visibleStoreIds.ToHashSet();
        var visibleOrders = orders
            .Where(IsQualifiedOrder)
            .Where(order => visibleStoreSet.Contains(order.StoreId))
            .Where(order => !string.IsNullOrWhiteSpace(NormalizePhone(order.CustomerPhone)))
            .ToList();

        if (visibleOrders.Count == 0)
        {
            return new MemberContextSnapshot
            {
                TierLookup = tiers.ToDictionary(item => item.Id),
                Aggregates = []
            };
        }

        var metricsMap = BuildOrderMetrics(visibleOrders, DateTime.UtcNow);
        var tierLookup = tiers.ToDictionary(item => item.Id);
        var defaultTier = ResolveDefaultTier(tiers);

        var aggregates = new List<MemberAggregate>();
        foreach (var profile in profiles)
        {
            var mobile = NormalizePhone(profile.Mobile);
            if (string.IsNullOrWhiteSpace(mobile) || !metricsMap.TryGetValue(mobile, out var metrics))
            {
                continue;
            }

            var tier = profile.MemberTierId.HasValue && tierLookup.TryGetValue(profile.MemberTierId.Value, out var matchedTier)
                ? matchedTier
                : defaultTier;
            var name = ResolveDisplayName(profile, metrics);
            var avatarText = ResolveAvatarText(name, mobile);
            var avatarColor = ResolveAvatarColor(mobile);
            var joinedAt = profile.JoinedAt == default ? metrics.FirstOrderAt : profile.JoinedAt;

            aggregates.Add(new MemberAggregate
            {
                MemberId = profile.Id,
                Mobile = mobile,
                MobileMasked = MaskPhone(mobile),
                Name = name,
                AvatarText = avatarText,
                AvatarColor = avatarColor,
                JoinedAt = joinedAt,
                Tier = tier,
                Profile = profile,
                Metrics = metrics,
                IsDormant = metrics.LastOrderAt < DateTime.UtcNow.AddDays(-60)
            });
        }

        return new MemberContextSnapshot
        {
            TierLookup = tierLookup,
            Aggregates = aggregates
                .OrderByDescending(item => item.Metrics.LastOrderAt)
                .ThenByDescending(item => item.MemberId)
                .ToList()
        };
    }

    internal static IReadOnlyList<MemberAggregate> ApplyFilters(
        IReadOnlyList<MemberAggregate> source,
        string? keyword,
        long? tierId)
    {
        var normalizedKeyword = (keyword ?? string.Empty).Trim();
        var keywordDigits = NormalizePhone(normalizedKeyword);

        return source
            .Where(item =>
            {
                if (tierId.HasValue)
                {
                    if (!item.Tier?.Id.Equals(tierId.Value) ?? true)
                    {
                        return false;
                    }
                }

                if (string.IsNullOrWhiteSpace(normalizedKeyword))
                {
                    return true;
                }

                var matchedByName = item.Name.Contains(normalizedKeyword, StringComparison.OrdinalIgnoreCase);
                var matchedByMobile = !string.IsNullOrWhiteSpace(keywordDigits) &&
                                      item.Mobile.Contains(keywordDigits, StringComparison.Ordinal);
                return matchedByName || matchedByMobile;
            })
            .ToList();
    }

    internal static string BuildCsv(IReadOnlyList<MemberListItemDto> items)
    {
        var builder = new StringBuilder();
        builder.AppendLine("会员,手机号,等级,累计消费,消费次数,最近消费,储值余额,积分");

        foreach (var item in items)
        {
            builder.Append(EscapeCsv(item.Name));
            builder.Append(',');
            builder.Append(EscapeCsv(item.MobileMasked));
            builder.Append(',');
            builder.Append(EscapeCsv(item.TierName));
            builder.Append(',');
            builder.Append(item.TotalAmount.ToString("0.00", CultureInfo.InvariantCulture));
            builder.Append(',');
            builder.Append(item.OrderCount.ToString(CultureInfo.InvariantCulture));
            builder.Append(',');
            builder.Append(item.LastOrderAt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
            builder.Append(',');
            builder.Append(item.StoredBalance.ToString("0.00", CultureInfo.InvariantCulture));
            builder.Append(',');
            builder.Append(item.PointsBalance.ToString(CultureInfo.InvariantCulture));
            builder.AppendLine();
        }

        return builder.ToString();
    }

    internal static bool IsCouponVisibleToStores(CouponTemplate template, IReadOnlyCollection<long> visibleStoreIds)
    {
        if (visibleStoreIds.Count == 0)
        {
            return false;
        }

        var visibleStoreSet = visibleStoreIds.ToHashSet();

        if (string.IsNullOrWhiteSpace(template.StoreScopeJson))
        {
            return true;
        }

        try
        {
            using var document = JsonDocument.Parse(template.StoreScopeJson);
            if (!document.RootElement.TryGetProperty("mode", out var modeElement))
            {
                return true;
            }

            var mode = modeElement.GetString()?.Trim().ToLowerInvariant() ?? "stores";
            if (mode == "all")
            {
                return true;
            }

            if (!document.RootElement.TryGetProperty("storeIds", out var idsElement) || idsElement.ValueKind != JsonValueKind.Array)
            {
                return false;
            }

            foreach (var item in idsElement.EnumerateArray())
            {
                if (item.TryGetInt64(out var storeId) && visibleStoreSet.Contains(storeId))
                {
                    return true;
                }
            }

            return false;
        }
        catch
        {
            return false;
        }
    }

    internal static bool IsCouponActive(CouponTemplate template, DateTime nowUtc)
    {
        if (template.Status != CouponTemplateStatus.Active)
        {
            return false;
        }

        if (template.ValidFrom.HasValue && template.ValidFrom.Value > nowUtc)
        {
            return false;
        }

        if (template.ValidTo.HasValue && template.ValidTo.Value < nowUtc)
        {
            return false;
        }

        return true;
    }

    internal static string ResolveCouponTypeText(CouponType couponType)
    {
        return couponType switch
        {
            CouponType.AmountOff => "amount_off",
            CouponType.Percentage => "discount",
            CouponType.DeliveryFee => "free_delivery",
            CouponType.Cash => "cash",
            CouponType.Gift => "gift",
            _ => "amount_off"
        };
    }

    internal static string BuildCouponDisplayText(CouponTemplate template)
    {
        return template.CouponType switch
        {
            CouponType.AmountOff => template.MinimumSpend.HasValue && template.MinimumSpend.Value > 0
                ? $"满{template.MinimumSpend.Value:0.##}减{template.Value:0.##}"
                : $"减{template.Value:0.##}",
            CouponType.Percentage => template.MinimumSpend.HasValue && template.MinimumSpend.Value > 0
                ? $"满{template.MinimumSpend.Value:0.##}享{template.Value:0.##}折"
                : $"{template.Value:0.##}折",
            CouponType.DeliveryFee => "免配送费",
            CouponType.Cash => $"现金券 {template.Value:0.##}",
            CouponType.Gift => "礼品券",
            _ => template.Name
        };
    }

    internal static MemberTierDetailDto ToTierDetailDto(MemberTier tier, bool canDelete)
    {
        var benefits = DeserializeBenefits(tier.BenefitsJson);
        return new MemberTierDetailDto
        {
            TierId = tier.Id,
            SortOrder = tier.SortOrder,
            Name = tier.Name,
            IconKey = tier.IconKey,
            ColorHex = tier.ColorHex,
            IsDefault = tier.IsDefault,
            Rule = new MemberTierRuleDto
            {
                UpgradeRuleType = NormalizeRuleType(tier.UpgradeRuleType),
                UpgradeAmountThreshold = tier.UpgradeAmountThreshold,
                UpgradeOrderCountThreshold = tier.UpgradeOrderCountThreshold,
                DowngradeWindowDays = tier.DowngradeWindowDays
            },
            Benefits = benefits,
            CanDelete = canDelete
        };
    }

    internal static MemberDaySettingDto ToMemberDaySettingDto(MemberDaySetting setting)
    {
        return new MemberDaySettingDto
        {
            IsEnabled = setting.IsEnabled,
            Weekday = setting.Weekday,
            ExtraDiscountRate = setting.ExtraDiscountRate
        };
    }

    private static string EscapeCsv(string? value)
    {
        var source = value ?? string.Empty;
        if (!source.Contains(',') && !source.Contains('"') && !source.Contains('\n'))
        {
            return source;
        }

        return $"\"{source.Replace("\"", "\"\"")}\"";
    }

    private static IReadOnlyList<long> NormalizeCouponIds(IReadOnlyList<long>? source)
    {
        return (source ?? [])
            .Where(item => item > 0)
            .Distinct()
            .OrderBy(item => item)
            .ToList();
    }

    private static string FormatAmount(decimal? value)
    {
        var amount = Math.Max(0m, value ?? 0m);
        return $"¥{amount:0.##}";
    }

    private static bool IsQualifiedOrder(Order order)
    {
        if (order.Status == OrderStatus.Cancelled)
        {
            return false;
        }

        if (order.PaymentStatus is PaymentStatus.Unpaid or PaymentStatus.Paying or PaymentStatus.Failed)
        {
            return order.PaidAmount > 0;
        }

        return true;
    }

    private static int ToGrowthValue(decimal totalAmount)
    {
        var value = decimal.Round(totalAmount, 0, MidpointRounding.AwayFromZero);
        if (value > int.MaxValue)
        {
            return int.MaxValue;
        }

        if (value < 0)
        {
            return 0;
        }

        return (int)value;
    }

    private static MemberTier? ResolveDefaultTier(IReadOnlyList<MemberTier> tiers)
    {
        return tiers
            .OrderByDescending(item => item.IsDefault)
            .ThenBy(item => item.SortOrder)
            .ThenBy(item => item.Id)
            .FirstOrDefault();
    }

    private static MemberTier? ResolveTargetTier(
        MemberProfile profile,
        IReadOnlyList<MemberTier> orderedTiers,
        IReadOnlyDictionary<long, MemberTier> tierLookup,
        MemberOrderMetrics metrics,
        DateTime nowUtc)
    {
        if (orderedTiers.Count == 0)
        {
            return null;
        }

        var defaultTier = ResolveDefaultTier(orderedTiers) ?? orderedTiers[0];
        var candidateTier = ResolveTierByMetrics(orderedTiers, metrics.TotalAmount, metrics.TotalOrderCount) ?? defaultTier;

        if (!profile.MemberTierId.HasValue || !tierLookup.TryGetValue(profile.MemberTierId.Value, out var currentTier))
        {
            return candidateTier;
        }

        if (candidateTier.SortOrder > currentTier.SortOrder)
        {
            return candidateTier;
        }

        var downgradeWindowDays = currentTier.DowngradeWindowDays > 0 ? currentTier.DowngradeWindowDays : 90;
        var windowStart = nowUtc.AddDays(-downgradeWindowDays);
        var windowOrders = metrics.Orders
            .Where(item => item.OrderedAt >= windowStart)
            .ToList();

        var windowAmount = windowOrders.Sum(item => item.Amount);
        var windowOrderCount = windowOrders.Count;

        if (MatchTierRule(currentTier, windowAmount, windowOrderCount))
        {
            return currentTier;
        }

        var previousTier = orderedTiers
            .Where(item => item.SortOrder < currentTier.SortOrder)
            .OrderByDescending(item => item.SortOrder)
            .ThenByDescending(item => item.Id)
            .FirstOrDefault();

        if (previousTier is null)
        {
            return defaultTier;
        }

        return previousTier;
    }

    private static MemberTier? ResolveTierByMetrics(
        IReadOnlyList<MemberTier> orderedTiers,
        decimal totalAmount,
        int totalOrderCount)
    {
        MemberTier? matched = null;
        foreach (var tier in orderedTiers)
        {
            if (MatchTierRule(tier, totalAmount, totalOrderCount))
            {
                matched = tier;
            }
        }

        return matched;
    }

    private static bool MatchTierRule(MemberTier tier, decimal amount, int orderCount)
    {
        var ruleType = NormalizeRuleType(tier.UpgradeRuleType);
        var thresholdAmount = Math.Max(0m, tier.UpgradeAmountThreshold ?? 0m);
        var thresholdCount = Math.Max(0, tier.UpgradeOrderCountThreshold ?? 0);

        return ruleType switch
        {
            "amount" => amount >= thresholdAmount,
            "count" => orderCount >= thresholdCount,
            "both" => amount >= thresholdAmount && orderCount >= thresholdCount,
            _ => true
        };
    }

    private static Dictionary<string, MemberOrderMetrics> BuildOrderMetrics(
        IReadOnlyList<Order> orders,
        DateTime nowUtc)
    {
        return orders
            .GroupBy(item => NormalizePhone(item.CustomerPhone), StringComparer.Ordinal)
            .Where(group => !string.IsNullOrWhiteSpace(group.Key))
            .ToDictionary(
                group => group.Key,
                group =>
                {
                    var sorted = group
                        .OrderByDescending(item => item.CreatedAt)
                        .ThenByDescending(item => item.Id)
                        .ToList();

                    var totalAmount = sorted.Sum(ResolveDisplayAmount);
                    var totalOrderCount = sorted.Count;
                    var latestCustomerName = sorted
                        .Select(item => (item.CustomerName ?? string.Empty).Trim())
                        .FirstOrDefault(item => !string.IsNullOrWhiteSpace(item)) ?? string.Empty;

                    var snapshots = sorted
                        .Select(item => new MemberOrderSnapshot
                        {
                            OrderId = item.Id,
                            OrderNo = item.OrderNo,
                            OrderedAt = item.CreatedAt,
                            Amount = ResolveDisplayAmount(item),
                            Status = item.Status
                        })
                        .ToList();

                    return new MemberOrderMetrics
                    {
                        Mobile = group.Key,
                        TotalAmount = totalAmount,
                        TotalOrderCount = totalOrderCount,
                        FirstOrderAt = sorted.Min(item => item.CreatedAt),
                        LastOrderAt = sorted.Max(item => item.CreatedAt),
                        LatestCustomerName = latestCustomerName,
                        Orders = snapshots
                    };
                },
                StringComparer.Ordinal);
    }

    private static IReadOnlyList<MemberTier> BuildDefaultTierSeeds()
    {
        var defaults = new List<MemberTier>
        {
            new()
            {
                Name = "普通会员",
                RequiredGrowth = 0,
                IconKey = "user",
                ColorHex = "#999999",
                UpgradeRuleType = "none",
                UpgradeAmountThreshold = null,
                UpgradeOrderCountThreshold = null,
                DowngradeWindowDays = 90,
                IsDefault = true,
                SortOrder = 1,
                BenefitsJson = SerializeBenefits(new MemberTierBenefitsDto())
            },
            new()
            {
                Name = "银卡会员",
                RequiredGrowth = 500,
                IconKey = "award",
                ColorHex = "#1890ff",
                UpgradeRuleType = "amount",
                UpgradeAmountThreshold = 500m,
                UpgradeOrderCountThreshold = null,
                DowngradeWindowDays = 90,
                IsDefault = false,
                SortOrder = 2,
                BenefitsJson = SerializeBenefits(new MemberTierBenefitsDto
                {
                    Discount = new MemberTierDiscountBenefitDto
                    {
                        Enabled = true,
                        DiscountRate = 9.8m
                    },
                    PointMultiplier = new MemberTierPointMultiplierBenefitDto
                    {
                        Enabled = true,
                        Multiplier = 1.2m
                    },
                    Birthday = new MemberTierBirthdayBenefitDto
                    {
                        Enabled = true,
                        DoublePointsEnabled = false,
                        CouponTemplateIds = []
                    }
                })
            },
            new()
            {
                Name = "金卡会员",
                RequiredGrowth = 2000,
                IconKey = "trophy",
                ColorHex = "#fa8c16",
                UpgradeRuleType = "amount",
                UpgradeAmountThreshold = 2000m,
                UpgradeOrderCountThreshold = null,
                DowngradeWindowDays = 90,
                IsDefault = false,
                SortOrder = 3,
                BenefitsJson = SerializeBenefits(new MemberTierBenefitsDto
                {
                    Discount = new MemberTierDiscountBenefitDto
                    {
                        Enabled = true,
                        DiscountRate = 9.5m
                    },
                    PointMultiplier = new MemberTierPointMultiplierBenefitDto
                    {
                        Enabled = true,
                        Multiplier = 1.5m
                    },
                    Birthday = new MemberTierBirthdayBenefitDto
                    {
                        Enabled = true,
                        DoublePointsEnabled = true,
                        CouponTemplateIds = []
                    },
                    MonthlyCoupon = new MemberTierMonthlyCouponBenefitDto
                    {
                        Enabled = true,
                        GrantDay = 1,
                        CouponTemplateIds = []
                    }
                })
            },
            new()
            {
                Name = "钻石会员",
                RequiredGrowth = 5000,
                IconKey = "gem",
                ColorHex = "#722ed1",
                UpgradeRuleType = "amount",
                UpgradeAmountThreshold = 5000m,
                UpgradeOrderCountThreshold = null,
                DowngradeWindowDays = 90,
                IsDefault = false,
                SortOrder = 4,
                BenefitsJson = SerializeBenefits(new MemberTierBenefitsDto
                {
                    Discount = new MemberTierDiscountBenefitDto
                    {
                        Enabled = true,
                        DiscountRate = 9m
                    },
                    PointMultiplier = new MemberTierPointMultiplierBenefitDto
                    {
                        Enabled = true,
                        Multiplier = 2m
                    },
                    Birthday = new MemberTierBirthdayBenefitDto
                    {
                        Enabled = true,
                        DoublePointsEnabled = true,
                        CouponTemplateIds = []
                    },
                    MonthlyCoupon = new MemberTierMonthlyCouponBenefitDto
                    {
                        Enabled = true,
                        GrantDay = 1,
                        CouponTemplateIds = []
                    },
                    FreeDelivery = new MemberTierFreeDeliveryBenefitDto
                    {
                        Enabled = true,
                        MonthlyFreeTimes = 5
                    },
                    PriorityDeliveryEnabled = true,
                    ExclusiveServiceEnabled = true
                })
            }
        };

        return defaults;
    }
}

internal sealed class MemberContextSnapshot
{
    internal IReadOnlyList<MemberAggregate> Aggregates { get; init; } = [];
    internal IReadOnlyDictionary<long, MemberTier> TierLookup { get; init; } = new Dictionary<long, MemberTier>();
}

internal sealed class MemberAggregate
{
    internal string AvatarColor { get; init; } = string.Empty;
    internal string AvatarText { get; init; } = string.Empty;
    internal long MemberId { get; init; }
    internal MemberOrderMetrics Metrics { get; init; } = new();
    internal string Mobile { get; init; } = string.Empty;
    internal string MobileMasked { get; init; } = string.Empty;
    internal string Name { get; init; } = string.Empty;
    internal MemberProfile Profile { get; init; } = new();
    internal MemberTier? Tier { get; init; }
    internal DateTime JoinedAt { get; init; }
    internal bool IsDormant { get; init; }
}

internal sealed class MemberOrderMetrics
{
    internal DateTime FirstOrderAt { get; init; }
    internal DateTime LastOrderAt { get; init; }
    internal string LatestCustomerName { get; init; } = string.Empty;
    internal string Mobile { get; init; } = string.Empty;
    internal IReadOnlyList<MemberOrderSnapshot> Orders { get; init; } = [];
    internal decimal TotalAmount { get; init; }
    internal int TotalOrderCount { get; init; }
}

internal sealed class MemberOrderSnapshot
{
    internal decimal Amount { get; init; }
    internal long OrderId { get; init; }
    internal string OrderNo { get; init; } = string.Empty;
    internal DateTime OrderedAt { get; init; }
    internal OrderStatus Status { get; init; }
}
