using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Domain.Products.Entities;
using TakeoutSaaS.Domain.Products.Enums;
using TakeoutSaaS.Domain.Products.Repositories;
using TakeoutSaaS.Infrastructure.App.Persistence;

namespace TakeoutSaaS.Infrastructure.App.Repositories;

/// <summary>
/// 商品聚合的 EF Core 仓储实现。
/// </summary>
/// <remarks>
/// 初始化仓储。
/// </remarks>
public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductRepository
{
    /// <inheritdoc />
    public Task<Product?> FindByIdAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        return context.Products
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.Id == productId)
            .FirstOrDefaultAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Product>> SearchAsync(long tenantId, long? storeId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default, DateTime? updatedAfter = null)
    {
        var query = context.Products
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId);

        if (storeId.HasValue)
        {
            query = query.Where(x => x.StoreId == storeId.Value);
        }

        if (categoryId.HasValue)
        {
            query = query.Where(x => x.CategoryId == categoryId.Value);
        }

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

        if (updatedAfter.HasValue)
        {
            query = query.Where(x => (x.UpdatedAt ?? x.CreatedAt) >= updatedAfter.Value);
        }

        var products = await query
            .OrderBy(x => x.Name)
            .ToListAsync(cancellationToken);

        return products;
    }

    /// <inheritdoc />
    public async Task<(IReadOnlyList<Product> Items, int Total)> SearchPagedAsync(
        ProductSearchFilter filter,
        CancellationToken cancellationToken = default)
    {
        if (filter.TenantId <= 0)
        {
            throw new InvalidOperationException("TenantId 不能为空且必须大于 0");
        }

        var page = filter.Page <= 0 ? 1 : filter.Page;
        var pageSize = filter.PageSize <= 0 ? 20 : filter.PageSize;

        var query = context.Products
            .AsNoTracking()
            .Where(x => x.TenantId == filter.TenantId);

        if (filter.StoreId.HasValue)
        {
            query = query.Where(x => x.StoreId == filter.StoreId.Value);
        }

        if (filter.CategoryId.HasValue)
        {
            query = query.Where(x => x.CategoryId == filter.CategoryId.Value);
        }

        if (filter.Status.HasValue)
        {
            query = query.Where(x => x.Status == filter.Status.Value);
        }

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = $"%{filter.Keyword.Trim()}%";
            query = query.Where(x =>
                EF.Functions.ILike(x.Name, keyword) ||
                EF.Functions.ILike(x.SpuCode, keyword));
        }

        if (filter.Kind.HasValue)
        {
            query = query.Where(x => x.Kind == filter.Kind.Value);
        }

        if (filter.IsSoldOut == true)
        {
            query = query.Where(x => x.SoldoutMode.HasValue);
        }
        else if (filter.Status == ProductStatus.OffShelf)
        {
            // 状态为 OffShelf 时，排除“沽清”以保持前台“下架”口径。
            query = query.Where(x => !x.SoldoutMode.HasValue);
        }

        var sorted = ApplySearchSort(query, filter.SortBy, filter.SortDescending);
        var total = await sorted.CountAsync(cancellationToken);
        var items = await sorted
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync(cancellationToken);

        return (items, total);
    }

    private static IOrderedQueryable<Product> ApplySearchSort(
        IQueryable<Product> query,
        string? sortBy,
        bool sortDescending)
    {
        return sortBy?.ToLowerInvariant() switch
        {
            "name" => sortDescending
                ? query.OrderByDescending(x => x.Name).ThenByDescending(x => x.Id)
                : query.OrderBy(x => x.Name).ThenBy(x => x.Id),
            "price" => sortDescending
                ? query.OrderByDescending(x => x.Price).ThenByDescending(x => x.Id)
                : query.OrderBy(x => x.Price).ThenBy(x => x.Id),
            "status" => sortDescending
                ? query.OrderByDescending(x => x.Status).ThenByDescending(x => x.Id)
                : query.OrderBy(x => x.Status).ThenBy(x => x.Id),
            _ => sortDescending
                ? query.OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)
                : query.OrderBy(x => x.CreatedAt).ThenBy(x => x.Id)
        };
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductCategory>> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default)
    {
        var categories = await context.ProductCategories
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId)
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);

        return categories;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductCategory>> GetCategoriesByStoreAsync(long tenantId, long storeId, bool onlyEnabled = true, CancellationToken cancellationToken = default)
    {
        var query = context.ProductCategories
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == storeId);
        if (onlyEnabled)
        {
            query = query.Where(x => x.IsEnabled);
        }
        var categories = await query
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);
        return categories;
    }

    /// <inheritdoc />
    public Task<ProductCategory?> FindCategoryByIdAsync(long categoryId, long tenantId, CancellationToken cancellationToken = default)
    {
        return context.ProductCategories
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.Id == categoryId)
            .FirstOrDefaultAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<bool> ExistsCategoryNameAsync(long tenantId, long storeId, string name, long? excludeCategoryId = null, CancellationToken cancellationToken = default)
    {
        var normalizedName = (name ?? string.Empty).Trim();
        var normalizedLower = normalizedName.ToLowerInvariant();
        var query = context.ProductCategories
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                x.Name.ToLower() == normalizedLower);

        if (excludeCategoryId.HasValue)
        {
            query = query.Where(x => x.Id != excludeCategoryId.Value);
        }

        return query.AnyAsync(cancellationToken);
    }

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

        return await context.Products
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == storeId && categoryIds.Contains(x.CategoryId))
            .GroupBy(x => x.CategoryId)
            .Select(group => new { CategoryId = group.Key, Count = group.Count() })
            .ToDictionaryAsync(item => item.CategoryId, item => item.Count, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductSpecTemplate>> GetSpecTemplatesByStoreAsync(long tenantId, long storeId, CancellationToken cancellationToken = default)
    {
        return await context.ProductSpecTemplates
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                x.TemplateType != ProductSpecTemplateType.Addon)
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductSpecTemplate>> GetAddonTemplatesByStoreAsync(long tenantId, long storeId, CancellationToken cancellationToken = default)
    {
        return await context.ProductSpecTemplates
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                x.TemplateType == ProductSpecTemplateType.Addon)
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductLabel>> GetLabelsByStoreAsync(long tenantId, long storeId, CancellationToken cancellationToken = default)
    {
        return await context.ProductLabels
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == storeId)
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductSchedule>> GetSchedulesByStoreAsync(long tenantId, long storeId, CancellationToken cancellationToken = default)
    {
        return await context.ProductSchedules
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == storeId)
            .OrderBy(x => x.Name)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<ProductSpecTemplate?> FindSpecTemplateByIdAsync(long templateId, long tenantId, CancellationToken cancellationToken = default)
    {
        return context.ProductSpecTemplates
            .Where(x =>
                x.TenantId == tenantId &&
                x.Id == templateId &&
                x.TemplateType != ProductSpecTemplateType.Addon)
            .FirstOrDefaultAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<ProductSpecTemplate?> FindAddonTemplateByIdAsync(long templateId, long tenantId, CancellationToken cancellationToken = default)
    {
        return context.ProductSpecTemplates
            .Where(x =>
                x.TenantId == tenantId &&
                x.Id == templateId &&
                x.TemplateType == ProductSpecTemplateType.Addon)
            .FirstOrDefaultAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<ProductLabel?> FindLabelByIdAsync(long labelId, long tenantId, CancellationToken cancellationToken = default)
    {
        return context.ProductLabels
            .Where(x => x.TenantId == tenantId && x.Id == labelId)
            .FirstOrDefaultAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<ProductSchedule?> FindScheduleByIdAsync(long scheduleId, long tenantId, CancellationToken cancellationToken = default)
    {
        return context.ProductSchedules
            .Where(x => x.TenantId == tenantId && x.Id == scheduleId)
            .FirstOrDefaultAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<bool> ExistsSpecTemplateNameAsync(long tenantId, long storeId, string name, long? excludeTemplateId = null, CancellationToken cancellationToken = default)
    {
        var normalizedName = (name ?? string.Empty).Trim();
        var normalizedLower = normalizedName.ToLowerInvariant();
        var query = context.ProductSpecTemplates
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                x.TemplateType != ProductSpecTemplateType.Addon &&
                x.Name.ToLower() == normalizedLower);

        if (excludeTemplateId.HasValue)
        {
            query = query.Where(x => x.Id != excludeTemplateId.Value);
        }

        return query.AnyAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<bool> ExistsAddonTemplateNameAsync(long tenantId, long storeId, string name, long? excludeTemplateId = null, CancellationToken cancellationToken = default)
    {
        var normalizedName = (name ?? string.Empty).Trim();
        var normalizedLower = normalizedName.ToLowerInvariant();
        var query = context.ProductSpecTemplates
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                x.TemplateType == ProductSpecTemplateType.Addon &&
                x.Name.ToLower() == normalizedLower);

        if (excludeTemplateId.HasValue)
        {
            query = query.Where(x => x.Id != excludeTemplateId.Value);
        }

        return query.AnyAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<bool> ExistsLabelNameAsync(long tenantId, long storeId, string name, long? excludeLabelId = null, CancellationToken cancellationToken = default)
    {
        var normalizedName = (name ?? string.Empty).Trim();
        var normalizedLower = normalizedName.ToLowerInvariant();
        var query = context.ProductLabels
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                x.Name.ToLower() == normalizedLower);

        if (excludeLabelId.HasValue)
        {
            query = query.Where(x => x.Id != excludeLabelId.Value);
        }

        return query.AnyAsync(cancellationToken);
    }

    /// <inheritdoc />
    public Task<bool> ExistsScheduleNameAsync(long tenantId, long storeId, string name, long? excludeScheduleId = null, CancellationToken cancellationToken = default)
    {
        var normalizedName = (name ?? string.Empty).Trim();
        var normalizedLower = normalizedName.ToLowerInvariant();
        var query = context.ProductSchedules
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                x.Name.ToLower() == normalizedLower);

        if (excludeScheduleId.HasValue)
        {
            query = query.Where(x => x.Id != excludeScheduleId.Value);
        }

        return query.AnyAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductSpecTemplateOption>> GetSpecTemplateOptionsByTemplateIdsAsync(IReadOnlyCollection<long> templateIds, long tenantId, CancellationToken cancellationToken = default)
    {
        if (templateIds.Count == 0)
        {
            return Array.Empty<ProductSpecTemplateOption>();
        }

        return await context.ProductSpecTemplateOptions
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && templateIds.Contains(x.TemplateId))
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductSpecTemplateProduct>> GetSpecTemplateProductsByTemplateIdsAsync(IReadOnlyCollection<long> templateIds, long tenantId, long storeId, CancellationToken cancellationToken = default)
    {
        if (templateIds.Count == 0)
        {
            return Array.Empty<ProductSpecTemplateProduct>();
        }

        return await context.ProductSpecTemplateProducts
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                templateIds.Contains(x.TemplateId))
            .OrderBy(x => x.TemplateId)
            .ThenBy(x => x.ProductId)
            .ToListAsync(cancellationToken);
    }

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

        return await context.ProductLabelProducts
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                labelIds.Contains(x.LabelId))
            .GroupBy(x => x.LabelId)
            .Select(group => new { LabelId = group.Key, Count = group.Count() })
            .ToDictionaryAsync(item => item.LabelId, item => item.Count, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductScheduleProduct>> GetScheduleProductsByScheduleIdsAsync(IReadOnlyCollection<long> scheduleIds, long tenantId, long storeId, CancellationToken cancellationToken = default)
    {
        if (scheduleIds.Count == 0)
        {
            return Array.Empty<ProductScheduleProduct>();
        }

        return await context.ProductScheduleProducts
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                scheduleIds.Contains(x.ScheduleId))
            .OrderBy(x => x.ScheduleId)
            .ThenBy(x => x.ProductId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<long>> FilterExistingProductIdsAsync(long tenantId, long storeId, IReadOnlyCollection<long> productIds, CancellationToken cancellationToken = default)
    {
        if (productIds.Count == 0)
        {
            return Array.Empty<long>();
        }

        return await context.Products
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                productIds.Contains(x.Id))
            .Select(x => x.Id)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Product>> SearchPickerAsync(long tenantId, long storeId, long? categoryId, string? keyword, int limit, CancellationToken cancellationToken = default)
    {
        var query = context.Products
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == storeId);

        if (categoryId.HasValue)
        {
            query = query.Where(x => x.CategoryId == categoryId.Value);
        }

        var normalizedKeyword = keyword?.Trim();
        if (!string.IsNullOrWhiteSpace(normalizedKeyword))
        {
            var loweredKeyword = normalizedKeyword.ToLowerInvariant();
            query = query.Where(x =>
                x.Name.ToLower().Contains(loweredKeyword) ||
                x.SpuCode.ToLower().Contains(loweredKeyword));
        }

        var normalizedLimit = Math.Clamp(limit, 1, 500);
        return await query
            .OrderBy(x => x.Name)
            .ThenBy(x => x.Id)
            .Take(normalizedLimit)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Product>> GetByIdsAsync(
        long tenantId,
        long storeId,
        IReadOnlyCollection<long> productIds,
        CancellationToken cancellationToken = default)
    {
        if (productIds.Count == 0)
        {
            return [];
        }

        return await context.Products
            .AsNoTracking()
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                productIds.Contains(x.Id))
            .OrderBy(x => x.Name)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<int> BatchUpdateProductCategoryAsync(long tenantId, long storeId, long categoryId, IReadOnlyCollection<long> productIds, CancellationToken cancellationToken = default)
    {
        if (productIds.Count == 0)
        {
            return 0;
        }

        var products = await context.Products
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                productIds.Contains(x.Id))
            .ToListAsync(cancellationToken);

        foreach (var product in products)
        {
            product.CategoryId = categoryId;
        }

        return products.Count;
    }

    /// <inheritdoc />
    public async Task<bool> MoveProductToCategoryAsync(long tenantId, long storeId, long fromCategoryId, long toCategoryId, long productId, CancellationToken cancellationToken = default)
    {
        var product = await context.Products
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                x.Id == productId &&
                x.CategoryId == fromCategoryId)
            .FirstOrDefaultAsync(cancellationToken);

        if (product is null)
        {
            return false;
        }

        product.CategoryId = toCategoryId;
        return true;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductSku>> GetSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var skus = await context.ProductSkus
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);

        return skus;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductSku>> GetSkusByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default)
    {
        if (productIds.Count == 0)
        {
            return Array.Empty<ProductSku>();
        }
        var skus = await context.ProductSkus
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && productIds.Contains(x.ProductId))
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);
        return skus;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductComboGroup>> GetComboGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var groups = await context.ProductComboGroups
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);

        return groups;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductComboGroup>> GetComboGroupsByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default)
    {
        if (productIds.Count == 0)
        {
            return Array.Empty<ProductComboGroup>();
        }

        var groups = await context.ProductComboGroups
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && productIds.Contains(x.ProductId))
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);
        return groups;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductComboGroupItem>> GetComboGroupItemsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var groupIds = await context.ProductComboGroups
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .Select(x => x.Id)
            .ToListAsync(cancellationToken);

        if (groupIds.Count == 0)
        {
            return Array.Empty<ProductComboGroupItem>();
        }

        var items = await context.ProductComboGroupItems
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && groupIds.Contains(x.ComboGroupId))
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);

        return items;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductComboGroupItem>> GetComboGroupItemsByGroupIdsAsync(IReadOnlyCollection<long> comboGroupIds, long tenantId, CancellationToken cancellationToken = default)
    {
        if (comboGroupIds.Count == 0)
        {
            return Array.Empty<ProductComboGroupItem>();
        }

        var items = await context.ProductComboGroupItems
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && comboGroupIds.Contains(x.ComboGroupId))
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);
        return items;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductAddonGroup>> GetAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var groups = await context.ProductAddonGroups
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);

        return groups;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductAddonGroup>> GetAddonGroupsByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default)
    {
        if (productIds.Count == 0)
        {
            return Array.Empty<ProductAddonGroup>();
        }
        var groups = await context.ProductAddonGroups
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && productIds.Contains(x.ProductId))
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);
        return groups;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductAddonOption>> GetAddonOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var groupIds = await context.ProductAddonGroups
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .Select(x => x.Id)
            .ToListAsync(cancellationToken);

        if (groupIds.Count == 0)
        {
            return Array.Empty<ProductAddonOption>();
        }

        var options = await context.ProductAddonOptions
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId))
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);

        return options;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductAddonOption>> GetAddonOptionsByGroupIdsAsync(IReadOnlyCollection<long> addonGroupIds, long tenantId, CancellationToken cancellationToken = default)
    {
        if (addonGroupIds.Count == 0)
        {
            return Array.Empty<ProductAddonOption>();
        }
        var options = await context.ProductAddonOptions
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && addonGroupIds.Contains(x.AddonGroupId))
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);
        return options;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductAttributeGroup>> GetAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var groups = await context.ProductAttributeGroups
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);

        return groups;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductAttributeGroup>> GetAttributeGroupsByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default)
    {
        if (productIds.Count == 0)
        {
            return Array.Empty<ProductAttributeGroup>();
        }
        var groups = await context.ProductAttributeGroups
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && productIds.Contains(x.ProductId))
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);
        return groups;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductAttributeOption>> GetAttributeOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var groupIds = await context.ProductAttributeGroups
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .Select(x => x.Id)
            .ToListAsync(cancellationToken);

        if (groupIds.Count == 0)
        {
            return Array.Empty<ProductAttributeOption>();
        }

        var options = await context.ProductAttributeOptions
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId))
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);

        return options;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductAttributeOption>> GetAttributeOptionsByGroupIdsAsync(IReadOnlyCollection<long> attributeGroupIds, long tenantId, CancellationToken cancellationToken = default)
    {
        if (attributeGroupIds.Count == 0)
        {
            return Array.Empty<ProductAttributeOption>();
        }
        var options = await context.ProductAttributeOptions
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && attributeGroupIds.Contains(x.AttributeGroupId))
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);
        return options;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductMediaAsset>> GetMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var assets = await context.ProductMediaAssets
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);

        return assets;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductMediaAsset>> GetMediaAssetsByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default)
    {
        if (productIds.Count == 0)
        {
            return Array.Empty<ProductMediaAsset>();
        }
        var assets = await context.ProductMediaAssets
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && productIds.Contains(x.ProductId))
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);
        return assets;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductPricingRule>> GetPricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var rules = await context.ProductPricingRules
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);

        return rules;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ProductPricingRule>> GetPricingRulesByProductIdsAsync(IReadOnlyCollection<long> productIds, long tenantId, CancellationToken cancellationToken = default)
    {
        if (productIds.Count == 0)
        {
            return Array.Empty<ProductPricingRule>();
        }
        var rules = await context.ProductPricingRules
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && productIds.Contains(x.ProductId))
            .OrderBy(x => x.SortOrder)
            .ToListAsync(cancellationToken);
        return rules;
    }

    /// <inheritdoc />
    public Task AddCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default)
    {
        return context.ProductCategories.AddAsync(category, cancellationToken).AsTask();
    }

    /// <inheritdoc />
    public Task AddSpecTemplateAsync(ProductSpecTemplate template, CancellationToken cancellationToken = default)
    {
        return context.ProductSpecTemplates.AddAsync(template, cancellationToken).AsTask();
    }

    /// <inheritdoc />
    public Task AddSpecTemplateOptionsAsync(IEnumerable<ProductSpecTemplateOption> options, CancellationToken cancellationToken = default)
    {
        return context.ProductSpecTemplateOptions.AddRangeAsync(options, cancellationToken);
    }

    /// <inheritdoc />
    public Task AddSpecTemplateProductsAsync(IEnumerable<ProductSpecTemplateProduct> relations, CancellationToken cancellationToken = default)
    {
        return context.ProductSpecTemplateProducts.AddRangeAsync(relations, cancellationToken);
    }

    /// <inheritdoc />
    public Task AddLabelAsync(ProductLabel label, CancellationToken cancellationToken = default)
    {
        return context.ProductLabels.AddAsync(label, cancellationToken).AsTask();
    }

    /// <inheritdoc />
    public Task AddScheduleAsync(ProductSchedule schedule, CancellationToken cancellationToken = default)
    {
        return context.ProductSchedules.AddAsync(schedule, cancellationToken).AsTask();
    }

    /// <inheritdoc />
    public Task AddScheduleProductsAsync(IEnumerable<ProductScheduleProduct> relations, CancellationToken cancellationToken = default)
    {
        return context.ProductScheduleProducts.AddRangeAsync(relations, cancellationToken);
    }

    /// <inheritdoc />
    public Task AddProductAsync(Product product, CancellationToken cancellationToken = default)
    {
        return context.Products.AddAsync(product, cancellationToken).AsTask();
    }

    /// <inheritdoc />
    public Task AddSkusAsync(IEnumerable<ProductSku> skus, CancellationToken cancellationToken = default)
    {
        return context.ProductSkus.AddRangeAsync(skus, cancellationToken);
    }

    /// <inheritdoc />
    public Task AddComboGroupsAsync(IEnumerable<ProductComboGroup> groups, IEnumerable<ProductComboGroupItem> items, CancellationToken cancellationToken = default)
    {
        var addGroupsTask = context.ProductComboGroups.AddRangeAsync(groups, cancellationToken);
        var addItemsTask = context.ProductComboGroupItems.AddRangeAsync(items, cancellationToken);
        return Task.WhenAll(addGroupsTask, addItemsTask);
    }

    /// <inheritdoc />
    public Task AddAddonGroupsAsync(IEnumerable<ProductAddonGroup> groups, IEnumerable<ProductAddonOption> options, CancellationToken cancellationToken = default)
    {
        var addGroupsTask = context.ProductAddonGroups.AddRangeAsync(groups, cancellationToken);
        var addOptionsTask = context.ProductAddonOptions.AddRangeAsync(options, cancellationToken);
        return Task.WhenAll(addGroupsTask, addOptionsTask);
    }

    /// <inheritdoc />
    public Task AddAttributeGroupsAsync(IEnumerable<ProductAttributeGroup> groups, IEnumerable<ProductAttributeOption> options, CancellationToken cancellationToken = default)
    {
        var addGroupsTask = context.ProductAttributeGroups.AddRangeAsync(groups, cancellationToken);
        var addOptionsTask = context.ProductAttributeOptions.AddRangeAsync(options, cancellationToken);
        return Task.WhenAll(addGroupsTask, addOptionsTask);
    }

    /// <inheritdoc />
    public Task AddMediaAssetsAsync(IEnumerable<ProductMediaAsset> assets, CancellationToken cancellationToken = default)
    {
        return context.ProductMediaAssets.AddRangeAsync(assets, cancellationToken);
    }

    /// <inheritdoc />
    public Task AddPricingRulesAsync(IEnumerable<ProductPricingRule> rules, CancellationToken cancellationToken = default)
    {
        return context.ProductPricingRules.AddRangeAsync(rules, cancellationToken);
    }

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

    /// <inheritdoc />
    public Task UpdateProductAsync(Product product, CancellationToken cancellationToken = default)
    {
        context.Products.Update(product);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteProductAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        await RemovePricingRulesAsync(productId, tenantId, cancellationToken);
        await RemoveMediaAssetsAsync(productId, tenantId, cancellationToken);
        await RemoveAttributeGroupsAsync(productId, tenantId, cancellationToken);
        await RemoveAddonGroupsAsync(productId, tenantId, cancellationToken);
        await RemoveComboGroupsAsync(productId, tenantId, cancellationToken);
        await RemoveSkusAsync(productId, tenantId, cancellationToken);

        var existing = await context.Products
            .Where(x => x.TenantId == tenantId && x.Id == productId)
            .FirstOrDefaultAsync(cancellationToken);

        if (existing == null)
        {
            return;
        }

        context.Products.Remove(existing);
    }

    /// <inheritdoc />
    public Task UpdateCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default)
    {
        context.ProductCategories.Update(category);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task UpdateSpecTemplateAsync(ProductSpecTemplate template, CancellationToken cancellationToken = default)
    {
        context.ProductSpecTemplates.Update(template);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task UpdateLabelAsync(ProductLabel label, CancellationToken cancellationToken = default)
    {
        context.ProductLabels.Update(label);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task UpdateScheduleAsync(ProductSchedule schedule, CancellationToken cancellationToken = default)
    {
        context.ProductSchedules.Update(schedule);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteCategoryAsync(long categoryId, long tenantId, CancellationToken cancellationToken = default)
    {
        var existing = await context.ProductCategories
            .Where(x => x.TenantId == tenantId && x.Id == categoryId)
            .FirstOrDefaultAsync(cancellationToken);

        if (existing == null)
        {
            return;
        }

        context.ProductCategories.Remove(existing);
    }

    /// <inheritdoc />
    public async Task DeleteSpecTemplateAsync(long templateId, long tenantId, CancellationToken cancellationToken = default)
    {
        var existing = await context.ProductSpecTemplates
            .Where(x => x.TenantId == tenantId && x.Id == templateId)
            .FirstOrDefaultAsync(cancellationToken);

        if (existing is null)
        {
            return;
        }

        await context.ProductSpecTemplates
            .Where(x => x.TenantId == tenantId && x.Id == templateId)
            .ExecuteDeleteAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task DeleteLabelAsync(long labelId, long tenantId, CancellationToken cancellationToken = default)
    {
        var existing = await context.ProductLabels
            .Where(x => x.TenantId == tenantId && x.Id == labelId)
            .FirstOrDefaultAsync(cancellationToken);

        if (existing is null)
        {
            return;
        }

        await context.ProductLabels
            .Where(x => x.TenantId == tenantId && x.Id == labelId)
            .ExecuteDeleteAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task DeleteScheduleAsync(long scheduleId, long tenantId, CancellationToken cancellationToken = default)
    {
        var existing = await context.ProductSchedules
            .Where(x => x.TenantId == tenantId && x.Id == scheduleId)
            .FirstOrDefaultAsync(cancellationToken);

        if (existing is null)
        {
            return;
        }

        await context.ProductSchedules
            .Where(x => x.TenantId == tenantId && x.Id == scheduleId)
            .ExecuteDeleteAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task RemoveSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var skus = await context.ProductSkus
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .ToListAsync(cancellationToken);

        if (skus.Count == 0)
        {
            return;
        }

        context.ProductSkus.RemoveRange(skus);
    }

    /// <inheritdoc />
    public async Task RemoveComboGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var groupIds = await context.ProductComboGroups
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .Select(x => x.Id)
            .ToListAsync(cancellationToken);

        if (groupIds.Count == 0)
        {
            return;
        }

        var items = await context.ProductComboGroupItems
            .Where(x => x.TenantId == tenantId && groupIds.Contains(x.ComboGroupId))
            .ToListAsync(cancellationToken);

        if (items.Count > 0)
        {
            context.ProductComboGroupItems.RemoveRange(items);
        }

        var groups = await context.ProductComboGroups
            .Where(x => groupIds.Contains(x.Id))
            .ToListAsync(cancellationToken);

        if (groups.Count > 0)
        {
            context.ProductComboGroups.RemoveRange(groups);
        }
    }

    /// <inheritdoc />
    public async Task RemoveSpecTemplateOptionsAsync(long templateId, long tenantId, CancellationToken cancellationToken = default)
    {
        await context.ProductSpecTemplateOptions
            .Where(x => x.TenantId == tenantId && x.TemplateId == templateId)
            .ExecuteDeleteAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task RemoveSpecTemplateProductsAsync(long templateId, long tenantId, long storeId, CancellationToken cancellationToken = default)
    {
        await context.ProductSpecTemplateProducts
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                x.TemplateId == templateId)
            .ExecuteDeleteAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task RemoveLabelProductsAsync(long labelId, long tenantId, long storeId, CancellationToken cancellationToken = default)
    {
        await context.ProductLabelProducts
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                x.LabelId == labelId)
            .ExecuteDeleteAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task RemoveScheduleProductsAsync(long scheduleId, long tenantId, long storeId, CancellationToken cancellationToken = default)
    {
        await context.ProductScheduleProducts
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == storeId &&
                x.ScheduleId == scheduleId)
            .ExecuteDeleteAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task RemoveAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var groupIds = await context.ProductAddonGroups
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .Select(x => x.Id)
            .ToListAsync(cancellationToken);

        if (groupIds.Count == 0)
        {
            return;
        }

        var options = await context.ProductAddonOptions
            .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId))
            .ToListAsync(cancellationToken);

        if (options.Count > 0)
        {
            context.ProductAddonOptions.RemoveRange(options);
        }

        var groups = await context.ProductAddonGroups
            .Where(x => groupIds.Contains(x.Id))
            .ToListAsync(cancellationToken);

        if (groups.Count > 0)
        {
            context.ProductAddonGroups.RemoveRange(groups);
        }
    }

    /// <inheritdoc />
    public async Task RemoveAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var groupIds = await context.ProductAttributeGroups
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .Select(x => x.Id)
            .ToListAsync(cancellationToken);

        if (groupIds.Count == 0)
        {
            return;
        }

        var options = await context.ProductAttributeOptions
            .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId))
            .ToListAsync(cancellationToken);

        if (options.Count > 0)
        {
            context.ProductAttributeOptions.RemoveRange(options);
        }

        var groups = await context.ProductAttributeGroups
            .Where(x => groupIds.Contains(x.Id))
            .ToListAsync(cancellationToken);

        if (groups.Count > 0)
        {
            context.ProductAttributeGroups.RemoveRange(groups);
        }
    }

    /// <inheritdoc />
    public async Task RemoveMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var assets = await context.ProductMediaAssets
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .ToListAsync(cancellationToken);

        if (assets.Count == 0)
        {
            return;
        }

        context.ProductMediaAssets.RemoveRange(assets);
    }

    /// <inheritdoc />
    public async Task RemovePricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
    {
        var rules = await context.ProductPricingRules
            .Where(x => x.TenantId == tenantId && x.ProductId == productId)
            .ToListAsync(cancellationToken);

        if (rules.Count == 0)
        {
            return;
        }

        context.ProductPricingRules.RemoveRange(rules);
    }
}
