using TakeoutSaaS.Application.App.Customers.Dto;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;

namespace TakeoutSaaS.Application.App.Customers.Handlers;

internal static class CustomerAnalysisSegmentSupport
{
    internal const string SegmentAll = "all";
    internal const string SegmentRepeatLoyal = "repeat_loyal";
    internal const string SegmentActiveNew = "active_new";
    internal const string SegmentActiveRecent = "active_recent";
    internal const string SegmentDormant = "dormant";
    internal const string SegmentChurn = "churn";
    internal const string SegmentHighValueTop = "high_value_top";

    private static readonly string[] CompositionSegmentOrder =
    [
        SegmentRepeatLoyal,
        SegmentActiveNew,
        SegmentDormant,
        SegmentChurn
    ];

    private static readonly (string SegmentCode, string Label, string Tone)[] CompositionDefinitions =
    [
        (SegmentRepeatLoyal, "老客户（复购2次+）", "blue"),
        (SegmentActiveNew, "活跃新客", "green"),
        (SegmentDormant, "沉睡客户", "orange"),
        (SegmentChurn, "流失客户", "gray")
    ];

    private static readonly AmountDistributionDefinition[] AmountDistributionDefinitions =
    [
        new("amount_0_30", "0 - 30元", 0m, 30m),
        new("amount_30_60", "30 - 60元", 30m, 60m),
        new("amount_60_100", "60 - 100元", 60m, 100m),
        new("amount_100_150", "100 - 150元", 100m, 150m),
        new("amount_150_plus", "150元以上", 150m, null)
    ];

    private static readonly string[] RfmRowLabels =
    [
        "近期活跃",
        "中期沉默",
        "长期流失"
    ];

    private static readonly string[] RfmColumnLabels =
    [
        "高频高额",
        "高频低额",
        "低频高额",
        "低频低额"
    ];

    private static readonly string[,] RfmCellLabels =
    {
        { "重要价值", "潜力客户", "新客培育", "一般维护" },
        { "重要挽留", "一般发展", "一般保持", "低优先级" },
        { "重要召回", "即将流失", "基本流失", "已流失" }
    };

    private static readonly string[,] RfmCellTones =
    {
        { "hot", "warm", "warm", "cool" },
        { "warm", "cool", "cool", "cold" },
        { "cool", "cold", "cold", "cold" }
    };

    internal static string NormalizeSegmentCode(string? segmentCode)
    {
        var normalized = (segmentCode ?? string.Empty).Trim().ToLowerInvariant();
        return string.IsNullOrWhiteSpace(normalized) ? SegmentAll : normalized;
    }

    internal static SegmentMeta ResolveSegmentMeta(string normalizedSegmentCode)
    {
        var amountDefinition = ResolveAmountDistributionDefinition(normalizedSegmentCode);
        if (amountDefinition is not null)
        {
            return new SegmentMeta(
                normalizedSegmentCode,
                $"客单价分布 · {amountDefinition.Label}",
                $"筛选客单价位于 {amountDefinition.Label} 区间的客户");
        }

        if (TryParseRfmSegmentCode(normalizedSegmentCode, out var rowIndex, out var columnIndex))
        {
            return new SegmentMeta(
                normalizedSegmentCode,
                $"RFM分层 · {RfmRowLabels[rowIndex]} / {RfmColumnLabels[columnIndex]}",
                $"当前客群标签：{RfmCellLabels[rowIndex, columnIndex]}");
        }

        return normalizedSegmentCode switch
        {
            SegmentAll => new SegmentMeta(SegmentAll, "全部客户", "当前门店下全部客户明细"),
            SegmentRepeatLoyal => new SegmentMeta(SegmentRepeatLoyal, "老客户（复购2次+）", "非流失/沉睡且非新客的稳定复购客户"),
            SegmentActiveNew => new SegmentMeta(SegmentActiveNew, "活跃新客", "统计周期内注册且有消费的客户"),
            SegmentActiveRecent => new SegmentMeta(SegmentActiveRecent, "周期活跃客户", "统计周期内发生过消费行为的客户"),
            SegmentDormant => new SegmentMeta(SegmentDormant, "沉睡客户", "31-60天未消费的客户"),
            SegmentChurn => new SegmentMeta(SegmentChurn, "流失客户", "超过60天未消费的客户"),
            SegmentHighValueTop => new SegmentMeta(SegmentHighValueTop, "高价值客户", "累计消费或消费能力达到高价值阈值的客户"),
            _ => throw new BusinessException(ErrorCodes.BadRequest, "segmentCode 参数不合法")
        };
    }

    internal static IReadOnlyList<CustomerAnalysisCompositionItemDto> BuildComposition(
        IReadOnlyList<CustomerAggregate> customers,
        DateTime nowUtc,
        int periodDays)
    {
        if (customers.Count == 0)
        {
            return CompositionDefinitions
                .Select(item => new CustomerAnalysisCompositionItemDto
                {
                    SegmentCode = item.SegmentCode,
                    Label = item.Label,
                    Count = 0,
                    Percent = 0,
                    Tone = item.Tone
                })
                .ToList();
        }

        var counter = CompositionSegmentOrder.ToDictionary(item => item, _ => 0, StringComparer.Ordinal);
        foreach (var customer in customers)
        {
            var segmentCode = ResolveCompositionSegmentCode(customer, nowUtc, periodDays);
            counter[segmentCode] += 1;
        }

        return CompositionDefinitions
            .Select(item => new CustomerAnalysisCompositionItemDto
            {
                SegmentCode = item.SegmentCode,
                Label = item.Label,
                Count = counter[item.SegmentCode],
                Percent = CustomerAnalyticsSupport.ToRatePercent(counter[item.SegmentCode], customers.Count),
                Tone = item.Tone
            })
            .ToList();
    }

    internal static IReadOnlyList<CustomerAnalysisAmountDistributionItemDto> BuildAmountDistribution(
        IReadOnlyList<CustomerAggregate> customers)
    {
        if (customers.Count == 0)
        {
            return AmountDistributionDefinitions
                .Select(item => new CustomerAnalysisAmountDistributionItemDto
                {
                    SegmentCode = item.SegmentCode,
                    Label = item.Label,
                    Count = 0,
                    Percent = 0
                })
                .ToList();
        }

        return AmountDistributionDefinitions
            .Select(item =>
            {
                var count = customers.Count(customer => MatchAmountDistribution(customer.AverageAmount, item));
                return new CustomerAnalysisAmountDistributionItemDto
                {
                    SegmentCode = item.SegmentCode,
                    Label = item.Label,
                    Count = count,
                    Percent = CustomerAnalyticsSupport.ToRatePercent(count, customers.Count)
                };
            })
            .ToList();
    }

    internal static IReadOnlyList<CustomerAnalysisRfmRowDto> BuildRfmRows(
        IReadOnlyList<CustomerAggregate> customers,
        DateTime nowUtc)
    {
        var counters = new int[3, 4];
        foreach (var customer in customers)
        {
            var rowIndex = ResolveRfmRecencyRow(customer, nowUtc);
            var columnIndex = ResolveRfmFrequencyAmountColumn(customer);
            counters[rowIndex, columnIndex] += 1;
        }

        var rows = new List<CustomerAnalysisRfmRowDto>(3);
        for (var rowIndex = 0; rowIndex < 3; rowIndex += 1)
        {
            var cells = new List<CustomerAnalysisRfmCellDto>(4);
            for (var columnIndex = 0; columnIndex < 4; columnIndex += 1)
            {
                cells.Add(new CustomerAnalysisRfmCellDto
                {
                    SegmentCode = BuildRfmSegmentCode(rowIndex, columnIndex),
                    Label = RfmCellLabels[rowIndex, columnIndex],
                    Count = counters[rowIndex, columnIndex],
                    Tone = RfmCellTones[rowIndex, columnIndex]
                });
            }

            rows.Add(new CustomerAnalysisRfmRowDto
            {
                Label = RfmRowLabels[rowIndex],
                Cells = cells
            });
        }

        return rows;
    }

    internal static IReadOnlyList<CustomerAnalysisTrendPointDto> BuildGrowthTrend(
        IReadOnlyList<CustomerAggregate> customers,
        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);

        var countLookup = customers
            .Where(item => item.RegisteredAt >= windowStart && item.RegisteredAt < monthStart.AddMonths(1))
            .GroupBy(item => new DateTime(item.RegisteredAt.Year, item.RegisteredAt.Month, 1, 0, 0, 0, DateTimeKind.Utc))
            .ToDictionary(group => group.Key, group => group.Count());

        var trend = new List<CustomerAnalysisTrendPointDto>(normalizedMonthCount);
        for (var index = 0; index < normalizedMonthCount; index += 1)
        {
            var currentMonth = windowStart.AddMonths(index);
            countLookup.TryGetValue(currentMonth, out var value);

            trend.Add(new CustomerAnalysisTrendPointDto
            {
                Label = $"{currentMonth.Month}月",
                Value = value
            });
        }

        return trend;
    }

    internal static IReadOnlyList<CustomerAnalysisTopCustomerDto> BuildTopCustomers(
        IReadOnlyList<CustomerAggregate> customers,
        int takeCount)
    {
        var normalizedTakeCount = Math.Clamp(takeCount, 1, 200);

        return customers
            .OrderByDescending(item => item.TotalAmount)
            .ThenByDescending(item => item.OrderCount)
            .ThenByDescending(item => item.LastOrderAt)
            .ThenBy(item => item.CustomerKey, StringComparer.Ordinal)
            .Take(normalizedTakeCount)
            .Select((item, index) => new CustomerAnalysisTopCustomerDto
            {
                Rank = index + 1,
                CustomerKey = item.CustomerKey,
                Name = item.Name,
                PhoneMasked = item.PhoneMasked,
                TotalAmount = item.TotalAmount,
                OrderCount = item.OrderCount,
                AverageAmount = item.AverageAmount,
                LastOrderAt = item.LastOrderAt,
                Tags = item.Tags
            })
            .ToList();
    }

    internal static IReadOnlyList<CustomerAggregate> FilterBySegment(
        IReadOnlyList<CustomerAggregate> customers,
        string normalizedSegmentCode,
        DateTime nowUtc,
        int periodDays)
    {
        if (normalizedSegmentCode == SegmentAll)
        {
            return customers;
        }

        if (TryParseRfmSegmentCode(normalizedSegmentCode, out var rowIndex, out var columnIndex))
        {
            return customers
                .Where(customer =>
                    ResolveRfmRecencyRow(customer, nowUtc) == rowIndex &&
                    ResolveRfmFrequencyAmountColumn(customer) == columnIndex)
                .ToList();
        }

        var amountDefinition = ResolveAmountDistributionDefinition(normalizedSegmentCode);
        if (amountDefinition is not null)
        {
            return customers
                .Where(customer => MatchAmountDistribution(customer.AverageAmount, amountDefinition))
                .ToList();
        }

        return normalizedSegmentCode switch
        {
            SegmentRepeatLoyal => customers
                .Where(customer => string.Equals(
                    ResolveCompositionSegmentCode(customer, nowUtc, periodDays),
                    SegmentRepeatLoyal,
                    StringComparison.Ordinal))
                .ToList(),
            SegmentActiveNew => customers
                .Where(customer => string.Equals(
                    ResolveCompositionSegmentCode(customer, nowUtc, periodDays),
                    SegmentActiveNew,
                    StringComparison.Ordinal))
                .ToList(),
            SegmentActiveRecent => customers
                .Where(customer => customer.LastOrderAt >= nowUtc.AddDays(-Math.Clamp(periodDays, 7, 365)))
                .ToList(),
            SegmentDormant => customers
                .Where(customer => string.Equals(
                    ResolveCompositionSegmentCode(customer, nowUtc, periodDays),
                    SegmentDormant,
                    StringComparison.Ordinal))
                .ToList(),
            SegmentChurn => customers
                .Where(customer => string.Equals(
                    ResolveCompositionSegmentCode(customer, nowUtc, periodDays),
                    SegmentChurn,
                    StringComparison.Ordinal))
                .ToList(),
            SegmentHighValueTop => customers
                .Where(customer =>
                    customer.Tags.Any(tag => string.Equals(tag.Code, CustomerAnalyticsSupport.TagHighValue, StringComparison.Ordinal)))
                .ToList(),
            _ => throw new BusinessException(ErrorCodes.BadRequest, "segmentCode 参数不合法")
        };
    }

    internal static IReadOnlyList<CustomerAggregate> ApplyKeyword(
        IReadOnlyList<CustomerAggregate> customers,
        string? keyword)
    {
        var normalizedKeyword = (keyword ?? string.Empty).Trim();
        if (string.IsNullOrWhiteSpace(normalizedKeyword))
        {
            return customers;
        }

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

    internal static bool TryParseRfmSegmentCode(
        string normalizedSegmentCode,
        out int rowIndex,
        out int columnIndex)
    {
        rowIndex = -1;
        columnIndex = -1;

        if (!normalizedSegmentCode.StartsWith("rfm_r", StringComparison.Ordinal) ||
            normalizedSegmentCode.Length != 8)
        {
            return false;
        }

        var rowChar = normalizedSegmentCode[5];
        var separator = normalizedSegmentCode[6];
        var columnChar = normalizedSegmentCode[7];

        if (separator != 'c' || !char.IsDigit(rowChar) || !char.IsDigit(columnChar))
        {
            return false;
        }

        rowIndex = rowChar - '1';
        columnIndex = columnChar - '1';

        return rowIndex is >= 0 and < 3 && columnIndex is >= 0 and < 4;
    }

    internal static string BuildRfmSegmentCode(int rowIndex, int columnIndex)
    {
        return $"rfm_r{rowIndex + 1}c{columnIndex + 1}";
    }

    private static string ResolveCompositionSegmentCode(
        CustomerAggregate customer,
        DateTime nowUtc,
        int periodDays)
    {
        var silentDays = (nowUtc.Date - customer.LastOrderAt.Date).TotalDays;
        if (silentDays > 60)
        {
            return SegmentChurn;
        }

        if (silentDays > 30)
        {
            return SegmentDormant;
        }

        if (customer.RegisteredAt >= nowUtc.AddDays(-Math.Clamp(periodDays, 7, 365)))
        {
            return SegmentActiveNew;
        }

        return SegmentRepeatLoyal;
    }

    private static AmountDistributionDefinition? ResolveAmountDistributionDefinition(string normalizedSegmentCode)
    {
        return AmountDistributionDefinitions.FirstOrDefault(item =>
            string.Equals(item.SegmentCode, normalizedSegmentCode, StringComparison.Ordinal));
    }

    private static bool MatchAmountDistribution(decimal amount, AmountDistributionDefinition definition)
    {
        if (amount < definition.MinInclusive)
        {
            return false;
        }

        if (definition.MaxExclusive.HasValue && amount >= definition.MaxExclusive.Value)
        {
            return false;
        }

        return true;
    }

    private static int ResolveRfmRecencyRow(CustomerAggregate customer, DateTime nowUtc)
    {
        var silentDays = (nowUtc.Date - customer.LastOrderAt.Date).TotalDays;
        if (silentDays <= 30)
        {
            return 0;
        }

        if (silentDays <= 60)
        {
            return 1;
        }

        return 2;
    }

    private static int ResolveRfmFrequencyAmountColumn(CustomerAggregate customer)
    {
        var highFrequency = customer.OrderCount >= 10;
        var highAmount = customer.AverageAmount >= 100m;

        return (highFrequency, highAmount) switch
        {
            (true, true) => 0,
            (true, false) => 1,
            (false, true) => 2,
            _ => 3
        };
    }

    private sealed record AmountDistributionDefinition(
        string SegmentCode,
        string Label,
        decimal MinInclusive,
        decimal? MaxExclusive);
}

internal sealed record SegmentMeta(
    string SegmentCode,
    string SegmentTitle,
    string SegmentDescription);
