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.Results;
using TakeoutSaaS.Shared.Abstractions.Tenancy;

namespace TakeoutSaaS.Application.App.Billings.Handlers;

/// <summary>
/// 分页查询账单列表处理器。
/// </summary>
public sealed class GetBillingListQueryHandler(
    IDapperExecutor dapperExecutor,
    ITenantProvider tenantProvider)
    : IRequestHandler<GetBillingListQuery, PagedResult<BillingListDto>>
{
    /// <summary>
    /// 处理分页查询账单列表请求。
    /// </summary>
    /// <param name="request">查询命令。</param>
    /// <param name="cancellationToken">取消标记。</param>
    /// <returns>分页账单列表 DTO。</returns>
    public async Task<PagedResult<BillingListDto>> Handle(GetBillingListQuery 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 page = request.PageNumber <= 0 ? 1 : request.PageNumber;
        var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize;
        var keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim();
        var minAmount = request.MinAmount;
        var maxAmount = request.MaxAmount;
        var offset = (page - 1) * pageSize;

        // 1.1 金额区间规范化（避免 min > max 导致结果为空）
        if (minAmount.HasValue && maxAmount.HasValue && minAmount.Value > maxAmount.Value)
        {
            (minAmount, maxAmount) = (maxAmount, minAmount);
        }

        // 2. 排序白名单（防 SQL 注入）
        var orderBy = request.SortBy?.Trim() switch
        {
            "DueDate" => "b.\"DueDate\"",
            "AmountDue" => "b.\"AmountDue\"",
            "PeriodStart" => "b.\"PeriodStart\"",
            "PeriodEnd" => "b.\"PeriodEnd\"",
            "CreatedAt" => "b.\"CreatedAt\"",
            _ => "b.\"CreatedAt\""
        };

        // 3. 查询总数 + 列表
        return await dapperExecutor.QueryAsync(
            DatabaseConstants.AppDataSource,
            DatabaseConnectionRole.Read,
            async (connection, token) =>
            {
                // 3.1 统计总数
                var total = await ExecuteScalarIntAsync(
                    connection,
                    BuildCountSql(),
                    [
                        ("tenantId", tenantId),
                        ("status", request.Status.HasValue ? (int)request.Status.Value : null),
                        ("billingType", request.BillingType.HasValue ? (int)request.BillingType.Value : null),
                        ("startDate", request.StartDate),
                        ("endDate", request.EndDate),
                        ("minAmount", minAmount),
                        ("maxAmount", maxAmount),
                        ("keyword", keyword)
                    ],
                    token);

                // 3.2 查询列表
                var listSql = BuildListSql(orderBy, request.SortDesc);
                await using var listCommand = CreateCommand(
                    connection,
                    listSql,
                    [
                        ("tenantId", tenantId),
                        ("status", request.Status.HasValue ? (int)request.Status.Value : null),
                        ("billingType", request.BillingType.HasValue ? (int)request.BillingType.Value : null),
                        ("startDate", request.StartDate),
                        ("endDate", request.EndDate),
                        ("minAmount", minAmount),
                        ("maxAmount", maxAmount),
                        ("keyword", keyword),
                        ("offset", offset),
                        ("limit", pageSize)
                    ]);

                await using var reader = await listCommand.ExecuteReaderAsync(token);
                var now = DateTime.UtcNow;
                var items = new List<BillingListDto>();
                while (await reader.ReadAsync(token))
                {
                    var dueDate = reader.GetDateTime(13);
                    var status = (TenantBillingStatus)reader.GetInt32(12);
                    var amountDue = reader.GetDecimal(8);
                    var discountAmount = reader.GetDecimal(9);
                    var taxAmount = reader.GetDecimal(10);
                    var totalAmount = amountDue - discountAmount + taxAmount;

                    // 3.2.1 逾期辅助字段
                    var isOverdue = status is TenantBillingStatus.Overdue
                                    || (status is TenantBillingStatus.Pending && dueDate < now);
                    var overdueDays = dueDate < now ? (int)(now - dueDate).TotalDays : 0;

                    items.Add(new BillingListDto
                    {
                        Id = reader.GetInt64(0),
                        TenantId = reader.GetInt64(1),
                        TenantName = reader.IsDBNull(2) ? string.Empty : reader.GetString(2),
                        SubscriptionId = reader.IsDBNull(3) ? null : reader.GetInt64(3),
                        StatementNo = reader.GetString(4),
                        BillingType = (BillingType)reader.GetInt32(5),
                        PeriodStart = reader.GetDateTime(6),
                        PeriodEnd = reader.GetDateTime(7),
                        AmountDue = amountDue,
                        DiscountAmount = discountAmount,
                        TaxAmount = taxAmount,
                        TotalAmount = totalAmount,
                        AmountPaid = reader.GetDecimal(11),
                        Currency = reader.IsDBNull(14) ? "CNY" : reader.GetString(14),
                        Status = status,
                        DueDate = dueDate,
                        IsOverdue = isOverdue,
                        OverdueDays = overdueDays,
                        CreatedAt = reader.GetDateTime(15),
                        UpdatedAt = reader.IsDBNull(16) ? null : reader.GetDateTime(16)
                    });
                }

                // 3.3 返回分页
                return new PagedResult<BillingListDto>(items, page, pageSize, total);
            },
            cancellationToken);
    }

    private static string BuildCountSql()
    {
        return """
            select count(*)
            from public.tenant_billing_statements b
            join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null
            where b."DeletedAt" is null
              and b."TenantId" = @tenantId
              and (@status::int is null or b."Status" = @status)
              and (@billingType::int is null or b."BillingType" = @billingType)
              and (@startDate::timestamp with time zone is null or b."PeriodStart" >= @startDate)
              and (@endDate::timestamp with time zone is null or b."PeriodEnd" <= @endDate)
              and (@minAmount::numeric is null or b."AmountDue" >= @minAmount)
              and (@maxAmount::numeric is null or b."AmountDue" <= @maxAmount)
              and (
                @keyword::text is null
                or b."StatementNo" ilike ('%' || @keyword::text || '%')
                or t."Name" ilike ('%' || @keyword::text || '%')
              );
            """;
    }

    private static string BuildListSql(string orderBy, bool sortDesc)
    {
        var direction = sortDesc ? "desc" : "asc";

        return $"""
            select
              b."Id",
              b."TenantId",
              t."Name" as "TenantName",
              b."SubscriptionId",
              b."StatementNo",
              b."BillingType",
              b."PeriodStart",
              b."PeriodEnd",
              b."AmountDue",
              b."DiscountAmount",
              b."TaxAmount",
              b."AmountPaid",
              b."Status",
              b."DueDate",
              b."Currency",
              b."CreatedAt",
              b."UpdatedAt"
            from public.tenant_billing_statements b
            join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null
            where b."DeletedAt" is null
              and b."TenantId" = @tenantId
              and (@status::int is null or b."Status" = @status)
              and (@billingType::int is null or b."BillingType" = @billingType)
              and (@startDate::timestamp with time zone is null or b."PeriodStart" >= @startDate)
              and (@endDate::timestamp with time zone is null or b."PeriodEnd" <= @endDate)
              and (@minAmount::numeric is null or b."AmountDue" >= @minAmount)
              and (@maxAmount::numeric is null or b."AmountDue" <= @maxAmount)
              and (
                @keyword::text is null
                or b."StatementNo" ilike ('%' || @keyword::text || '%')
                or t."Name" ilike ('%' || @keyword::text || '%')
              )
            order by {orderBy} {direction}
            offset @offset
            limit @limit;
            """;
    }

    private static async Task<int> ExecuteScalarIntAsync(
        IDbConnection connection,
        string sql,
        (string Name, object? Value)[] parameters,
        CancellationToken cancellationToken)
    {
        await using var command = CreateCommand(connection, sql, parameters);
        var result = await command.ExecuteScalarAsync(cancellationToken);
        return result is null or DBNull ? 0 : Convert.ToInt32(result);
    }

    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;
    }
}
