using System.Globalization;
using ClosedXML.Excel;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Application.App.Stores.Services;
using TakeoutSaaS.Domain.Products.Entities;
using TakeoutSaaS.Domain.Products.Enums;
using TakeoutSaaS.Infrastructure.App.Persistence;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Ids;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Web.Api;
using TakeoutSaaS.TenantApi.Contracts.Product;

namespace TakeoutSaaS.TenantApi.Controllers;

/// <summary>
/// 提供商品批量工具能力，包括批量调价、上下架、移类、跨店同步、导入与导出。
/// </summary>
/// <param name="dbContext">应用数据库上下文。</param>
/// <param name="storeContextService">门店上下文服务。</param>
/// <param name="idGenerator">雪花 ID 生成器。</param>
[ApiVersion("1.0")]
[Authorize]
[Route("api/tenant/v{version:apiVersion}/product/batch")]
public sealed class ProductBatchToolController(
    TakeoutAppDbContext dbContext,
    StoreContextService storeContextService,
    IIdGenerator idGenerator) : BaseApiController
{
    private const int MaxImportFileSize = 10 * 1024 * 1024;

    private static readonly string[] ImportHeaders =
    [
        "SPU编码",
        "商品名称",
        "副标题",
        "分类名称",
        "商品类型",
        "售价",
        "划线价",
        "库存",
        "状态"
    ];

    /// <summary>
    /// 预览批量调价结果。
    /// </summary>
    /// <param name="request">调价预览请求。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>调价预览结果。</returns>
    [HttpPost("price-adjust/preview")]
    [ProducesResponseType(typeof(ApiResponse<BatchPricePreviewResponse>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<BatchPricePreviewResponse>> PreviewPriceAdjust(
        [FromBody] BatchPriceAdjustPreviewRequest request,
        CancellationToken cancellationToken)
    {
        var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
        var (tenantId, _) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await EnsureStoreAccessibleAsync(storeId, cancellationToken);

        ValidatePriceAdjustRequest(request.Direction, request.AmountType, request.Amount);
        var scopedQuery = BuildScopedProductQuery(tenantId, storeId, request.Scope);
        var totalCount = await scopedQuery.CountAsync(cancellationToken);
        var previewRows = await scopedQuery
            .OrderBy(x => x.Name)
            .ThenBy(x => x.Id)
            .Take(50)
            .Select(x => new { x.Id, x.Name, x.Price })
            .ToListAsync(cancellationToken);

        var items = previewRows
            .Select(row =>
            {
                var newPrice = CalculateAdjustedPrice(row.Price, request.Direction, request.AmountType, request.Amount);
                return new BatchPricePreviewItemResponse
                {
                    ProductId = row.Id.ToString(),
                    ProductName = row.Name,
                    OriginalPrice = row.Price,
                    NewPrice = newPrice,
                    DeltaPrice = decimal.Round(newPrice - row.Price, 2, MidpointRounding.AwayFromZero)
                };
            })
            .ToList();

        return ApiResponse<BatchPricePreviewResponse>.Ok(new BatchPricePreviewResponse
        {
            TotalCount = totalCount,
            Items = items
        });
    }

    /// <summary>
    /// 执行批量调价。
    /// </summary>
    /// <param name="request">调价请求。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>批量工具执行结果。</returns>
    [HttpPost("price-adjust")]
    [ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<BatchToolResultResponse>> PriceAdjust(
        [FromBody] BatchPriceAdjustRequest request,
        CancellationToken cancellationToken)
    {
        var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
        var (tenantId, _) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await EnsureStoreAccessibleAsync(storeId, cancellationToken);

        ValidatePriceAdjustRequest(request.Direction, request.AmountType, request.Amount);
        var products = await BuildScopedProductQuery(tenantId, storeId, request.Scope).ToListAsync(cancellationToken);

        var successCount = 0;
        var skippedCount = 0;
        foreach (var product in products)
        {
            var newPrice = CalculateAdjustedPrice(product.Price, request.Direction, request.AmountType, request.Amount);
            if (newPrice == product.Price)
            {
                skippedCount++;
                continue;
            }

            product.Price = newPrice;
            successCount++;
        }

        if (successCount > 0)
        {
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        return ApiResponse<BatchToolResultResponse>.Ok(new BatchToolResultResponse
        {
            TotalCount = products.Count,
            SuccessCount = successCount,
            FailedCount = 0,
            SkippedCount = skippedCount
        });
    }

    /// <summary>
    /// 批量切换商品上下架状态。
    /// </summary>
    /// <param name="request">上下架切换请求。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>批量工具执行结果。</returns>
    [HttpPost("sale-switch")]
    [ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<BatchToolResultResponse>> SaleSwitch(
        [FromBody] BatchSaleSwitchRequest request,
        CancellationToken cancellationToken)
    {
        var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
        var (tenantId, _) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await EnsureStoreAccessibleAsync(storeId, cancellationToken);

        var action = NormalizeName(request.Action);
        var targetStatus = action switch
        {
            "on" => ProductStatus.OnSale,
            "off" => ProductStatus.OffShelf,
            _ => throw new BusinessException(ErrorCodes.BadRequest, "action 非法，仅支持 on/off")
        };

        var products = await BuildScopedProductQuery(tenantId, storeId, request.Scope).ToListAsync(cancellationToken);
        var successCount = 0;
        var skippedCount = 0;
        foreach (var product in products)
        {
            var changed = false;
            if (product.Status != targetStatus)
            {
                product.Status = targetStatus;
                changed = true;
            }

            if (product.SoldoutMode.HasValue || product.SoldoutReason is not null || product.RecoverAt.HasValue || product.RemainStock.HasValue)
            {
                product.SoldoutMode = null;
                product.SoldoutReason = null;
                product.RecoverAt = null;
                product.RemainStock = null;
                changed = true;
            }

            if (changed)
            {
                successCount++;
            }
            else
            {
                skippedCount++;
            }
        }

        if (successCount > 0)
        {
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        return ApiResponse<BatchToolResultResponse>.Ok(new BatchToolResultResponse
        {
            TotalCount = products.Count,
            SuccessCount = successCount,
            FailedCount = 0,
            SkippedCount = skippedCount
        });
    }

    /// <summary>
    /// 批量移动商品分类。
    /// </summary>
    /// <param name="request">移类请求。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>批量工具执行结果。</returns>
    [HttpPost("move-category")]
    [ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<BatchToolResultResponse>> MoveCategory(
        [FromBody] BatchMoveCategoryRequest request,
        CancellationToken cancellationToken)
    {
        var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
        var (tenantId, _) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await EnsureStoreAccessibleAsync(storeId, cancellationToken);

        var targetCategoryId = StoreApiHelpers.ParseRequiredSnowflake(request.TargetCategoryId, nameof(request.TargetCategoryId));
        var sourceCategoryId = StoreApiHelpers.ParseSnowflakeOrNull(request.SourceCategoryId);

        var categoryIdsToCheck = new List<long> { targetCategoryId };
        if (sourceCategoryId.HasValue)
        {
            categoryIdsToCheck.Add(sourceCategoryId.Value);
        }

        var categorySet = await dbContext.ProductCategories
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == storeId && categoryIdsToCheck.Contains(x.Id))
            .Select(x => x.Id)
            .ToListAsync(cancellationToken);

        if (!categorySet.Contains(targetCategoryId))
        {
            throw new BusinessException(ErrorCodes.BadRequest, "目标分类不存在");
        }

        if (sourceCategoryId.HasValue && !categorySet.Contains(sourceCategoryId.Value))
        {
            throw new BusinessException(ErrorCodes.BadRequest, "源分类不存在");
        }

        var scopedQuery = BuildScopedProductQuery(tenantId, storeId, request.Scope);
        if (sourceCategoryId.HasValue)
        {
            scopedQuery = scopedQuery.Where(x => x.CategoryId == sourceCategoryId.Value);
        }

        var products = await scopedQuery.ToListAsync(cancellationToken);
        var successCount = 0;
        var skippedCount = 0;
        foreach (var product in products)
        {
            if (product.CategoryId == targetCategoryId)
            {
                skippedCount++;
                continue;
            }

            product.CategoryId = targetCategoryId;
            successCount++;
        }

        if (successCount > 0)
        {
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        return ApiResponse<BatchToolResultResponse>.Ok(new BatchToolResultResponse
        {
            TotalCount = products.Count,
            SuccessCount = successCount,
            FailedCount = 0,
            SkippedCount = skippedCount
        });
    }

    /// <summary>
    /// 将源门店商品批量同步到目标门店。
    /// </summary>
    /// <param name="request">跨店同步请求。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>批量工具执行结果。</returns>
    [HttpPost("store-sync")]
    [ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<BatchToolResultResponse>> SyncStore(
        [FromBody] BatchSyncStoreRequest request,
        CancellationToken cancellationToken)
    {
        var sourceStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.SourceStoreId, nameof(request.SourceStoreId));
        var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await EnsureStoreAccessibleAsync(sourceStoreId, cancellationToken);

        var targetStoreIds = StoreApiHelpers.ParseSnowflakeList(request.TargetStoreIds)
            .Where(id => id != sourceStoreId)
            .Distinct()
            .ToList();
        if (targetStoreIds.Count == 0)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "targetStoreIds 不能为空");
        }

        var accessibleTargetStoreIds = await StoreApiHelpers.FilterAccessibleStoreIdsAsync(
            dbContext,
            tenantId,
            merchantId,
            targetStoreIds,
            cancellationToken);
        targetStoreIds = targetStoreIds.Where(accessibleTargetStoreIds.Contains).ToList();
        if (targetStoreIds.Count == 0)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "目标门店不可访问");
        }

        var productIds = StoreApiHelpers.ParseSnowflakeList(request.ProductIds);
        if (productIds.Count == 0)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "productIds 不能为空");
        }

        var sourceProducts = await dbContext.Products
            .Where(x =>
                x.TenantId == tenantId &&
                x.StoreId == sourceStoreId &&
                productIds.Contains(x.Id))
            .OrderBy(x => x.Name)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);

        if (sourceProducts.Count == 0)
        {
            return ApiResponse<BatchToolResultResponse>.Ok(new BatchToolResultResponse());
        }

        var sourceCategories = await dbContext.ProductCategories
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
            .ToDictionaryAsync(x => x.Id, x => x.Name, cancellationToken);

        var totalCount = sourceProducts.Count * targetStoreIds.Count;
        var successCount = 0;
        var failedCount = 0;
        var skippedCount = 0;

        foreach (var targetStoreId in targetStoreIds)
        {
            var targetCategories = await dbContext.ProductCategories
                .AsNoTracking()
                .Where(x => x.TenantId == tenantId && x.StoreId == targetStoreId)
                .OrderBy(x => x.SortOrder)
                .ThenBy(x => x.Id)
                .ToListAsync(cancellationToken);
            if (targetCategories.Count == 0)
            {
                failedCount += sourceProducts.Count;
                continue;
            }

            var targetCategoryByName = targetCategories
                .GroupBy(x => NormalizeName(x.Name))
                .ToDictionary(group => group.Key, group => group.First().Id);
            var fallbackCategoryId = targetCategories[0].Id;

            var sourceNames = sourceProducts.Select(x => x.Name).Distinct().ToList();
            var existingTargetProducts = await dbContext.Products
                .Where(x =>
                    x.TenantId == tenantId &&
                    x.StoreId == targetStoreId &&
                    sourceNames.Contains(x.Name))
                .ToListAsync(cancellationToken);
            var existingLookup = existingTargetProducts
                .GroupBy(x => BuildSyncProductKey(x.Name, x.Kind))
                .ToDictionary(group => group.Key, group => group.First());

            foreach (var source in sourceProducts)
            {
                var sourceCategoryName = sourceCategories.GetValueOrDefault(source.CategoryId, string.Empty);
                var targetCategoryId = targetCategoryByName.GetValueOrDefault(NormalizeName(sourceCategoryName), fallbackCategoryId);
                if (targetCategoryId <= 0)
                {
                    failedCount++;
                    continue;
                }

                var key = BuildSyncProductKey(source.Name, source.Kind);
                if (existingLookup.TryGetValue(key, out var existing))
                {
                    var changed = false;
                    if (existing.CategoryId != targetCategoryId)
                    {
                        existing.CategoryId = targetCategoryId;
                        changed = true;
                    }

                    if (request.SyncPrice)
                    {
                        if (existing.Price != source.Price)
                        {
                            existing.Price = source.Price;
                            changed = true;
                        }

                        if (existing.OriginalPrice != source.OriginalPrice)
                        {
                            existing.OriginalPrice = source.OriginalPrice;
                            changed = true;
                        }
                    }

                    if (request.SyncStock && existing.StockQuantity != source.StockQuantity)
                    {
                        existing.StockQuantity = source.StockQuantity;
                        changed = true;
                    }

                    if (request.SyncStatus)
                    {
                        if (existing.Status != source.Status)
                        {
                            existing.Status = source.Status;
                            changed = true;
                        }

                        if (existing.SoldoutMode != source.SoldoutMode ||
                            existing.SoldoutReason != source.SoldoutReason ||
                            existing.RecoverAt != source.RecoverAt ||
                            existing.RemainStock != source.RemainStock)
                        {
                            existing.SoldoutMode = source.SoldoutMode;
                            existing.SoldoutReason = source.SoldoutReason;
                            existing.RecoverAt = source.RecoverAt;
                            existing.RemainStock = source.RemainStock;
                            changed = true;
                        }
                    }

                    if (changed)
                    {
                        successCount++;
                    }
                    else
                    {
                        skippedCount++;
                    }
                }
                else
                {
                    var nextId = idGenerator.NextId();
                    var copied = new Product
                    {
                        Id = nextId,
                        TenantId = tenantId,
                        StoreId = targetStoreId,
                        CategoryId = targetCategoryId,
                        SpuCode = GenerateSpuCode(nextId),
                        Name = source.Name,
                        Subtitle = source.Subtitle,
                        Unit = source.Unit,
                        Price = source.Price,
                        OriginalPrice = source.OriginalPrice,
                        StockQuantity = source.StockQuantity,
                        MaxQuantityPerOrder = source.MaxQuantityPerOrder,
                        Status = request.SyncStatus ? source.Status : ProductStatus.OffShelf,
                        Kind = source.Kind,
                        SalesMonthly = source.SalesMonthly,
                        TagsJson = source.TagsJson,
                        SoldoutMode = request.SyncStatus ? source.SoldoutMode : null,
                        RecoverAt = request.SyncStatus ? source.RecoverAt : null,
                        RemainStock = request.SyncStatus ? source.RemainStock : null,
                        SoldoutReason = request.SyncStatus ? source.SoldoutReason : null,
                        SyncToPlatform = source.SyncToPlatform,
                        NotifyManager = source.NotifyManager,
                        TimedOnShelfAt = source.TimedOnShelfAt,
                        CoverImage = source.CoverImage,
                        GalleryImages = source.GalleryImages,
                        Description = source.Description,
                        SortWeight = source.SortWeight,
                        WarningStock = source.WarningStock,
                        PackingFee = source.PackingFee,
                        EnableDineIn = source.EnableDineIn,
                        EnablePickup = source.EnablePickup,
                        EnableDelivery = source.EnableDelivery,
                        IsFeatured = source.IsFeatured
                    };

                    dbContext.Products.Add(copied);
                    existingLookup[key] = copied;
                    successCount++;
                }
            }
        }

        if (successCount > 0)
        {
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        return ApiResponse<BatchToolResultResponse>.Ok(new BatchToolResultResponse
        {
            TotalCount = totalCount,
            SuccessCount = successCount,
            FailedCount = failedCount,
            SkippedCount = skippedCount
        });
    }

    /// <summary>
    /// 下载商品批量导入模板。
    /// </summary>
    /// <param name="storeId">门店 ID。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>导入模板文件。</returns>
    [HttpGet("import/template")]
    [ProducesResponseType(typeof(ApiResponse<BatchExcelFileResponse>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<BatchExcelFileResponse>> DownloadImportTemplate(
        [FromQuery] string storeId,
        CancellationToken cancellationToken)
    {
        var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, nameof(storeId));
        await EnsureStoreAccessibleAsync(parsedStoreId, cancellationToken);

        using var workbook = new XLWorkbook();
        var sheet = workbook.Worksheets.Add("商品导入模板");
        for (var index = 0; index < ImportHeaders.Length; index++)
        {
            var cell = sheet.Cell(1, index + 1);
            cell.Value = ImportHeaders[index];
            cell.Style.Font.Bold = true;
            cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#F5F7FA");
        }

        sheet.Cell(2, 1).Value = "SPU202602260001";
        sheet.Cell(2, 2).Value = "经典宫保鸡丁";
        sheet.Cell(2, 3).Value = "下饭推荐";
        sheet.Cell(2, 4).Value = "热销菜品";
        sheet.Cell(2, 5).Value = "单品";
        sheet.Cell(2, 6).Value = 28.00m;
        sheet.Cell(2, 7).Value = 32.00m;
        sheet.Cell(2, 8).Value = 99;
        sheet.Cell(2, 9).Value = "上架";
        sheet.Columns(1, ImportHeaders.Length).AdjustToContents();

        return ApiResponse<BatchExcelFileResponse>.Ok(new BatchExcelFileResponse
        {
            FileName = $"商品导入模板_{DateTime.UtcNow:yyyyMMddHHmmss}.xlsx",
            FileContentBase64 = EncodeWorkbookToBase64(workbook),
            TotalCount = 0,
            SuccessCount = 0,
            FailedCount = 0
        });
    }

    /// <summary>
    /// 批量导入商品。
    /// </summary>
    /// <param name="request">导入请求。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>导入结果。</returns>
    [HttpPost("import")]
    [Consumes("multipart/form-data")]
    [ProducesResponseType(typeof(ApiResponse<BatchImportResultResponse>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<BatchImportResultResponse>> Import(
        [FromForm] BatchImportRequest request,
        CancellationToken cancellationToken)
    {
        var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
        var (tenantId, _) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await EnsureStoreAccessibleAsync(storeId, cancellationToken);

        if (request.File is null || request.File.Length <= 0)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "请上传 Excel 文件");
        }

        if (request.File.Length > MaxImportFileSize)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "导入文件不能超过 10MB");
        }

        var extension = Path.GetExtension(request.File.FileName);
        if (!string.Equals(extension, ".xlsx", StringComparison.OrdinalIgnoreCase))
        {
            throw new BusinessException(ErrorCodes.BadRequest, "仅支持 .xlsx 文件");
        }

        using var stream = request.File.OpenReadStream();
        using var workbook = new XLWorkbook(stream);
        var sheet = workbook.Worksheets.FirstOrDefault();
        if (sheet is null)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "导入文件为空");
        }

        for (var index = 0; index < ImportHeaders.Length; index++)
        {
            var actual = NormalizeName(ReadCellText(sheet.Cell(1, index + 1)));
            var expected = NormalizeName(ImportHeaders[index]);
            if (!string.Equals(actual, expected, StringComparison.Ordinal))
            {
                throw new BusinessException(ErrorCodes.BadRequest, $"导入模板不正确，第 {index + 1} 列应为 {ImportHeaders[index]}");
            }
        }

        var categories = await dbContext.ProductCategories
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == storeId)
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);
        if (categories.Count == 0)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "当前门店没有商品分类，无法导入");
        }

        var categoryLookup = categories
            .GroupBy(x => NormalizeName(x.Name))
            .ToDictionary(group => group.Key, group => group.First().Id);

        var lastRow = sheet.LastRowUsed()?.RowNumber() ?? 1;
        var importRows = new List<ImportRowInput>();
        var errors = new List<BatchImportErrorItemResponse>();
        var seenSpuCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var totalCount = 0;

        for (var rowNo = 2; rowNo <= lastRow; rowNo++)
        {
            var row = sheet.Row(rowNo);
            var spuCodeRaw = ReadCellText(row.Cell(1));
            var nameRaw = ReadCellText(row.Cell(2));
            var subtitleRaw = ReadCellText(row.Cell(3));
            var categoryNameRaw = ReadCellText(row.Cell(4));
            var kindRaw = ReadCellText(row.Cell(5));
            var priceRaw = ReadCellText(row.Cell(6));
            var originalPriceRaw = ReadCellText(row.Cell(7));
            var stockRaw = ReadCellText(row.Cell(8));
            var statusRaw = ReadCellText(row.Cell(9));

            if (string.IsNullOrWhiteSpace(spuCodeRaw) &&
                string.IsNullOrWhiteSpace(nameRaw) &&
                string.IsNullOrWhiteSpace(subtitleRaw) &&
                string.IsNullOrWhiteSpace(categoryNameRaw) &&
                string.IsNullOrWhiteSpace(kindRaw) &&
                string.IsNullOrWhiteSpace(priceRaw) &&
                string.IsNullOrWhiteSpace(originalPriceRaw) &&
                string.IsNullOrWhiteSpace(stockRaw) &&
                string.IsNullOrWhiteSpace(statusRaw))
            {
                continue;
            }

            totalCount++;
            var rowErrors = new List<string>();

            var name = (nameRaw ?? string.Empty).Trim();
            if (string.IsNullOrWhiteSpace(name))
            {
                rowErrors.Add("商品名称不能为空");
            }
            else if (name.Length > 128)
            {
                rowErrors.Add("商品名称长度不能超过 128");
            }

            var subtitle = TrimOrNull(subtitleRaw);
            if (!string.IsNullOrWhiteSpace(subtitle) && subtitle.Length > 256)
            {
                rowErrors.Add("副标题长度不能超过 256");
            }

            var categoryKey = NormalizeName(categoryNameRaw);
            if (!categoryLookup.TryGetValue(categoryKey, out var categoryId))
            {
                rowErrors.Add("分类不存在");
            }

            var kind = ProductKind.Single;
            try
            {
                kind = ParseImportKind(kindRaw);
            }
            catch (BusinessException ex)
            {
                rowErrors.Add(ex.Message);
            }

            if (!TryParseDecimal(priceRaw, out var price) || price < 0)
            {
                rowErrors.Add("售价必须为大于等于 0 的数字");
            }
            else
            {
                price = decimal.Round(price, 2, MidpointRounding.AwayFromZero);
            }

            decimal? originalPrice = null;
            if (!string.IsNullOrWhiteSpace(originalPriceRaw))
            {
                if (!TryParseDecimal(originalPriceRaw, out var parsedOriginalPrice) || parsedOriginalPrice < 0)
                {
                    rowErrors.Add("划线价必须为大于等于 0 的数字");
                }
                else
                {
                    originalPrice = decimal.Round(parsedOriginalPrice, 2, MidpointRounding.AwayFromZero);
                }
            }

            if (!TryParseInt(stockRaw, out var stock) || stock < 0)
            {
                rowErrors.Add("库存必须为大于等于 0 的整数");
            }

            var status = ProductStatus.OffShelf;
            try
            {
                status = ParseImportStatus(statusRaw);
            }
            catch (BusinessException ex)
            {
                rowErrors.Add(ex.Message);
            }

            var spuCode = TrimOrNull(spuCodeRaw);
            if (string.IsNullOrWhiteSpace(spuCode))
            {
                spuCode = GenerateSpuCode(idGenerator.NextId());
            }
            else if (spuCode.Length > 32)
            {
                rowErrors.Add("SPU编码长度不能超过 32");
            }

            if (!string.IsNullOrWhiteSpace(spuCode) && !seenSpuCodes.Add(spuCode))
            {
                rowErrors.Add("导入文件内 SPU 编码重复");
            }

            if (rowErrors.Count > 0)
            {
                errors.Add(new BatchImportErrorItemResponse
                {
                    RowNo = rowNo,
                    Message = string.Join("；", rowErrors)
                });
                continue;
            }

            importRows.Add(new ImportRowInput(
                rowNo,
                spuCode!,
                name,
                subtitle,
                categoryId,
                kind,
                price,
                originalPrice,
                stock,
                status));
        }

        var skippedCount = 0;
        var successCount = 0;

        if (importRows.Count > 0)
        {
            var spuCodes = importRows.Select(x => x.SpuCode).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
            var existingBySpu = await dbContext.Products
                .Where(x => x.TenantId == tenantId && spuCodes.Contains(x.SpuCode))
                .ToDictionaryAsync(x => x.SpuCode, x => x, StringComparer.OrdinalIgnoreCase, cancellationToken);

            foreach (var row in importRows)
            {
                if (existingBySpu.TryGetValue(row.SpuCode, out var existing))
                {
                    if (existing.StoreId != storeId)
                    {
                        errors.Add(new BatchImportErrorItemResponse
                        {
                            RowNo = row.RowNo,
                            Message = "SPU编码已被其他门店占用"
                        });
                        continue;
                    }

                    var changed = false;
                    if (existing.Name != row.Name)
                    {
                        existing.Name = row.Name;
                        changed = true;
                    }

                    if (existing.Subtitle != row.Subtitle)
                    {
                        existing.Subtitle = row.Subtitle;
                        changed = true;
                    }

                    if (existing.CategoryId != row.CategoryId)
                    {
                        existing.CategoryId = row.CategoryId;
                        changed = true;
                    }

                    if (existing.Kind != row.Kind)
                    {
                        existing.Kind = row.Kind;
                        changed = true;
                    }

                    if (existing.Price != row.Price)
                    {
                        existing.Price = row.Price;
                        changed = true;
                    }

                    if (existing.OriginalPrice != row.OriginalPrice)
                    {
                        existing.OriginalPrice = row.OriginalPrice;
                        changed = true;
                    }

                    if (existing.StockQuantity != row.Stock)
                    {
                        existing.StockQuantity = row.Stock;
                        changed = true;
                    }

                    if (existing.Status != row.Status)
                    {
                        existing.Status = row.Status;
                        changed = true;
                    }

                    if (existing.SoldoutMode.HasValue || existing.SoldoutReason is not null || existing.RecoverAt.HasValue || existing.RemainStock.HasValue)
                    {
                        existing.SoldoutMode = null;
                        existing.SoldoutReason = null;
                        existing.RecoverAt = null;
                        existing.RemainStock = null;
                        changed = true;
                    }

                    if (changed)
                    {
                        successCount++;
                    }
                    else
                    {
                        skippedCount++;
                    }
                }
                else
                {
                    var nextId = idGenerator.NextId();
                    dbContext.Products.Add(new Product
                    {
                        Id = nextId,
                        TenantId = tenantId,
                        StoreId = storeId,
                        CategoryId = row.CategoryId,
                        SpuCode = row.SpuCode,
                        Name = row.Name,
                        Subtitle = row.Subtitle,
                        Price = row.Price,
                        OriginalPrice = row.OriginalPrice,
                        StockQuantity = row.Stock,
                        Status = row.Status,
                        Kind = row.Kind,
                        SalesMonthly = 0,
                        SyncToPlatform = true,
                        NotifyManager = false,
                        SortWeight = 0,
                        EnableDineIn = true,
                        EnablePickup = true,
                        EnableDelivery = true
                    });
                    successCount++;
                }
            }

            if (successCount > 0)
            {
                await dbContext.SaveChangesAsync(cancellationToken);
            }
        }

        return ApiResponse<BatchImportResultResponse>.Ok(new BatchImportResultResponse
        {
            FileName = request.File.FileName,
            TotalCount = totalCount,
            SuccessCount = successCount,
            FailedCount = errors.Count,
            SkippedCount = skippedCount,
            Errors = errors
        });
    }

    /// <summary>
    /// 按范围导出商品 Excel。
    /// </summary>
    /// <param name="request">导出请求。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>导出文件。</returns>
    [HttpPost("export")]
    [ProducesResponseType(typeof(ApiResponse<BatchExcelFileResponse>), StatusCodes.Status200OK)]
    public async Task<ApiResponse<BatchExcelFileResponse>> Export(
        [FromBody] BatchExportRequest request,
        CancellationToken cancellationToken)
    {
        var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
        var (tenantId, _) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await EnsureStoreAccessibleAsync(storeId, cancellationToken);

        var products = await BuildScopedProductQuery(tenantId, storeId, request.Scope)
            .OrderBy(x => x.Name)
            .ThenBy(x => x.Id)
            .ToListAsync(cancellationToken);

        var categoryLookup = await dbContext.ProductCategories
            .AsNoTracking()
            .Where(x => x.TenantId == tenantId && x.StoreId == storeId)
            .ToDictionaryAsync(x => x.Id, x => x.Name, cancellationToken);

        using var workbook = new XLWorkbook();
        var sheet = workbook.Worksheets.Add("商品导出");
        for (var index = 0; index < ImportHeaders.Length; index++)
        {
            var cell = sheet.Cell(1, index + 1);
            cell.Value = ImportHeaders[index];
            cell.Style.Font.Bold = true;
            cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#F5F7FA");
        }

        for (var rowIndex = 0; rowIndex < products.Count; rowIndex++)
        {
            var product = products[rowIndex];
            var rowNo = rowIndex + 2;
            sheet.Cell(rowNo, 1).Value = product.SpuCode;
            sheet.Cell(rowNo, 2).Value = product.Name;
            sheet.Cell(rowNo, 3).Value = product.Subtitle ?? string.Empty;
            sheet.Cell(rowNo, 4).Value = categoryLookup.GetValueOrDefault(product.CategoryId, string.Empty);
            sheet.Cell(rowNo, 5).Value = ToImportKindText(product.Kind);
            sheet.Cell(rowNo, 6).Value = decimal.Round(product.Price, 2, MidpointRounding.AwayFromZero);
            sheet.Cell(rowNo, 7).Value = product.OriginalPrice.HasValue
                ? decimal.Round(product.OriginalPrice.Value, 2, MidpointRounding.AwayFromZero)
                : string.Empty;
            sheet.Cell(rowNo, 8).Value = Math.Max(0, product.StockQuantity ?? 0);
            sheet.Cell(rowNo, 9).Value = ToImportStatusText(product.Status);
        }

        sheet.Columns(1, ImportHeaders.Length).AdjustToContents();

        return ApiResponse<BatchExcelFileResponse>.Ok(new BatchExcelFileResponse
        {
            FileName = $"商品导出_{DateTime.UtcNow:yyyyMMddHHmmss}.xlsx",
            FileContentBase64 = EncodeWorkbookToBase64(workbook),
            TotalCount = products.Count,
            SuccessCount = products.Count,
            FailedCount = 0
        });
    }

    private async Task EnsureStoreAccessibleAsync(long storeId, CancellationToken cancellationToken)
    {
        var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
        await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, storeId, cancellationToken);
    }

    private IQueryable<Product> BuildScopedProductQuery(long tenantId, long storeId, ProductBatchScopeRequest? scope)
    {
        var query = dbContext.Products
            .Where(x => x.TenantId == tenantId && x.StoreId == storeId);

        if (scope is null)
        {
            return query;
        }

        var type = NormalizeName(scope.Type);
        if (string.IsNullOrWhiteSpace(type) || type == "all")
        {
            return query;
        }

        if (type == "category")
        {
            var rawCategoryIds = new List<string>(scope.CategoryIds);
            if (!string.IsNullOrWhiteSpace(scope.CategoryId))
            {
                rawCategoryIds.Add(scope.CategoryId);
            }

            var categoryIds = StoreApiHelpers.ParseSnowflakeList(rawCategoryIds);
            if (categoryIds.Count == 0)
            {
                throw new BusinessException(ErrorCodes.BadRequest, "按分类范围必须至少选择一个分类");
            }

            return query.Where(x => categoryIds.Contains(x.CategoryId));
        }

        if (type is "selected" or "manual")
        {
            var productIds = StoreApiHelpers.ParseSnowflakeList(scope.ProductIds);
            if (productIds.Count == 0)
            {
                throw new BusinessException(ErrorCodes.BadRequest, "手动范围必须至少选择一个商品");
            }

            return query.Where(x => productIds.Contains(x.Id));
        }

        throw new BusinessException(ErrorCodes.BadRequest, "scope.type 非法");
    }

    private static void ValidatePriceAdjustRequest(string? direction, string? amountType, decimal amount)
    {
        var normalizedDirection = NormalizeName(direction);
        if (normalizedDirection is not ("up" or "down"))
        {
            throw new BusinessException(ErrorCodes.BadRequest, "direction 非法，仅支持 up/down");
        }

        var normalizedAmountType = NormalizeName(amountType);
        if (normalizedAmountType is not ("fixed" or "percent"))
        {
            throw new BusinessException(ErrorCodes.BadRequest, "amountType 非法，仅支持 fixed/percent");
        }

        if (amount < 0)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "amount 不能小于 0");
        }

        if (normalizedAmountType == "percent" && amount > 100)
        {
            throw new BusinessException(ErrorCodes.BadRequest, "百分比调价 amount 不能大于 100");
        }
    }

    private static decimal CalculateAdjustedPrice(decimal currentPrice, string? direction, string? amountType, decimal amount)
    {
        var normalizedDirection = NormalizeName(direction);
        var normalizedAmountType = NormalizeName(amountType);

        decimal delta;
        if (normalizedAmountType == "percent")
        {
            delta = decimal.Round(currentPrice * (amount / 100m), 2, MidpointRounding.AwayFromZero);
        }
        else
        {
            delta = decimal.Round(amount, 2, MidpointRounding.AwayFromZero);
        }

        var next = normalizedDirection == "down"
            ? currentPrice - delta
            : currentPrice + delta;

        if (next < 0)
        {
            next = 0;
        }

        return decimal.Round(next, 2, MidpointRounding.AwayFromZero);
    }

    private static ProductKind ParseImportKind(string? value)
    {
        var normalized = NormalizeName(value);
        return normalized switch
        {
            "" => ProductKind.Single,
            "single" => ProductKind.Single,
            "单品" => ProductKind.Single,
            "0" => ProductKind.Single,
            "combo" => ProductKind.Combo,
            "套餐" => ProductKind.Combo,
            "1" => ProductKind.Combo,
            _ => throw new BusinessException(ErrorCodes.BadRequest, "商品类型仅支持 单品/套餐")
        };
    }

    private static ProductStatus ParseImportStatus(string? value)
    {
        var normalized = NormalizeName(value);
        return normalized switch
        {
            "" => ProductStatus.OffShelf,
            "onsale" => ProductStatus.OnSale,
            "on_sale" => ProductStatus.OnSale,
            "on" => ProductStatus.OnSale,
            "上架" => ProductStatus.OnSale,
            "在售" => ProductStatus.OnSale,
            "1" => ProductStatus.OnSale,
            "offshelf" => ProductStatus.OffShelf,
            "off_shelf" => ProductStatus.OffShelf,
            "off" => ProductStatus.OffShelf,
            "下架" => ProductStatus.OffShelf,
            "停售" => ProductStatus.OffShelf,
            "2" => ProductStatus.OffShelf,
            "draft" => ProductStatus.Draft,
            "草稿" => ProductStatus.Draft,
            "0" => ProductStatus.Draft,
            _ => throw new BusinessException(ErrorCodes.BadRequest, "状态仅支持 上架/下架/草稿")
        };
    }

    private static string ToImportKindText(ProductKind kind)
    {
        return kind switch
        {
            ProductKind.Combo => "套餐",
            _ => "单品"
        };
    }

    private static string ToImportStatusText(ProductStatus status)
    {
        return status switch
        {
            ProductStatus.OnSale => "上架",
            ProductStatus.Draft => "草稿",
            _ => "下架"
        };
    }

    private static string ReadCellText(IXLCell cell)
    {
        return cell.GetFormattedString().Trim();
    }

    private static bool TryParseDecimal(string? input, out decimal value)
    {
        var text = (input ?? string.Empty).Trim();
        if (string.IsNullOrWhiteSpace(text))
        {
            value = 0m;
            return false;
        }

        if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out value))
        {
            return true;
        }

        return decimal.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out value);
    }

    private static bool TryParseInt(string? input, out int value)
    {
        var text = (input ?? string.Empty).Trim();
        if (string.IsNullOrWhiteSpace(text))
        {
            value = 0;
            return false;
        }

        if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
        {
            return true;
        }

        return int.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out value);
    }

    private static string? TrimOrNull(string? value)
    {
        var trimmed = (value ?? string.Empty).Trim();
        return string.IsNullOrWhiteSpace(trimmed) ? null : trimmed;
    }

    private static string NormalizeName(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        return string.Concat(value.Trim().ToLowerInvariant().Where(ch => !char.IsWhiteSpace(ch)));
    }

    private static string BuildSyncProductKey(string? productName, ProductKind kind)
    {
        return $"{NormalizeName(productName)}|{(int)kind}";
    }

    private static string GenerateSpuCode(long seed)
    {
        return $"SPU{DateTime.UtcNow:yyyyMMddHHmmss}{Math.Abs(seed % 10_000):D4}";
    }

    private static string EncodeWorkbookToBase64(XLWorkbook workbook)
    {
        using var stream = new MemoryStream();
        workbook.SaveAs(stream);
        return Convert.ToBase64String(stream.ToArray());
    }

    private sealed record ImportRowInput(
        int RowNo,
        string SpuCode,
        string Name,
        string? Subtitle,
        long CategoryId,
        ProductKind Kind,
        decimal Price,
        decimal? OriginalPrice,
        int Stock,
        ProductStatus Status);
}
