using System.Data;
using System.Data.Common;
using TakeoutSaaS.Application.App.Customers.Dto;
using TakeoutSaaS.Domain.Orders.Entities;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Domain.Orders.Repositories;
using TakeoutSaaS.Domain.Payments.Enums;
using TakeoutSaaS.Domain.Products.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Data;
using TakeoutSaaS.Shared.Abstractions.Tenancy;

namespace TakeoutSaaS.Application.App.Customers.Handlers;

internal static class CustomerAnalyticsSupport
{
    private static readonly string[] AvatarColors =
    [
        "#f56a00",
        "#7265e6",
        "#52c41a",
        "#fa8c16",
        "#1890ff",
        "#bfbfbf",
        "#13c2c2",
        "#eb2f96"
    ];

    internal const string TagHighValue = "high_value";
    internal const string TagActive = "active";
    internal const string TagDormant = "dormant";
    internal const string TagChurn = "churn";
    internal const string TagNewCustomer = "new_customer";

    internal static readonly string[] SupportedTags =
    [
        TagHighValue,
        TagActive,
        TagDormant,
        TagChurn,
        TagNewCustomer
    ];

    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 ResolveAvatarText(string name, string customerKey)
    {
        var candidate = (name ?? string.Empty).Trim();
        if (!string.IsNullOrWhiteSpace(candidate))
        {
            return candidate[..1];
        }

        return customerKey.Length > 0 ? customerKey[..1] : "客";
    }

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

        return AvatarColors[hash % AvatarColors.Length];
    }

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

    internal static string ToDeliveryTypeText(DeliveryType value)
    {
        return value switch
        {
            DeliveryType.Delivery => "外卖",
            DeliveryType.Pickup => "自提",
            DeliveryType.DineIn => "堂食",
            _ => "未知"
        };
    }

    internal static string ToPaymentMethodText(PaymentMethod value)
    {
        return value switch
        {
            PaymentMethod.WeChatPay => "微信支付",
            PaymentMethod.Alipay => "支付宝",
            PaymentMethod.Balance => "余额支付",
            PaymentMethod.Cash => "现金",
            PaymentMethod.Card => "刷卡",
            _ => "--"
        };
    }

    internal static string ToOrderStatusText(OrderStatus status, DeliveryType deliveryType)
    {
        return status switch
        {
            OrderStatus.PendingPayment => "待接单",
            OrderStatus.AwaitingPreparation => "待接单",
            OrderStatus.InProgress => "制作中",
            OrderStatus.Ready => deliveryType == DeliveryType.Delivery ? "配送中" : "待取餐",
            OrderStatus.Completed => "已完成",
            OrderStatus.Cancelled => "已取消",
            _ => "未知"
        };
    }

    internal static decimal ToRatePercent(int numerator, int denominator)
    {
        if (denominator <= 0 || numerator <= 0)
        {
            return 0;
        }

        return decimal.Round(
            numerator * 100m / denominator,
            1,
            MidpointRounding.AwayFromZero);
    }

    internal static decimal ToGrowthRatePercent(int current, int previous)
    {
        if (previous <= 0)
        {
            return current <= 0 ? 0 : 100;
        }

        return decimal.Round(
            (current - previous) * 100m / previous,
            1,
            MidpointRounding.AwayFromZero);
    }

    internal static string NormalizeTag(string? tag)
    {
        var normalized = (tag ?? string.Empty).Trim().ToLowerInvariant();
        return normalized switch
        {
            "highvalue" or "high_value" or "high-value" => TagHighValue,
            "active" => TagActive,
            "dormant" or "sleeping" => TagDormant,
            "churn" or "lost" => TagChurn,
            "new" or "new_customer" or "new-customer" => TagNewCustomer,
            _ => string.Empty
        };
    }

    internal static string NormalizeOrderCountRange(string? range)
    {
        var normalized = (range ?? string.Empty).Trim().ToLowerInvariant();
        return normalized switch
        {
            "once" or "one" or "1" => "once",
            "two_to_five" or "2_5" or "2-5" => "two_to_five",
            "six_to_ten" or "6_10" or "6-10" => "six_to_ten",
            "ten_plus" or "10+" or "more_than_ten" => "ten_plus",
            _ => string.Empty
        };
    }

    internal static bool MatchOrderCountRange(int orderCount, string normalizedRange)
    {
        return normalizedRange switch
        {
            "once" => orderCount == 1,
            "two_to_five" => orderCount >= 2 && orderCount <= 5,
            "six_to_ten" => orderCount >= 6 && orderCount <= 10,
            "ten_plus" => orderCount > 10,
            _ => true
        };
    }

    internal static IReadOnlyList<CustomerTagDto> BuildTags(
        decimal totalAmount,
        decimal averageAmount,
        int orderCount,
        DateTime registeredAt,
        DateTime lastOrderAt,
        DateTime nowUtc)
    {
        var tagList = new List<CustomerTagDto>();

        // 1. 计算基础状态
        var silentDays = (nowUtc.Date - lastOrderAt.Date).TotalDays;
        var isHighValue = totalAmount >= 3000m || (averageAmount >= 100m && orderCount >= 10);
        var isNewCustomer = registeredAt >= nowUtc.AddDays(-30);
        var isActive = silentDays <= 30;
        var isDormant = silentDays > 30 && silentDays <= 60;
        var isChurn = silentDays > 60;

        // 2. 组合标签（优先保留原型主标签）
        if (isHighValue)
        {
            tagList.Add(new CustomerTagDto
            {
                Code = TagHighValue,
                Label = "高价值",
                Tone = "orange"
            });
        }

        if (isNewCustomer)
        {
            tagList.Add(new CustomerTagDto
            {
                Code = TagNewCustomer,
                Label = "新客户",
                Tone = "green"
            });
            return tagList;
        }

        if (isActive)
        {
            tagList.Add(new CustomerTagDto
            {
                Code = TagActive,
                Label = "活跃",
                Tone = "blue"
            });
            return tagList;
        }

        if (isDormant)
        {
            tagList.Add(new CustomerTagDto
            {
                Code = TagDormant,
                Label = "沉睡",
                Tone = "gray"
            });
            return tagList;
        }

        if (isChurn)
        {
            tagList.Add(new CustomerTagDto
            {
                Code = TagChurn,
                Label = "流失",
                Tone = "red"
            });
        }

        return tagList;
    }

    internal static string ResolveCustomerName(
        string customerKey,
        string latestName,
        MemberProfileSnapshot? memberProfile)
    {
        if (!string.IsNullOrWhiteSpace(memberProfile?.Nickname))
        {
            return memberProfile.Nickname.Trim();
        }

        if (!string.IsNullOrWhiteSpace(latestName))
        {
            return latestName.Trim();
        }

        return customerKey.Length >= 4 ? $"客户{customerKey[^4..]}" : "客户";
    }

    internal static CustomerMemberSummaryDto BuildMemberSummary(MemberProfileSnapshot? memberProfile)
    {
        if (memberProfile is null)
        {
            return new CustomerMemberSummaryDto
            {
                IsMember = false,
                TierName = string.Empty,
                PointsBalance = 0,
                GrowthValue = 0,
                JoinedAt = null
            };
        }

        return new CustomerMemberSummaryDto
        {
            IsMember = true,
            TierName = string.IsNullOrWhiteSpace(memberProfile.TierName)
                ? string.Empty
                : memberProfile.TierName.Trim(),
            PointsBalance = Math.Max(0, memberProfile.PointsBalance),
            GrowthValue = Math.Max(0, memberProfile.GrowthValue),
            JoinedAt = memberProfile.JoinedAt
        };
    }

    internal static IReadOnlyList<CustomerAggregate> ApplyFilters(
        IReadOnlyList<CustomerAggregate> customers,
        string? keyword,
        string? normalizedTag,
        string? normalizedOrderCountRange,
        int? registerPeriodDays,
        DateTime nowUtc)
    {
        var normalizedKeyword = (keyword ?? string.Empty).Trim();
        var keywordDigits = NormalizePhone(normalizedKeyword);

        return customers
            .Where(customer =>
            {
                if (!string.IsNullOrWhiteSpace(normalizedKeyword))
                {
                    var matchedByName = customer.Name.Contains(normalizedKeyword, StringComparison.OrdinalIgnoreCase);
                    var matchedByPhone = !string.IsNullOrWhiteSpace(keywordDigits) &&
                                         customer.CustomerKey.Contains(keywordDigits, StringComparison.Ordinal);
                    if (!matchedByName && !matchedByPhone)
                    {
                        return false;
                    }
                }

                if (!string.IsNullOrWhiteSpace(normalizedTag) &&
                    !customer.Tags.Any(tag => string.Equals(tag.Code, normalizedTag, StringComparison.OrdinalIgnoreCase)))
                {
                    return false;
                }

                if (!string.IsNullOrWhiteSpace(normalizedOrderCountRange) &&
                    !MatchOrderCountRange(customer.OrderCount, normalizedOrderCountRange))
                {
                    return false;
                }

                if (registerPeriodDays.HasValue && registerPeriodDays.Value > 0)
                {
                    var threshold = nowUtc.AddDays(-registerPeriodDays.Value);
                    if (customer.RegisteredAt < threshold)
                    {
                        return false;
                    }
                }

                return true;
            })
            .ToList();
    }

    internal static IReadOnlyList<CustomerTrendPointDto> BuildMonthlyTrend(
        IReadOnlyList<CustomerOrderSnapshot> orders,
        DateTime nowUtc,
        int monthCount)
    {
        var normalizedMonthCount = Math.Clamp(monthCount, 1, 24);
        var monthStart = new DateTime(nowUtc.Year, nowUtc.Month, 1, 0, 0, 0, DateTimeKind.Utc);
        var windowStart = monthStart.AddMonths(-normalizedMonthCount + 1);

        // 1. 预计算窗口内订单金额
        var monthAmountMap = orders
            .Where(item => item.OrderedAt >= windowStart && item.OrderedAt < monthStart.AddMonths(1))
            .GroupBy(item => new DateTime(item.OrderedAt.Year, item.OrderedAt.Month, 1, 0, 0, 0, DateTimeKind.Utc))
            .ToDictionary(group => group.Key, group => group.Sum(item => item.Amount));

        // 2. 生成连续月份点
        var trend = new List<CustomerTrendPointDto>(normalizedMonthCount);
        for (var index = 0; index < normalizedMonthCount; index += 1)
        {
            var currentMonth = windowStart.AddMonths(index);
            monthAmountMap.TryGetValue(currentMonth, out var amount);
            trend.Add(new CustomerTrendPointDto
            {
                Label = $"{currentMonth.Month}月",
                Amount = decimal.Round(amount, 2, MidpointRounding.AwayFromZero)
            });
        }

        return trend;
    }

    internal static decimal CalculateAverageIntervalDays(IReadOnlyList<CustomerOrderSnapshot> orders)
    {
        if (orders.Count < 2)
        {
            return 0;
        }

        var ascOrders = orders
            .OrderBy(item => item.OrderedAt)
            .ThenBy(item => item.OrderId)
            .ToList();

        var totalDays = 0m;
        for (var index = 1; index < ascOrders.Count; index += 1)
        {
            totalDays += (decimal)(ascOrders[index].OrderedAt - ascOrders[index - 1].OrderedAt).TotalDays;
        }

        return decimal.Round(totalDays / (ascOrders.Count - 1), 1, MidpointRounding.AwayFromZero);
    }

    internal static string ResolvePreferredOrderPeaks(IReadOnlyList<CustomerOrderSnapshot> orders)
    {
        if (orders.Count == 0)
        {
            return string.Empty;
        }

        var slots = orders
            .GroupBy(item => ResolvePeakSlot(item.OrderedAt.Hour))
            .Select(group => new
            {
                Slot = group.Key,
                Count = group.Count()
            })
            .OrderByDescending(item => item.Count)
            .ThenBy(item => item.Slot, StringComparer.Ordinal)
            .Take(2)
            .Select(item => item.Slot)
            .ToList();

        return slots.Count == 0 ? string.Empty : string.Join("、", slots);
    }

    internal static string ResolvePreferredDelivery(IReadOnlyList<CustomerOrderSnapshot> orders)
    {
        if (orders.Count == 0)
        {
            return string.Empty;
        }

        var grouped = orders
            .GroupBy(item => item.DeliveryType)
            .Select(group => new
            {
                Type = group.Key,
                Count = group.Count()
            })
            .OrderByDescending(item => item.Count)
            .ThenBy(item => item.Type)
            .ToList();

        var totalCount = grouped.Sum(item => item.Count);
        if (totalCount <= 0)
        {
            return string.Empty;
        }

        return string.Join("、", grouped.Select(item =>
        {
            var ratio = ToRatePercent(item.Count, totalCount);
            return $"{ToDeliveryTypeText(item.Type)} ({ratio:0.#}%)";
        }));
    }

    internal static async Task<string> ResolvePreferredPaymentMethodAsync(
        IOrderRepository orderRepository,
        long tenantId,
        IReadOnlyList<CustomerOrderSnapshot> orders,
        CancellationToken cancellationToken)
    {
        if (orders.Count == 0)
        {
            return string.Empty;
        }

        // 1. 控制计算成本，仅统计最近 60 单
        var recentOrders = orders
            .OrderByDescending(item => item.OrderedAt)
            .ThenByDescending(item => item.OrderId)
            .Take(60)
            .ToList();

        // 2. 统计支付方式
        var counter = new Dictionary<PaymentMethod, int>();
        foreach (var order in recentOrders)
        {
            var payment = await orderRepository.GetLatestPaymentRecordAsync(order.OrderId, tenantId, cancellationToken);
            if (payment is null)
            {
                continue;
            }

            if (!counter.TryAdd(payment.Method, 1))
            {
                counter[payment.Method] += 1;
            }
        }

        if (counter.Count == 0)
        {
            return string.Empty;
        }

        return counter
            .OrderByDescending(item => item.Value)
            .ThenBy(item => item.Key)
            .Select(item => $"{ToPaymentMethodText(item.Key)} ({item.Value}次)")
            .FirstOrDefault() ?? string.Empty;
    }

    internal static IReadOnlyList<CustomerTopProductDto> BuildTopProducts(
        IReadOnlyDictionary<long, IReadOnlyList<OrderItem>> itemsLookup,
        IReadOnlyList<long> orderIds,
        int takeCount)
    {
        if (orderIds.Count == 0 || takeCount <= 0)
        {
            return [];
        }

        // 1. 汇总商品购买次数
        var productCounter = new Dictionary<string, ProductCounter>();
        foreach (var orderId in orderIds)
        {
            if (!itemsLookup.TryGetValue(orderId, out var items))
            {
                continue;
            }

            foreach (var item in items)
            {
                var normalizedName = string.IsNullOrWhiteSpace(item.ProductName) ? "商品" : item.ProductName.Trim();
                var key = item.ProductId > 0 ? $"id:{item.ProductId}" : $"name:{normalizedName}";
                var quantity = Math.Max(1, item.Quantity);

                if (!productCounter.TryGetValue(key, out var counter))
                {
                    counter = new ProductCounter
                    {
                        ProductId = item.ProductId,
                        ProductName = normalizedName,
                        Count = quantity
                    };
                    productCounter[key] = counter;
                    continue;
                }

                counter.Count += quantity;
            }
        }

        var sorted = productCounter.Values
            .OrderByDescending(item => item.Count)
            .ThenBy(item => item.ProductName, StringComparer.Ordinal)
            .Take(takeCount)
            .ToList();

        if (sorted.Count == 0)
        {
            return [];
        }

        var maxCount = Math.Max(1, sorted[0].Count);
        return sorted
            .Select((item, index) => new CustomerTopProductDto
            {
                Rank = index + 1,
                ProductName = item.ProductName,
                Count = item.Count,
                ProportionPercent = decimal.Round(item.Count * 100m / maxCount, 1, MidpointRounding.AwayFromZero)
            })
            .ToList();
    }

    internal static async Task<IReadOnlyList<string>> ResolvePreferredCategoriesAsync(
        IProductRepository productRepository,
        long tenantId,
        IReadOnlyDictionary<long, IReadOnlyList<OrderItem>> itemsLookup,
        IReadOnlyList<long> orderIds,
        CancellationToken cancellationToken)
    {
        if (orderIds.Count == 0)
        {
            return [];
        }

        // 1. 汇总分类出现频次
        var productCache = new Dictionary<long, long>();
        var categoryCounter = new Dictionary<long, int>();

        foreach (var orderId in orderIds)
        {
            if (!itemsLookup.TryGetValue(orderId, out var items))
            {
                continue;
            }

            foreach (var item in items)
            {
                if (item.ProductId <= 0)
                {
                    continue;
                }

                if (!productCache.TryGetValue(item.ProductId, out var categoryId))
                {
                    var product = await productRepository.FindByIdAsync(item.ProductId, tenantId, cancellationToken);
                    categoryId = product?.CategoryId ?? 0;
                    productCache[item.ProductId] = categoryId;
                }

                if (categoryId <= 0)
                {
                    continue;
                }

                var quantity = Math.Max(1, item.Quantity);
                if (!categoryCounter.TryAdd(categoryId, quantity))
                {
                    categoryCounter[categoryId] += quantity;
                }
            }
        }

        if (categoryCounter.Count == 0)
        {
            return [];
        }

        // 2. 读取分类名称并返回前 3
        var categoryIds = categoryCounter
            .OrderByDescending(item => item.Value)
            .ThenBy(item => item.Key)
            .Take(3)
            .Select(item => item.Key)
            .ToList();

        var categoryNames = new List<string>(categoryIds.Count);
        foreach (var categoryId in categoryIds)
        {
            var category = await productRepository.FindCategoryByIdAsync(categoryId, tenantId, cancellationToken);
            if (category is null || string.IsNullOrWhiteSpace(category.Name))
            {
                continue;
            }

            categoryNames.Add(category.Name.Trim());
        }

        return categoryNames;
    }

    internal static IReadOnlyList<CustomerRecentOrderDto> BuildRecentOrders(
        IReadOnlyList<CustomerOrderSnapshot> orders,
        IReadOnlyDictionary<long, IReadOnlyList<OrderItem>> itemsLookup,
        int takeCount)
    {
        return orders
            .OrderByDescending(item => item.OrderedAt)
            .ThenByDescending(item => item.OrderId)
            .Take(Math.Max(1, takeCount))
            .Select(item => new CustomerRecentOrderDto
            {
                OrderNo = item.OrderNo,
                OrderedAt = item.OrderedAt,
                Amount = item.Amount,
                ItemsSummary = BuildItemsSummary(item.OrderId, itemsLookup),
                DeliveryType = ToDeliveryTypeText(item.DeliveryType),
                Status = ToOrderStatusText(item.Status, item.DeliveryType)
            })
            .ToList();
    }

    internal static string BuildItemsSummary(
        long orderId,
        IReadOnlyDictionary<long, IReadOnlyList<OrderItem>> itemsLookup)
    {
        if (!itemsLookup.TryGetValue(orderId, out var items) || items.Count == 0)
        {
            return "--";
        }

        var summaries = items
            .Take(3)
            .Select(item =>
            {
                var productName = string.IsNullOrWhiteSpace(item.ProductName) ? "商品" : item.ProductName.Trim();
                var quantity = Math.Max(1, item.Quantity);
                return $"{productName} x{quantity}";
            })
            .ToList();

        if (items.Count > 3)
        {
            summaries.Add("等");
        }

        return string.Join("、", summaries);
    }

    internal static async Task<IReadOnlyList<CustomerAggregate>> LoadCustomersAsync(
        IOrderRepository orderRepository,
        IDapperExecutor dapperExecutor,
        ITenantProvider tenantProvider,
        IReadOnlyCollection<long> visibleStoreIds,
        CancellationToken cancellationToken)
    {
        if (visibleStoreIds.Count == 0)
        {
            return [];
        }

        var tenantId = tenantProvider.GetCurrentTenantId();
        if (tenantId <= 0)
        {
            return [];
        }

        var visibleStoreSet = visibleStoreIds.ToHashSet();
        var rawOrders = await orderRepository.SearchAllOrdersAsync(
            tenantId,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            cancellationToken);

        // 1. 过滤可见门店并构建订单快照
        var orderSnapshots = rawOrders
            .Where(item => visibleStoreSet.Contains(item.StoreId))
            .Select(item =>
            {
                var customerKey = NormalizePhone(item.CustomerPhone);
                return new CustomerOrderSnapshot
                {
                    OrderId = item.Id,
                    OrderNo = item.OrderNo,
                    StoreId = item.StoreId,
                    CustomerKey = customerKey,
                    CustomerName = string.IsNullOrWhiteSpace(item.CustomerName) ? string.Empty : item.CustomerName.Trim(),
                    OrderedAt = item.CreatedAt,
                    Amount = ResolveDisplayAmount(item),
                    DeliveryType = item.DeliveryType,
                    Status = item.Status
                };
            })
            .Where(item => !string.IsNullOrWhiteSpace(item.CustomerKey))
            .ToList();

        if (orderSnapshots.Count == 0)
        {
            return [];
        }

        var customerKeys = orderSnapshots
            .Select(item => item.CustomerKey)
            .Distinct(StringComparer.Ordinal)
            .ToHashSet(StringComparer.Ordinal);

        var memberLookup = await LoadMemberProfileLookupAsync(
            dapperExecutor,
            tenantId,
            customerKeys,
            cancellationToken);

        var nowUtc = DateTime.UtcNow;
        return orderSnapshots
            .GroupBy(item => item.CustomerKey, StringComparer.Ordinal)
            .Select(group =>
            {
                var customerOrders = group
                    .OrderByDescending(item => item.OrderedAt)
                    .ThenByDescending(item => item.OrderId)
                    .ToList();

                var firstOrderAt = customerOrders.Min(item => item.OrderedAt);
                var lastOrderAt = customerOrders.Max(item => item.OrderedAt);
                var orderCount = customerOrders.Count;
                var totalAmount = customerOrders.Sum(item => item.Amount);
                var averageAmount = orderCount == 0
                    ? 0
                    : decimal.Round(totalAmount / orderCount, 2, MidpointRounding.AwayFromZero);

                memberLookup.TryGetValue(group.Key, out var memberProfile);

                var registeredAt = firstOrderAt;
                if (memberProfile?.JoinedAt is not null && memberProfile.JoinedAt.Value < registeredAt)
                {
                    registeredAt = memberProfile.JoinedAt.Value;
                }

                var latestName = customerOrders
                    .Select(item => item.CustomerName)
                    .FirstOrDefault(name => !string.IsNullOrWhiteSpace(name)) ?? string.Empty;

                var name = ResolveCustomerName(group.Key, latestName, memberProfile);
                var tags = BuildTags(totalAmount, averageAmount, orderCount, registeredAt, lastOrderAt, nowUtc);
                var member = BuildMemberSummary(memberProfile);

                return new CustomerAggregate
                {
                    CustomerKey = group.Key,
                    Name = name,
                    PhoneMasked = MaskPhone(group.Key),
                    AvatarText = ResolveAvatarText(name, group.Key),
                    AvatarColor = ResolveAvatarColor(group.Key),
                    RegisteredAt = registeredAt,
                    FirstOrderAt = firstOrderAt,
                    LastOrderAt = lastOrderAt,
                    Source = member.IsMember ? "会员中心" : "小程序",
                    TotalAmount = totalAmount,
                    AverageAmount = averageAmount,
                    OrderCount = orderCount,
                    Member = member,
                    Tags = tags,
                    IsDimmed = tags.Any(tag => tag.Code is TagDormant or TagChurn),
                    Orders = customerOrders
                };
            })
            .OrderByDescending(item => item.LastOrderAt)
            .ThenBy(item => item.CustomerKey, StringComparer.Ordinal)
            .ToList();
    }

    private static string ResolvePeakSlot(int hour)
    {
        return hour switch
        {
            >= 6 and < 10 => "06:00-10:00",
            >= 10 and < 14 => "10:00-14:00",
            >= 14 and < 17 => "14:00-17:00",
            >= 17 and < 21 => "17:00-21:00",
            _ => "21:00-06:00"
        };
    }

    private static async Task<Dictionary<string, MemberProfileSnapshot>> LoadMemberProfileLookupAsync(
        IDapperExecutor dapperExecutor,
        long tenantId,
        IReadOnlySet<string> customerKeys,
        CancellationToken cancellationToken)
    {
        if (customerKeys.Count == 0)
        {
            return [];
        }

        return await dapperExecutor.QueryAsync(
            DatabaseConstants.AppDataSource,
            DatabaseConnectionRole.Read,
            async (connection, token) =>
            {
                await using var command = CreateCommand(
                    connection,
                    """
                    select
                      profile."Mobile",
                      profile."Nickname",
                      profile."JoinedAt",
                      profile."PointsBalance",
                      profile."GrowthValue",
                      tier."Name" as "TierName"
                    from public.member_profiles profile
                    left join public.member_tiers tier
                      on tier."Id" = profile."MemberTierId"
                     and tier."TenantId" = profile."TenantId"
                     and tier."DeletedAt" is null
                    where profile."DeletedAt" is null
                      and profile."TenantId" = @tenantId;
                    """,
                    [
                        ("tenantId", tenantId)
                    ]);

                await using var reader = await command.ExecuteReaderAsync(token);
                var mobileOrdinal = reader.GetOrdinal("Mobile");
                var nicknameOrdinal = reader.GetOrdinal("Nickname");
                var joinedAtOrdinal = reader.GetOrdinal("JoinedAt");
                var tierNameOrdinal = reader.GetOrdinal("TierName");
                var pointsBalanceOrdinal = reader.GetOrdinal("PointsBalance");
                var growthValueOrdinal = reader.GetOrdinal("GrowthValue");

                var result = new Dictionary<string, MemberProfileSnapshot>(StringComparer.Ordinal);
                while (await reader.ReadAsync(token))
                {
                    var mobile = reader.IsDBNull(mobileOrdinal) ? string.Empty : reader.GetString(mobileOrdinal);
                    var normalizedPhone = NormalizePhone(mobile);
                    if (string.IsNullOrWhiteSpace(normalizedPhone) || !customerKeys.Contains(normalizedPhone))
                    {
                        continue;
                    }

                    var snapshot = new MemberProfileSnapshot
                    {
                        Nickname = reader.IsDBNull(nicknameOrdinal) ? string.Empty : reader.GetString(nicknameOrdinal),
                        JoinedAt = reader.IsDBNull(joinedAtOrdinal) ? null : reader.GetDateTime(joinedAtOrdinal),
                        TierName = reader.IsDBNull(tierNameOrdinal) ? string.Empty : reader.GetString(tierNameOrdinal),
                        PointsBalance = reader.IsDBNull(pointsBalanceOrdinal) ? 0 : reader.GetInt32(pointsBalanceOrdinal),
                        GrowthValue = reader.IsDBNull(growthValueOrdinal) ? 0 : reader.GetInt32(growthValueOrdinal)
                    };

                    result[normalizedPhone] = snapshot;
                }

                return result;
            },
            cancellationToken);
    }

    private static DbCommand CreateCommand(
        IDbConnection connection,
        string sql,
        (string Name, object? Value)[] parameters)
    {
        var command = connection.CreateCommand();
        command.CommandText = sql;

        foreach (var (name, value) in parameters)
        {
            var parameter = command.CreateParameter();
            parameter.ParameterName = name;
            parameter.Value = value ?? DBNull.Value;
            command.Parameters.Add(parameter);
        }

        return (DbCommand)command;
    }

    private sealed class ProductCounter
    {
        internal int Count { get; set; }
        internal long ProductId { get; init; }
        internal string ProductName { get; init; } = string.Empty;
    }
}

internal sealed class CustomerAggregate
{
    internal string AvatarColor { get; init; } = string.Empty;
    internal string AvatarText { get; init; } = string.Empty;
    internal decimal AverageAmount { get; init; }
    internal string CustomerKey { get; init; } = string.Empty;
    internal DateTime FirstOrderAt { get; init; }
    internal bool IsDimmed { get; init; }
    internal DateTime LastOrderAt { get; init; }
    internal CustomerMemberSummaryDto Member { get; init; } = new();
    internal string Name { get; init; } = string.Empty;
    internal int OrderCount { get; init; }
    internal IReadOnlyList<CustomerOrderSnapshot> Orders { get; init; } = [];
    internal string PhoneMasked { get; init; } = string.Empty;
    internal DateTime RegisteredAt { get; init; }
    internal string Source { get; init; } = string.Empty;
    internal IReadOnlyList<CustomerTagDto> Tags { get; init; } = [];
    internal decimal TotalAmount { get; init; }
}

internal sealed class CustomerOrderSnapshot
{
    internal decimal Amount { get; init; }
    internal string CustomerKey { get; init; } = string.Empty;
    internal string CustomerName { get; init; } = string.Empty;
    internal DeliveryType DeliveryType { get; init; }
    internal long OrderId { get; init; }
    internal string OrderNo { get; init; } = string.Empty;
    internal DateTime OrderedAt { get; init; }
    internal OrderStatus Status { get; init; }
    internal long StoreId { get; init; }
}

internal sealed class MemberProfileSnapshot
{
    internal int GrowthValue { get; init; }
    internal DateTime? JoinedAt { get; init; }
    internal string Nickname { get; init; } = string.Empty;
    internal int PointsBalance { get; init; }
    internal string TierName { get; init; } = string.Empty;
}
