using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Domain.Finance.Enums;
using TakeoutSaaS.Domain.Finance.Models;
using TakeoutSaaS.Domain.Finance.Repositories;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Domain.Payments.Enums;
using TakeoutSaaS.Infrastructure.App.Persistence;

namespace TakeoutSaaS.Infrastructure.App.Repositories;

/// <summary>
/// 财务概览 EF Core 仓储实现。
/// </summary>
public sealed class EfFinanceOverviewRepository(TakeoutAppDbContext context) : IFinanceOverviewRepository
{
    private const int TrendDays = 30;
    private const int TopProductDays = 30;

    /// <inheritdoc />
    public async Task<FinanceOverviewDashboardSnapshot> GetDashboardSnapshotAsync(
        long tenantId,
        FinanceCostDimension dimension,
        long? storeId,
        DateTime currentUtc,
        CancellationToken cancellationToken = default)
    {
        var utcNow = NormalizeUtc(currentUtc);
        var todayStart = NormalizeDate(utcNow);
        var tomorrowStart = todayStart.AddDays(1);
        var yesterdayStart = todayStart.AddDays(-1);
        var trendStart = todayStart.AddDays(0 - TrendDays + 1);
        var weekStart = todayStart.AddDays(-7);

        // 1. 查询支付与退款基础数据。
        var paidQuery = BuildPaidRecordQuery(tenantId, dimension, storeId);
        var refundQuery = BuildRefundRecordQuery(tenantId, dimension, storeId);

        // 2. 读取近 30 天按日按门店支付与退款汇总。
        var paidDailyRows = await paidQuery
            .Where(item => item.OccurredAt >= trendStart && item.OccurredAt < tomorrowStart)
            .GroupBy(item => new { Date = item.OccurredAt.Date, item.StoreId })
            .Select(group => new DailyStoreAmountRow
            {
                BusinessDate = group.Key.Date,
                StoreId = group.Key.StoreId,
                Amount = group.Sum(item => item.Amount)
            })
            .ToListAsync(cancellationToken);

        var refundDailyRows = await refundQuery
            .Where(item => item.OccurredAt >= trendStart && item.OccurredAt < tomorrowStart)
            .GroupBy(item => new { Date = item.OccurredAt.Date, item.StoreId })
            .Select(group => new DailyStoreAmountRow
            {
                BusinessDate = group.Key.Date,
                StoreId = group.Key.StoreId,
                Amount = group.Sum(item => item.Amount)
            })
            .ToListAsync(cancellationToken);

        // 3. 读取今日收入构成（渠道维度）。
        var paidTodayByChannel = await paidQuery
            .Where(item => item.OccurredAt >= todayStart && item.OccurredAt < tomorrowStart)
            .GroupBy(item => item.DeliveryType)
            .Select(group => new DailyChannelAmountRow
            {
                Channel = group.Key,
                Amount = group.Sum(item => item.Amount)
            })
            .ToListAsync(cancellationToken);

        var refundTodayByChannel = await refundQuery
            .Where(item => item.OccurredAt >= todayStart && item.OccurredAt < tomorrowStart)
            .GroupBy(item => item.DeliveryType)
            .Select(group => new DailyChannelAmountRow
            {
                Channel = group.Key,
                Amount = group.Sum(item => item.Amount)
            })
            .ToListAsync(cancellationToken);

        // 4. 读取作用域门店平台费率。
        var scopedStoreIds = paidDailyRows
            .Select(item => item.StoreId)
            .Concat(refundDailyRows.Select(item => item.StoreId))
            .Distinct()
            .ToList();
        if (dimension == FinanceCostDimension.Store && storeId.HasValue && !scopedStoreIds.Contains(storeId.Value))
        {
            scopedStoreIds.Add(storeId.Value);
        }

        var platformRateMap = scopedStoreIds.Count == 0
            ? new Dictionary<long, decimal>()
            : await context.StoreFees
                .AsNoTracking()
                .Where(item => item.TenantId == tenantId && scopedStoreIds.Contains(item.StoreId))
                .Select(item => new { item.StoreId, item.PlatformServiceRate })
                .ToDictionaryAsync(item => item.StoreId, item => item.PlatformServiceRate, cancellationToken);

        // 5. 近 30 天按日收入/退款/平台成本汇总。
        var grossByDate = new Dictionary<DateTime, decimal>();
        var refundByDate = new Dictionary<DateTime, decimal>();
        var platformCostByDate = new Dictionary<DateTime, decimal>();

        foreach (var row in paidDailyRows)
        {
            var businessDate = NormalizeDate(row.BusinessDate);
            AddAmount(grossByDate, businessDate, row.Amount);
            var platformRate = ResolvePlatformRate(platformRateMap, row.StoreId);
            AddAmount(platformCostByDate, businessDate, row.Amount * platformRate / 100m);
        }

        foreach (var row in refundDailyRows)
        {
            var businessDate = NormalizeDate(row.BusinessDate);
            AddAmount(refundByDate, businessDate, row.Amount);
            var platformRate = ResolvePlatformRate(platformRateMap, row.StoreId);
            AddAmount(platformCostByDate, businessDate, 0m - row.Amount * platformRate / 100m);
        }

        // 6. 读取近 30 天覆盖月份的月度成本录入，并折算为日成本。
        var monthStarts = BuildMonthStartRange(trendStart, todayStart);
        var monthlyCostRows = await BuildMonthlyCostQuery(tenantId, dimension, storeId, monthStarts)
            .Select(item => new MonthlyCostRow
            {
                CostMonth = item.CostMonth,
                Category = item.Category,
                Amount = item.TotalAmount
            })
            .ToListAsync(cancellationToken);
        var monthlyCostMap = BuildMonthlyCostMap(monthlyCostRows);

        // 7. 组装趋势数据与今日/昨日成本构成。
        var incomeTrend = new List<FinanceOverviewIncomeTrendPointSnapshot>(TrendDays);
        var profitTrend = new List<FinanceOverviewProfitTrendPointSnapshot>(TrendDays);
        var todayCostComposition = CreateEmptyCostComposition();
        var yesterdayCostComposition = CreateEmptyCostComposition();

        for (var currentDate = trendStart; currentDate <= todayStart; currentDate = currentDate.AddDays(1))
        {
            var grossAmount = RoundAmount(GetAmount(grossByDate, currentDate));
            var refundAmount = RoundAmount(GetAmount(refundByDate, currentDate));
            var netReceived = RoundAmount(grossAmount - refundAmount);
            var platformCost = RoundAmount(GetAmount(platformCostByDate, currentDate));

            var baseCost = GetDailyBaseCost(currentDate, monthlyCostMap);
            var totalCost = RoundAmount(baseCost.TotalCost + platformCost);
            var netProfit = RoundAmount(netReceived - totalCost);

            incomeTrend.Add(new FinanceOverviewIncomeTrendPointSnapshot
            {
                BusinessDate = currentDate,
                NetReceivedAmount = netReceived
            });
            profitTrend.Add(new FinanceOverviewProfitTrendPointSnapshot
            {
                BusinessDate = currentDate,
                RevenueAmount = grossAmount,
                CostAmount = totalCost,
                NetProfitAmount = netProfit
            });

            if (currentDate == todayStart)
            {
                todayCostComposition = new DailyCostAmounts
                {
                    Food = baseCost.Food,
                    Labor = baseCost.Labor,
                    Fixed = baseCost.Fixed,
                    Packaging = baseCost.Packaging,
                    Platform = platformCost
                };
            }

            if (currentDate == yesterdayStart)
            {
                yesterdayCostComposition = new DailyCostAmounts
                {
                    Food = baseCost.Food,
                    Labor = baseCost.Labor,
                    Fixed = baseCost.Fixed,
                    Packaging = baseCost.Packaging,
                    Platform = platformCost
                };
            }
        }

        // 8. 汇总核心指标。
        var todayGrossRevenue = RoundAmount(GetAmount(grossByDate, todayStart));
        var yesterdayGrossRevenue = RoundAmount(GetAmount(grossByDate, yesterdayStart));
        var todayRefundAmount = RoundAmount(GetAmount(refundByDate, todayStart));
        var yesterdayRefundAmount = RoundAmount(GetAmount(refundByDate, yesterdayStart));
        var todayNetReceived = RoundAmount(todayGrossRevenue - todayRefundAmount);
        var yesterdayNetReceived = RoundAmount(yesterdayGrossRevenue - yesterdayRefundAmount);
        var todayTotalCost = RoundAmount(todayCostComposition.TotalCost);
        var yesterdayTotalCost = RoundAmount(yesterdayCostComposition.TotalCost);

        var paidTotalAmount = await paidQuery
            .Where(item => item.OccurredAt < tomorrowStart)
            .Select(item => item.Amount)
            .DefaultIfEmpty(0m)
            .SumAsync(cancellationToken);
        var refundTotalAmount = await refundQuery
            .Where(item => item.OccurredAt < tomorrowStart)
            .Select(item => item.Amount)
            .DefaultIfEmpty(0m)
            .SumAsync(cancellationToken);
        var paidBeforeWeekAmount = await paidQuery
            .Where(item => item.OccurredAt < weekStart)
            .Select(item => item.Amount)
            .DefaultIfEmpty(0m)
            .SumAsync(cancellationToken);
        var refundBeforeWeekAmount = await refundQuery
            .Where(item => item.OccurredAt < weekStart)
            .Select(item => item.Amount)
            .DefaultIfEmpty(0m)
            .SumAsync(cancellationToken);

        var withdrawableBalance = RoundAmount(paidTotalAmount - refundTotalAmount);
        var withdrawableBalanceLastWeek = RoundAmount(paidBeforeWeekAmount - refundBeforeWeekAmount);

        // 9. 收入构成映射（今日）。
        var paidChannelMap = paidTodayByChannel.ToDictionary(item => item.Channel, item => item.Amount);
        var refundChannelMap = refundTodayByChannel.ToDictionary(item => item.Channel, item => item.Amount);
        var incomeComposition = new List<FinanceOverviewIncomeCompositionSnapshot>(3);
        foreach (var channel in new[] { DeliveryType.Delivery, DeliveryType.Pickup, DeliveryType.DineIn })
        {
            paidChannelMap.TryGetValue(channel, out var paidAmount);
            refundChannelMap.TryGetValue(channel, out var refundAmount);
            incomeComposition.Add(new FinanceOverviewIncomeCompositionSnapshot
            {
                Channel = channel,
                Amount = RoundAmount(Math.Max(0m, paidAmount - refundAmount))
            });
        }

        // 10. 成本构成映射（今日）。
        var costComposition = new List<FinanceOverviewCostCompositionSnapshot>
        {
            new() { CategoryCode = "food", Amount = RoundAmount(todayCostComposition.Food) },
            new() { CategoryCode = "labor", Amount = RoundAmount(todayCostComposition.Labor) },
            new() { CategoryCode = "fixed", Amount = RoundAmount(todayCostComposition.Fixed) },
            new() { CategoryCode = "packaging", Amount = RoundAmount(todayCostComposition.Packaging) },
            new() { CategoryCode = "platform", Amount = RoundAmount(todayCostComposition.Platform) }
        };

        // 11. 查询 TOP10 商品营收排行（固定近 30 天）。
        var topRangeStart = todayStart.AddDays(0 - TopProductDays + 1);
        var paidOrderIdsQuery = BuildPaidOrderIdsQuery(
            tenantId,
            dimension,
            storeId,
            topRangeStart,
            tomorrowStart);

        var topProductQuery = context.OrderItems
            .AsNoTracking()
            .Where(item => item.TenantId == tenantId && paidOrderIdsQuery.Contains(item.OrderId))
            .GroupBy(item => new { item.ProductId, item.ProductName })
            .Select(group => new FinanceOverviewTopProductSnapshot
            {
                ProductId = group.Key.ProductId,
                ProductName = group.Key.ProductName,
                SalesQuantity = group.Sum(item => item.Quantity),
                RevenueAmount = group.Sum(item => item.SubTotal)
            });

        var topProductTotalRevenue = await topProductQuery
            .Select(item => item.RevenueAmount)
            .DefaultIfEmpty(0m)
            .SumAsync(cancellationToken);
        var topProducts = await topProductQuery
            .OrderByDescending(item => item.RevenueAmount)
            .ThenByDescending(item => item.SalesQuantity)
            .Take(10)
            .ToListAsync(cancellationToken);

        return new FinanceOverviewDashboardSnapshot
        {
            Dimension = dimension,
            StoreId = dimension == FinanceCostDimension.Store ? storeId : null,
            Summary = new FinanceOverviewSummarySnapshot
            {
                TodayGrossRevenue = todayGrossRevenue,
                YesterdayGrossRevenue = yesterdayGrossRevenue,
                TodayNetReceived = todayNetReceived,
                YesterdayNetReceived = yesterdayNetReceived,
                TodayRefundAmount = todayRefundAmount,
                YesterdayRefundAmount = yesterdayRefundAmount,
                TodayTotalCost = todayTotalCost,
                YesterdayTotalCost = yesterdayTotalCost,
                WithdrawableBalance = withdrawableBalance,
                WithdrawableBalanceLastWeek = withdrawableBalanceLastWeek
            },
            IncomeTrend = incomeTrend,
            ProfitTrend = profitTrend,
            IncomeComposition = incomeComposition,
            CostComposition = costComposition,
            TopProducts = topProducts,
            TopProductTotalRevenue = RoundAmount(topProductTotalRevenue)
        };
    }

    private IQueryable<PaidRecordProjection> BuildPaidRecordQuery(
        long tenantId,
        FinanceCostDimension dimension,
        long? storeId)
    {
        var query =
            from payment in context.PaymentRecords.AsNoTracking()
            join order in context.Orders.AsNoTracking()
                on payment.OrderId equals order.Id
            where payment.TenantId == tenantId
                  && order.TenantId == tenantId
                  && payment.Status == PaymentStatus.Paid
                  && payment.PaidAt.HasValue
            select new PaidRecordProjection
            {
                StoreId = order.StoreId,
                DeliveryType = order.DeliveryType,
                Amount = payment.Amount,
                OccurredAt = payment.PaidAt!.Value
            };

        if (dimension == FinanceCostDimension.Store && storeId.HasValue)
        {
            query = query.Where(item => item.StoreId == storeId.Value);
        }

        return query;
    }

    private IQueryable<RefundRecordProjection> BuildRefundRecordQuery(
        long tenantId,
        FinanceCostDimension dimension,
        long? storeId)
    {
        var query =
            from refund in context.PaymentRefundRecords.AsNoTracking()
            join order in context.Orders.AsNoTracking()
                on refund.OrderId equals order.Id
            where refund.TenantId == tenantId
                  && order.TenantId == tenantId
                  && refund.Status == PaymentRefundStatus.Succeeded
            select new RefundRecordProjection
            {
                StoreId = order.StoreId,
                DeliveryType = order.DeliveryType,
                Amount = refund.Amount,
                OccurredAt = refund.CompletedAt ?? refund.RequestedAt
            };

        if (dimension == FinanceCostDimension.Store && storeId.HasValue)
        {
            query = query.Where(item => item.StoreId == storeId.Value);
        }

        return query;
    }

    private IQueryable<long> BuildPaidOrderIdsQuery(
        long tenantId,
        FinanceCostDimension dimension,
        long? storeId,
        DateTime startAt,
        DateTime endAt)
    {
        var query =
            from payment in context.PaymentRecords.AsNoTracking()
            join order in context.Orders.AsNoTracking()
                on payment.OrderId equals order.Id
            where payment.TenantId == tenantId
                  && order.TenantId == tenantId
                  && payment.Status == PaymentStatus.Paid
                  && payment.PaidAt.HasValue
                  && payment.PaidAt.Value >= startAt
                  && payment.PaidAt.Value < endAt
            select new
            {
                order.Id,
                order.StoreId
            };

        if (dimension == FinanceCostDimension.Store && storeId.HasValue)
        {
            query = query.Where(item => item.StoreId == storeId.Value);
        }

        return query.Select(item => item.Id).Distinct();
    }

    private IQueryable<TakeoutSaaS.Domain.Finance.Entities.FinanceCostEntry> BuildMonthlyCostQuery(
        long tenantId,
        FinanceCostDimension dimension,
        long? storeId,
        IReadOnlyCollection<DateTime> monthStarts)
    {
        var query = context.FinanceCostEntries
            .AsNoTracking()
            .Where(item => item.TenantId == tenantId && monthStarts.Contains(item.CostMonth));

        if (dimension == FinanceCostDimension.Store && storeId.HasValue)
        {
            return query.Where(item =>
                item.Dimension == FinanceCostDimension.Store &&
                item.StoreId == storeId.Value);
        }

        return query.Where(item =>
            item.Dimension == FinanceCostDimension.Tenant &&
            item.StoreId == null);
    }

    private static Dictionary<DateTime, Dictionary<FinanceCostCategory, decimal>> BuildMonthlyCostMap(
        IReadOnlyCollection<MonthlyCostRow> rows)
    {
        var result = new Dictionary<DateTime, Dictionary<FinanceCostCategory, decimal>>();
        foreach (var row in rows)
        {
            var monthStart = NormalizeMonthStart(row.CostMonth);
            if (!result.TryGetValue(monthStart, out var categoryMap))
            {
                categoryMap = new Dictionary<FinanceCostCategory, decimal>();
                result[monthStart] = categoryMap;
            }

            if (!categoryMap.ContainsKey(row.Category))
            {
                categoryMap[row.Category] = 0m;
            }
            categoryMap[row.Category] += row.Amount;
        }

        return result;
    }

    private static DailyCostAmounts GetDailyBaseCost(
        DateTime businessDate,
        IReadOnlyDictionary<DateTime, Dictionary<FinanceCostCategory, decimal>> monthlyCostMap)
    {
        var monthStart = NormalizeMonthStart(businessDate);
        var daysInMonth = DateTime.DaysInMonth(monthStart.Year, monthStart.Month);

        monthlyCostMap.TryGetValue(monthStart, out var categoryMap);
        categoryMap ??= new Dictionary<FinanceCostCategory, decimal>();

        var food = ResolveDailyCategoryAmount(categoryMap, FinanceCostCategory.FoodMaterial, daysInMonth);
        var labor = ResolveDailyCategoryAmount(categoryMap, FinanceCostCategory.Labor, daysInMonth);
        var fixedCost = ResolveDailyCategoryAmount(categoryMap, FinanceCostCategory.FixedExpense, daysInMonth);
        var packaging = ResolveDailyCategoryAmount(categoryMap, FinanceCostCategory.PackagingConsumable, daysInMonth);

        return new DailyCostAmounts
        {
            Food = food,
            Labor = labor,
            Fixed = fixedCost,
            Packaging = packaging,
            Platform = 0m
        };
    }

    private static decimal ResolveDailyCategoryAmount(
        IReadOnlyDictionary<FinanceCostCategory, decimal> categoryMap,
        FinanceCostCategory category,
        int daysInMonth)
    {
        categoryMap.TryGetValue(category, out var monthTotal);
        if (daysInMonth <= 0)
        {
            return 0m;
        }

        return RoundAmount(monthTotal / daysInMonth);
    }

    private static List<DateTime> BuildMonthStartRange(DateTime startDate, DateTime endDate)
    {
        var result = new List<DateTime>();
        var monthStart = NormalizeMonthStart(startDate);
        var endMonth = NormalizeMonthStart(endDate);
        while (monthStart <= endMonth)
        {
            result.Add(monthStart);
            monthStart = monthStart.AddMonths(1);
        }

        return result;
    }

    private static DailyCostAmounts CreateEmptyCostComposition()
    {
        return new DailyCostAmounts
        {
            Food = 0m,
            Labor = 0m,
            Fixed = 0m,
            Packaging = 0m,
            Platform = 0m
        };
    }

    private static void AddAmount(IDictionary<DateTime, decimal> map, DateTime key, decimal amount)
    {
        if (!map.ContainsKey(key))
        {
            map[key] = 0m;
        }
        map[key] += amount;
    }

    private static decimal GetAmount(IReadOnlyDictionary<DateTime, decimal> map, DateTime key)
    {
        return map.TryGetValue(key, out var value) ? value : 0m;
    }

    private static decimal ResolvePlatformRate(IReadOnlyDictionary<long, decimal> map, long storeId)
    {
        if (!map.TryGetValue(storeId, out var rate))
        {
            return 0m;
        }

        return Math.Clamp(rate, 0m, 100m);
    }

    private static DateTime NormalizeUtc(DateTime value)
    {
        return value.Kind switch
        {
            DateTimeKind.Utc => value,
            DateTimeKind.Local => value.ToUniversalTime(),
            _ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
        };
    }

    private static DateTime NormalizeDate(DateTime value)
    {
        var utc = NormalizeUtc(value);
        return new DateTime(utc.Year, utc.Month, utc.Day, 0, 0, 0, DateTimeKind.Utc);
    }

    private static DateTime NormalizeMonthStart(DateTime value)
    {
        var utc = NormalizeUtc(value);
        return new DateTime(utc.Year, utc.Month, 1, 0, 0, 0, DateTimeKind.Utc);
    }

    private static decimal RoundAmount(decimal value)
    {
        return decimal.Round(value, 2, MidpointRounding.AwayFromZero);
    }

    private sealed class PaidRecordProjection
    {
        public required long StoreId { get; init; }

        public required DeliveryType DeliveryType { get; init; }

        public required decimal Amount { get; init; }

        public required DateTime OccurredAt { get; init; }
    }

    private sealed class RefundRecordProjection
    {
        public required long StoreId { get; init; }

        public required DeliveryType DeliveryType { get; init; }

        public required decimal Amount { get; init; }

        public required DateTime OccurredAt { get; init; }
    }

    private sealed class DailyStoreAmountRow
    {
        public required DateTime BusinessDate { get; init; }

        public required long StoreId { get; init; }

        public required decimal Amount { get; init; }
    }

    private sealed class DailyChannelAmountRow
    {
        public required DeliveryType Channel { get; init; }

        public required decimal Amount { get; init; }
    }

    private sealed class MonthlyCostRow
    {
        public required DateTime CostMonth { get; init; }

        public required FinanceCostCategory Category { get; init; }

        public required decimal Amount { get; init; }
    }

    private sealed class DailyCostAmounts
    {
        public decimal Food { get; init; }

        public decimal Labor { get; init; }

        public decimal Fixed { get; init; }

        public decimal Packaging { get; init; }

        public decimal Platform { get; init; }

        public decimal TotalCost => RoundAmount(Food + Labor + Fixed + Packaging + Platform);
    }
}
