using MediatR;
using System.Data;
using System.Data.Common;
using TakeoutSaaS.Application.App.Billings.Dto;
using TakeoutSaaS.Application.App.Billings.Queries;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Data;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;

namespace TakeoutSaaS.Application.App.Billings.Handlers;

/// <summary>
/// 查询账单统计数据处理器。
/// </summary>
public sealed class GetBillingStatisticsQueryHandler(
    IDapperExecutor dapperExecutor,
    ITenantProvider tenantProvider)
    : IRequestHandler<GetBillingStatisticsQuery, BillingStatisticsDto>
{
    /// <summary>
    /// 处理查询账单统计数据请求。
    /// </summary>
    /// <param name="request">查询命令。</param>
    /// <param name="cancellationToken">取消标记。</param>
    /// <returns>账单统计数据 DTO。</returns>
    public async Task<BillingStatisticsDto> Handle(GetBillingStatisticsQuery request, CancellationToken cancellationToken)
    {
        // 1. 校验租户上下文（租户端禁止跨租户）
        var currentTenantId = tenantProvider.GetCurrentTenantId();
        if (currentTenantId <= 0)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
        }

        // 2. (空行后) 禁止跨租户统计
        if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
        {
            throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户统计账单");
        }
        var tenantId = currentTenantId;

        // 3. (空行后) 参数规范化
        var startDate = request.StartDate ?? DateTime.UtcNow.AddMonths(-1);
        var endDate = request.EndDate ?? DateTime.UtcNow;
        var groupBy = NormalizeGroupBy(request.GroupBy);

        // 2. 查询统计数据（总览 + 趋势）
        return await dapperExecutor.QueryAsync(
            DatabaseConstants.AppDataSource,
            DatabaseConnectionRole.Read,
            async (connection, token) =>
            {
                // 2.1 总览统计
                await using var summaryCommand = CreateCommand(
                    connection,
                    BuildSummarySql(),
                    [
                        ("tenantId", tenantId),
                        ("startDate", startDate),
                        ("endDate", endDate),
                        ("now", DateTime.UtcNow)
                    ]);

                await using var summaryReader = await summaryCommand.ExecuteReaderAsync(token);
                await summaryReader.ReadAsync(token);

                var totalCount = summaryReader.IsDBNull(0) ? 0 : summaryReader.GetInt32(0);
                var pendingCount = summaryReader.IsDBNull(1) ? 0 : summaryReader.GetInt32(1);
                var paidCount = summaryReader.IsDBNull(2) ? 0 : summaryReader.GetInt32(2);
                var overdueCount = summaryReader.IsDBNull(3) ? 0 : summaryReader.GetInt32(3);
                var cancelledCount = summaryReader.IsDBNull(4) ? 0 : summaryReader.GetInt32(4);
                var totalAmountDue = summaryReader.IsDBNull(5) ? 0m : summaryReader.GetDecimal(5);
                var totalAmountPaid = summaryReader.IsDBNull(6) ? 0m : summaryReader.GetDecimal(6);
                var totalAmountUnpaid = summaryReader.IsDBNull(7) ? 0m : summaryReader.GetDecimal(7);
                var totalOverdueAmount = summaryReader.IsDBNull(8) ? 0m : summaryReader.GetDecimal(8);

                // 2.2 趋势数据
                await using var trendCommand = CreateCommand(
                    connection,
                    BuildTrendSql(groupBy),
                    [
                        ("tenantId", tenantId),
                        ("startDate", startDate),
                        ("endDate", endDate)
                    ]);

                await using var trendReader = await trendCommand.ExecuteReaderAsync(token);
                var amountDueTrend = new Dictionary<string, decimal>();
                var amountPaidTrend = new Dictionary<string, decimal>();
                var countTrend = new Dictionary<string, int>();
                while (await trendReader.ReadAsync(token))
                {
                    var bucket = trendReader.GetDateTime(0);
                    var key = bucket.ToString("yyyy-MM-dd");

                    amountDueTrend[key] = trendReader.IsDBNull(1) ? 0m : trendReader.GetDecimal(1);
                    amountPaidTrend[key] = trendReader.IsDBNull(2) ? 0m : trendReader.GetDecimal(2);
                    countTrend[key] = trendReader.IsDBNull(3) ? 0 : trendReader.GetInt32(3);
                }

                // 2.3 组装 DTO
                return new BillingStatisticsDto
                {
                    TenantId = tenantId,
                    StartDate = startDate,
                    EndDate = endDate,
                    GroupBy = groupBy,
                    TotalCount = totalCount,
                    PendingCount = pendingCount,
                    PaidCount = paidCount,
                    OverdueCount = overdueCount,
                    CancelledCount = cancelledCount,
                    TotalAmountDue = totalAmountDue,
                    TotalAmountPaid = totalAmountPaid,
                    TotalAmountUnpaid = totalAmountUnpaid,
                    TotalOverdueAmount = totalOverdueAmount,
                    AmountDueTrend = amountDueTrend,
                    AmountPaidTrend = amountPaidTrend,
                    CountTrend = countTrend
                };
            },
            cancellationToken);
    }

    private static string NormalizeGroupBy(string? groupBy)
    {
        return groupBy?.Trim() switch
        {
            "Week" => "Week",
            "Month" => "Month",
            _ => "Day"
        };
    }

    private static string BuildSummarySql()
    {
        return """
            select
              count(*)::int as "TotalCount",
              coalesce(sum(case when b."Status" = 0 then 1 else 0 end), 0)::int as "PendingCount",
              coalesce(sum(case when b."Status" = 1 then 1 else 0 end), 0)::int as "PaidCount",
              coalesce(sum(case when b."Status" = 2 then 1 else 0 end), 0)::int as "OverdueCount",
              coalesce(sum(case when b."Status" = 3 then 1 else 0 end), 0)::int as "CancelledCount",
              coalesce(sum(b."AmountDue"), 0)::numeric as "TotalAmountDue",
              coalesce(sum(b."AmountPaid"), 0)::numeric as "TotalAmountPaid",
              coalesce(sum((b."AmountDue" - b."DiscountAmount" + b."TaxAmount") - b."AmountPaid"), 0)::numeric as "TotalAmountUnpaid",
              coalesce(sum(
                case
                  when b."Status" in (0, 2) and b."DueDate" < @now
                    then (b."AmountDue" - b."DiscountAmount" + b."TaxAmount") - b."AmountPaid"
                  else 0
                end
              ), 0)::numeric as "TotalOverdueAmount"
            from public.tenant_billing_statements b
            where b."DeletedAt" is null
              and b."TenantId" = @tenantId
              and b."PeriodStart" >= @startDate
              and b."PeriodEnd" <= @endDate;
            """;
    }

    private static string BuildTrendSql(string groupBy)
    {
        var dateTrunc = groupBy switch
        {
            "Week" => "week",
            "Month" => "month",
            _ => "day"
        };

        return $"""
            select
              date_trunc('{dateTrunc}', b."PeriodStart") as "Bucket",
              coalesce(sum(b."AmountDue"), 0)::numeric as "AmountDue",
              coalesce(sum(b."AmountPaid"), 0)::numeric as "AmountPaid",
              count(*)::int as "Count"
            from public.tenant_billing_statements b
            where b."DeletedAt" is null
              and b."TenantId" = @tenantId
              and b."PeriodStart" >= @startDate
              and b."PeriodEnd" <= @endDate
            group by 1
            order by 1 asc;
            """;
    }

    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 p = command.CreateParameter();
            p.ParameterName = name;
            p.Value = value ?? DBNull.Value;
            command.Parameters.Add(p);
        }

        return (DbCommand)command;
    }
}
