using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Domain.Membership.Entities;
using TakeoutSaaS.Domain.Membership.Enums;
using TakeoutSaaS.Domain.Membership.Repositories;
using TakeoutSaaS.Infrastructure.App.Persistence;

namespace TakeoutSaaS.Infrastructure.App.Repositories;

/// <summary>
/// 会员积分商城 EF Core 仓储实现。
/// </summary>
public sealed class EfPointMallRepository(TakeoutAppDbContext context) : IPointMallRepository
{
    /// <inheritdoc />
    public Task<MemberPointMallRule?> GetRuleByStoreAsync(
        long tenantId,
        long storeId,
        CancellationToken cancellationToken = default)
    {
        return context.MemberPointMallRules
            .AsNoTracking()
            .Where(item => item.TenantId == tenantId && item.StoreId == storeId)
            .FirstOrDefaultAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task AddRuleAsync(MemberPointMallRule entity, CancellationToken cancellationToken = default)
    {
        return context.MemberPointMallRules.AddAsync(entity, cancellationToken).AsTask();
    }

    /// <inheritdoc />
    public Task UpdateRuleAsync(MemberPointMallRule entity, CancellationToken cancellationToken = default)
    {
        context.MemberPointMallRules.Update(entity);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<MemberPointMallProduct>> SearchProductsAsync(
        long tenantId,
        long storeId,
        MemberPointMallProductStatus? status,
        string? keyword,
        CancellationToken cancellationToken = default)
    {
        var query = context.MemberPointMallProducts
            .AsNoTracking()
            .Where(item => item.TenantId == tenantId && item.StoreId == storeId);

        if (status.HasValue)
        {
            query = query.Where(item => item.Status == status.Value);
        }

        var normalizedKeyword = (keyword ?? string.Empty).Trim();
        if (!string.IsNullOrWhiteSpace(normalizedKeyword))
        {
            var like = $"%{normalizedKeyword}%";
            query = query.Where(item =>
                EF.Functions.ILike(item.Name, like) ||
                (item.PhysicalName != null && EF.Functions.ILike(item.PhysicalName, like)));
        }

        return await query
            .OrderByDescending(item => item.Status)
            .ThenByDescending(item => item.UpdatedAt ?? item.CreatedAt)
            .ThenByDescending(item => item.Id)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<MemberPointMallProduct?> FindProductByIdAsync(
        long tenantId,
        long storeId,
        long productId,
        CancellationToken cancellationToken = default)
    {
        return context.MemberPointMallProducts
            .Where(item =>
                item.TenantId == tenantId &&
                item.StoreId == storeId &&
                item.Id == productId)
            .FirstOrDefaultAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<MemberPointMallProduct?> GetProductByIdAsync(
        long tenantId,
        long storeId,
        long productId,
        CancellationToken cancellationToken = default)
    {
        return context.MemberPointMallProducts
            .AsNoTracking()
            .Where(item =>
                item.TenantId == tenantId &&
                item.StoreId == storeId &&
                item.Id == productId)
            .FirstOrDefaultAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task AddProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default)
    {
        return context.MemberPointMallProducts.AddAsync(entity, cancellationToken).AsTask();
    }

    /// <inheritdoc />
    public Task UpdateProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default)
    {
        context.MemberPointMallProducts.Update(entity);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task DeleteProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default)
    {
        context.MemberPointMallProducts.Remove(entity);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task<bool> HasRecordsByProductIdAsync(
        long tenantId,
        long storeId,
        long pointMallProductId,
        CancellationToken cancellationToken = default)
    {
        return context.MemberPointMallRecords
            .AsNoTracking()
            .AnyAsync(item =>
                item.TenantId == tenantId &&
                item.StoreId == storeId &&
                item.PointMallProductId == pointMallProductId,
                cancellationToken);
    }

    /// <inheritdoc />
    public Task<int> CountMemberRedeemsByProductAsync(
        long tenantId,
        long storeId,
        long pointMallProductId,
        long memberId,
        CancellationToken cancellationToken = default)
    {
        return context.MemberPointMallRecords
            .AsNoTracking()
            .Where(item =>
                item.TenantId == tenantId &&
                item.StoreId == storeId &&
                item.PointMallProductId == pointMallProductId &&
                item.MemberId == memberId &&
                item.Status != MemberPointMallRecordStatus.Canceled)
            .CountAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<(IReadOnlyList<MemberPointMallRecord> Items, int TotalCount)> SearchRecordsAsync(
        long tenantId,
        long storeId,
        MemberPointMallRedeemType? redeemType,
        MemberPointMallRecordStatus? status,
        DateTime? startUtc,
        DateTime? endUtc,
        string? keyword,
        int page,
        int pageSize,
        CancellationToken cancellationToken = default)
    {
        var normalizedPage = Math.Max(1, page);
        var normalizedPageSize = Math.Clamp(pageSize, 1, 500);

        var query = BuildRecordQuery(
            tenantId,
            storeId,
            redeemType,
            status,
            startUtc,
            endUtc,
            keyword);

        var totalCount = await query.CountAsync(cancellationToken);
        if (totalCount == 0)
        {
            return ([], 0);
        }

        var items = await query
            .OrderByDescending(item => item.RedeemedAt)
            .ThenByDescending(item => item.Id)
            .Skip((normalizedPage - 1) * normalizedPageSize)
            .Take(normalizedPageSize)
            .ToListAsync(cancellationToken);

        return (items, totalCount);
    }

    /// <inheritdoc />
    public Task<MemberPointMallRecord?> GetRecordByIdAsync(
        long tenantId,
        long storeId,
        long recordId,
        CancellationToken cancellationToken = default)
    {
        return context.MemberPointMallRecords
            .AsNoTracking()
            .Where(item =>
                item.TenantId == tenantId &&
                item.StoreId == storeId &&
                item.Id == recordId)
            .FirstOrDefaultAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<MemberPointMallRecord?> FindRecordByIdAsync(
        long tenantId,
        long storeId,
        long recordId,
        CancellationToken cancellationToken = default)
    {
        return context.MemberPointMallRecords
            .Where(item =>
                item.TenantId == tenantId &&
                item.StoreId == storeId &&
                item.Id == recordId)
            .FirstOrDefaultAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<MemberPointMallRecord>> ListRecordsForExportAsync(
        long tenantId,
        long storeId,
        MemberPointMallRedeemType? redeemType,
        MemberPointMallRecordStatus? status,
        DateTime? startUtc,
        DateTime? endUtc,
        string? keyword,
        CancellationToken cancellationToken = default)
    {
        return await BuildRecordQuery(
                tenantId,
                storeId,
                redeemType,
                status,
                startUtc,
                endUtc,
                keyword)
            .OrderByDescending(item => item.RedeemedAt)
            .ThenByDescending(item => item.Id)
            .Take(20_000)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task AddRecordAsync(MemberPointMallRecord entity, CancellationToken cancellationToken = default)
    {
        return context.MemberPointMallRecords.AddAsync(entity, cancellationToken).AsTask();
    }

    /// <inheritdoc />
    public Task UpdateRecordAsync(MemberPointMallRecord entity, CancellationToken cancellationToken = default)
    {
        context.MemberPointMallRecords.Update(entity);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task AddPointLedgerAsync(MemberPointLedger entity, CancellationToken cancellationToken = default)
    {
        return context.MemberPointLedgers.AddAsync(entity, cancellationToken).AsTask();
    }

    /// <inheritdoc />
    public async Task<MemberPointMallRuleStatsSnapshot> GetRuleStatsAsync(
        long tenantId,
        long storeId,
        CancellationToken cancellationToken = default)
    {
        var redeemedPoints = await context.MemberPointMallRecords
            .AsNoTracking()
            .Where(item => item.TenantId == tenantId && item.StoreId == storeId)
            .Select(item => (int?)item.UsedPoints)
            .SumAsync(cancellationToken) ?? 0;

        var memberIds = await context.MemberPointMallRecords
            .AsNoTracking()
            .Where(item => item.TenantId == tenantId && item.StoreId == storeId)
            .Select(item => item.MemberId)
            .Distinct()
            .ToListAsync(cancellationToken);

        var pointMembers = memberIds.Count;
        if (pointMembers == 0)
        {
            return new MemberPointMallRuleStatsSnapshot
            {
                TotalIssuedPoints = 0,
                RedeemedPoints = 0,
                PointMembers = 0,
                RedeemRate = 0m
            };
        }

        var currentPoints = await context.MemberProfiles
            .AsNoTracking()
            .Where(item => item.TenantId == tenantId && memberIds.Contains(item.Id))
            .Select(item => (int?)item.PointsBalance)
            .SumAsync(cancellationToken) ?? 0;

        var totalIssuedPoints = Math.Max(redeemedPoints + Math.Max(0, currentPoints), 0);
        var redeemRate = totalIssuedPoints <= 0
            ? 0m
            : decimal.Round((decimal)redeemedPoints * 100m / totalIssuedPoints, 1, MidpointRounding.AwayFromZero);

        return new MemberPointMallRuleStatsSnapshot
        {
            TotalIssuedPoints = totalIssuedPoints,
            RedeemedPoints = redeemedPoints,
            PointMembers = pointMembers,
            RedeemRate = redeemRate
        };
    }

    /// <inheritdoc />
    public async Task<MemberPointMallRecordStatsSnapshot> GetRecordStatsAsync(
        long tenantId,
        long storeId,
        DateTime nowUtc,
        CancellationToken cancellationToken = default)
    {
        var normalizedNow = NormalizeUtc(nowUtc);
        var dayStart = new DateTime(
            normalizedNow.Year,
            normalizedNow.Month,
            normalizedNow.Day,
            0,
            0,
            0,
            DateTimeKind.Utc);
        var monthStart = new DateTime(
            normalizedNow.Year,
            normalizedNow.Month,
            1,
            0,
            0,
            0,
            DateTimeKind.Utc);

        var todayRedeemCount = await context.MemberPointMallRecords
            .AsNoTracking()
            .Where(item =>
                item.TenantId == tenantId &&
                item.StoreId == storeId &&
                item.RedeemedAt >= dayStart &&
                item.RedeemedAt <= normalizedNow)
            .CountAsync(cancellationToken);

        var pendingPhysicalCount = await context.MemberPointMallRecords
            .AsNoTracking()
            .Where(item =>
                item.TenantId == tenantId &&
                item.StoreId == storeId &&
                item.RedeemType == MemberPointMallRedeemType.Physical &&
                item.Status == MemberPointMallRecordStatus.PendingPickup)
            .CountAsync(cancellationToken);

        var currentMonthUsedPoints = await context.MemberPointMallRecords
            .AsNoTracking()
            .Where(item =>
                item.TenantId == tenantId &&
                item.StoreId == storeId &&
                item.RedeemedAt >= monthStart &&
                item.RedeemedAt <= normalizedNow)
            .Select(item => (int?)item.UsedPoints)
            .SumAsync(cancellationToken) ?? 0;

        return new MemberPointMallRecordStatsSnapshot
        {
            TodayRedeemCount = todayRedeemCount,
            PendingPhysicalCount = pendingPhysicalCount,
            CurrentMonthUsedPoints = currentMonthUsedPoints
        };
    }

    /// <inheritdoc />
    public async Task<Dictionary<long, MemberPointMallProductAggregateSnapshot>> GetProductAggregatesAsync(
        long tenantId,
        long storeId,
        IReadOnlyCollection<long> pointMallProductIds,
        CancellationToken cancellationToken = default)
    {
        if (pointMallProductIds.Count == 0)
        {
            return [];
        }

        var aggregates = await context.MemberPointMallRecords
            .AsNoTracking()
            .Where(item =>
                item.TenantId == tenantId &&
                item.StoreId == storeId &&
                pointMallProductIds.Contains(item.PointMallProductId))
            .GroupBy(item => item.PointMallProductId)
            .Select(group => new MemberPointMallProductAggregateSnapshot
            {
                PointMallProductId = group.Key,
                RedeemedCount = group.Count()
            })
            .ToListAsync(cancellationToken);

        return aggregates.ToDictionary(item => item.PointMallProductId, item => item);
    }

    /// <inheritdoc />
    public Task SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return context.SaveChangesAsync(cancellationToken);
    }

    private IQueryable<MemberPointMallRecord> BuildRecordQuery(
        long tenantId,
        long storeId,
        MemberPointMallRedeemType? redeemType,
        MemberPointMallRecordStatus? status,
        DateTime? startUtc,
        DateTime? endUtc,
        string? keyword)
    {
        var query = context.MemberPointMallRecords
            .AsNoTracking()
            .Where(item => item.TenantId == tenantId && item.StoreId == storeId);

        if (redeemType.HasValue)
        {
            query = query.Where(item => item.RedeemType == redeemType.Value);
        }

        if (status.HasValue)
        {
            query = query.Where(item => item.Status == status.Value);
        }

        if (startUtc.HasValue)
        {
            var normalizedStart = NormalizeUtc(startUtc.Value);
            query = query.Where(item => item.RedeemedAt >= normalizedStart);
        }

        if (endUtc.HasValue)
        {
            var normalizedEnd = NormalizeUtc(endUtc.Value);
            query = query.Where(item => item.RedeemedAt <= normalizedEnd);
        }

        var normalizedKeyword = (keyword ?? string.Empty).Trim();
        if (!string.IsNullOrWhiteSpace(normalizedKeyword))
        {
            var like = $"%{normalizedKeyword}%";
            query = query.Where(item =>
                EF.Functions.ILike(item.RecordNo, like) ||
                EF.Functions.ILike(item.MemberName, like) ||
                EF.Functions.ILike(item.MemberMobileMasked, like) ||
                EF.Functions.ILike(item.ProductName, like));
        }

        return query;
    }

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