package com.jeesharp.business.modules.food.application;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.jeesharp.business.modules.common.support.QueryInCsvSupport;
import com.jeesharp.business.modules.food.convert.FoodMealMenuConvert;
import com.jeesharp.business.modules.food.convert.FoodMealMenuItemConvert;
import com.jeesharp.business.modules.food.dto.FoodMealMenuBatchSaveDTO;
import com.jeesharp.business.modules.food.dto.FoodMealMenuBatchTemplateDTO;
import com.jeesharp.business.modules.food.dto.FoodMealMenuCopyDTO;
import com.jeesharp.business.modules.food.dto.FoodMealMenuItemSaveDTO;
import com.jeesharp.business.modules.food.dto.FoodMealMenuItemSpecSaveDTO;
import com.jeesharp.business.modules.food.dto.FoodMealMenuSaveDTO;
import com.jeesharp.business.modules.food.entity.FoodDish;
import com.jeesharp.business.modules.food.entity.FoodMealMenu;
import com.jeesharp.business.modules.food.entity.FoodMealMenuItem;
import com.jeesharp.business.modules.food.entity.FoodMealMenuItemSpec;
import com.jeesharp.business.modules.food.entity.FoodMealMenuTemplate;
import com.jeesharp.business.modules.food.entity.FoodMealMenuTemplateItem;
import com.jeesharp.business.modules.food.entity.FoodMealMenuTemplateItemSpec;
import com.jeesharp.business.modules.food.entity.FoodMealServiceArea;
import com.jeesharp.business.modules.food.entity.FoodPackage;
import com.jeesharp.business.modules.food.service.*;
import com.jeesharp.business.modules.food.vo.FoodMealMenuBatchSaveResultVO;
import com.jeesharp.business.modules.food.vo.FoodMealMenuCopyResultVO;
import com.jeesharp.business.modules.food.vo.FoodMealMenuInfoVO;
import com.jeesharp.business.modules.food.vo.FoodMealMenuItemListVO;
import com.jeesharp.business.modules.food.vo.FoodMealMenuListVO;
import com.jeesharp.business.modules.food.vo.FoodMealMenuQueryVO;
import com.jeesharp.framework.exception.BizErrorException;
import com.jeesharp.framework.pojo.Page;
import com.jeesharp.framework.pojo.query.PageRespVO;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

@Slf4j
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class FoodMealMenuApplicationService {

    private static final Set<String> MEAL_TYPES = Set.of("breakfast", "lunch", "dinner", "snack");
    private static final Set<String> SCOPE_TYPES = Set.of("public", "company", "all");
    private static final Set<String> PUBLISH_STATUSES = Set.of("draft", "published", "offline");

    private final FoodMealMenuService foodMealMenuService;
    private final FoodMealMenuItemService foodMealMenuItemService;
    private final FoodMealMenuItemSpecService foodMealMenuItemSpecService;
    private final FoodMealServiceAreaService foodMealServiceAreaService;
    private final FoodMealMenuTemplateService foodMealMenuTemplateService;
    private final FoodMealMenuTemplateItemService foodMealMenuTemplateItemService;
    private final FoodMealMenuTemplateItemSpecService foodMealMenuTemplateItemSpecService;
    private final FoodRestaurantService foodRestaurantService;
    private final FoodDishService foodDishService;
    private final FoodPackageService foodPackageService;

    public FoodMealMenuInfoVO get(String id) {
        FoodMealMenu entity = foodMealMenuService.get(id);
        if (entity == null) {
            return null;
        }
        FoodMealMenuInfoVO vo = FoodMealMenuConvert.INSTANCE.toInfoVO(entity);
        List<FoodMealMenuItem> items = foodMealMenuItemService.listByMenuId(id);
        List<FoodMealMenuItemListVO> itemVOList = FoodMealMenuItemConvert.INSTANCE.toListVO(items);
        enrichMenuItemDetails(itemVOList, items);
        vo.setItems(itemVOList);
        enrichInfoDisplay(vo);
        return vo;
    }

    public PageRespVO<FoodMealMenuListVO> findPage(Page<FoodMealMenu> page, FoodMealMenuQueryVO query) {
        log.info("[菜单查询] 参数: serviceAreaId={}, startDate={}, endDate={}, scopeType={}, companyId={}, publishStatus={}, dataScope={}",
            query.getServiceAreaId(), query.getStartDate(), query.getEndDate(),
            query.getScopeType(), query.getCompanyId(), query.getPublishStatus(), query.getDataScope());
        FoodMealMenuQueryVO prepared = prepareQuery(query);
        if (prepared == null) {
            log.warn("[菜单查询] prepareQuery 返回 null（非法 publishStatus={}）", query.getPublishStatus());
            return emptyPage(page);
        }
        Page<FoodMealMenu> result = foodMealMenuService.findPage(page, prepared);
        log.info("[菜单查询] 结果: total={}, recordsSize={}", result.getTotal(), result.getRecords() != null ? result.getRecords().size() : 0);
        PageRespVO<FoodMealMenuListVO> pageVo = FoodMealMenuConvert.INSTANCE.toPageRespVO(result);
        enrichListDisplay(pageVo.getRecords());
        return pageVo;
    }

    @Transactional(readOnly = false)
    public void save(FoodMealMenuSaveDTO dto) {
        save(dto, false);
    }

    @Transactional(readOnly = false)
    public void save(FoodMealMenuSaveDTO dto, boolean skipConflictCheck) {
        FoodMealServiceArea serviceArea = validateMenu(dto, skipConflictCheck);
        FoodMealMenuTemplate template = foodMealMenuTemplateService.getEnabled(dto.getTemplateId());
        FoodMealMenu entity = FoodMealMenuConvert.INSTANCE.toEntity(dto);
        String areaId = StrUtil.isNotBlank(dto.getServiceAreaId()) ? StrUtil.trim(dto.getServiceAreaId()) : StrUtil.trim(dto.getRestaurantId());
        entity.setServiceAreaId(areaId);
        entity.setRestaurantId(StrUtil.blankToDefault(StrUtil.trim(dto.getRestaurantId()), areaId));
        entity.setServiceAreaName(serviceArea.getAreaName());
        entity.setTemplateName(template.getTemplateName());
        entity.setCompanyId(companyIdForSave(dto));
        entity.setCompanyName("company".equals(dto.getScopeType()) ? StrUtil.trimToNull(dto.getCompanyName()) : null);
        if (StrUtil.isBlank(entity.getPublishStatus())) {
            entity.setPublishStatus("draft");
        }
        if ("published".equals(entity.getPublishStatus()) && entity.getPublishTime() == null) {
            entity.setPublishTime(LocalDateTime.now());
        }
        if (StrUtil.isBlank(dto.getId())) {
            foodMealMenuService.createMenu(entity);
        } else {
            foodMealMenuService.updateMenu(entity);
        }
        saveItems(entity, buildItemsFromTemplateIfEmpty(dto));
    }

    @Transactional(readOnly = false)
    public FoodMealMenuBatchSaveResultVO batchSave(FoodMealMenuBatchSaveDTO dto) {
        validateBatch(dto);
        List<LocalDate> dates = batchDates(dto);
        Map<String, List<String>> templateMap = batchTemplateMap(dto.getTemplates());
        String companyId = companyIdForBatch(dto);
        int createdCount = 0;
        int overwrittenCount = 0;
        for (LocalDate menuDate : dates) {
            for (String serviceAreaId : dto.getServiceAreaIds()) {
                String areaId = StrUtil.trim(serviceAreaId);
                for (String mealType : dto.getMealTypes()) {
                    String meal = StrUtil.trim(mealType);
                    List<String> templateIds = templateMap.getOrDefault(batchTemplateKey(areaId, meal), Collections.emptyList());
                    if (CollUtil.isEmpty(templateIds)) {
                        throw new BizErrorException("请完整配置每个服务区域和餐别的菜单模板");
                    }
                    List<String> existingIds = foodMealMenuService.listIdsBySameScope(areaId, menuDate, meal, dto.getScopeType(), companyId);
                    if (CollUtil.isNotEmpty(existingIds) && !Boolean.TRUE.equals(dto.getOverwriteExisting())) {
                        throw new BizErrorException("存在已配置菜单，请确认覆盖风险后再提交");
                    }
                    if (CollUtil.isNotEmpty(existingIds)) {
                        for (String existingId : existingIds) {
                            foodMealMenuItemSpecService.deleteByMenuId(existingId);
                            foodMealMenuItemService.deleteByMenuId(existingId);
                        }
                        foodMealMenuService.batchDelete(existingIds);
                        overwrittenCount += existingIds.size();
                    }
                    for (String templateId : templateIds) {
                        FoodMealMenuSaveDTO saveDTO = new FoodMealMenuSaveDTO();
                        saveDTO.setRestaurantId(areaId);
                        saveDTO.setServiceAreaId(areaId);
                        saveDTO.setTemplateId(templateId);
                        saveDTO.setMenuDate(menuDate);
                        saveDTO.setMealType(meal);
                        saveDTO.setScopeType(dto.getScopeType());
                        saveDTO.setCompanyId(companyId);
                        saveDTO.setCompanyName(dto.getCompanyName());
                        saveDTO.setPublishStatus(StrUtil.blankToDefault(dto.getPublishStatus(), "draft"));
                        saveDTO.setSortNo(dto.getSortNo() == null ? 0 : dto.getSortNo());
                        save(saveDTO, true);
                        createdCount++;
                    }
                }
            }
        }
        FoodMealMenuBatchSaveResultVO result = new FoodMealMenuBatchSaveResultVO();
        result.setTargetCount(dates.size() * dto.getTemplates().size());
        result.setCreatedCount(createdCount);
        result.setOverwrittenCount(overwrittenCount);
        return result;
    }

    @Transactional(readOnly = false)
    public FoodMealMenuCopyResultVO copy(FoodMealMenuCopyDTO dto) {
        String copyMode = StrUtil.trim(dto.getCopyMode());
        if (!Set.of("day", "week", "month").contains(copyMode)) {
            throw new BizErrorException("复制模式不正确");
        }
        if (dto.getSourceStartDate().isAfter(dto.getSourceEndDate())) {
            throw new BizErrorException("源开始日期不能晚于源结束日期");
        }
        List<FoodMealMenu> sourceMenus = foodMealMenuService.listByDateRange(
            dto.getSourceStartDate(), dto.getSourceEndDate(), dto.getServiceAreaIds());
        if (CollUtil.isEmpty(sourceMenus)) {
            throw new BizErrorException("源日期范围内没有菜单可复制");
        }
        int createdCount = 0;
        int overwrittenCount = 0;
        for (LocalDate targetStart : dto.getTargetStartDates()) {
            long offsetDays = ChronoUnit.DAYS.between(dto.getSourceStartDate(), targetStart);
            for (FoodMealMenu sourceMenu : sourceMenus) {
                LocalDate targetDate = sourceMenu.getMenuDate().plusDays(offsetDays);
                List<String> existingIds = foodMealMenuService.listIdsBySameScope(
                    sourceMenu.getServiceAreaId(), targetDate, sourceMenu.getMealType(),
                    sourceMenu.getScopeType(), sourceMenu.getCompanyId() == null ? "" : sourceMenu.getCompanyId());
                if (CollUtil.isNotEmpty(existingIds)) {
                    if (!Boolean.TRUE.equals(dto.getOverwriteExisting())) {
                        throw new BizErrorException("目标日期存在已配置菜单，请确认覆盖后再提交");
                    }
                    for (String existingId : existingIds) {
                        foodMealMenuItemSpecService.deleteByMenuId(existingId);
                        foodMealMenuItemService.deleteByMenuId(existingId);
                    }
                    foodMealMenuService.batchDelete(existingIds);
                    overwrittenCount += existingIds.size();
                }
                FoodMealMenu newMenu = new FoodMealMenu();
                newMenu.setServiceAreaId(sourceMenu.getServiceAreaId());
                newMenu.setServiceAreaName(sourceMenu.getServiceAreaName());
                newMenu.setTemplateId(sourceMenu.getTemplateId());
                newMenu.setTemplateName(sourceMenu.getTemplateName());
                newMenu.setRestaurantId(sourceMenu.getRestaurantId());
                newMenu.setMenuDate(targetDate);
                newMenu.setMealType(sourceMenu.getMealType());
                newMenu.setScopeType(sourceMenu.getScopeType());
                newMenu.setCompanyId(sourceMenu.getCompanyId());
                newMenu.setCompanyName(sourceMenu.getCompanyName());
                newMenu.setPublishStatus("draft");
                newMenu.setSortNo(sourceMenu.getSortNo() == null ? 0 : sourceMenu.getSortNo());
                foodMealMenuService.createMenu(newMenu);
                copyMenuItems(sourceMenu.getId(), newMenu);
                createdCount++;
            }
        }
        FoodMealMenuCopyResultVO result = new FoodMealMenuCopyResultVO();
        result.setSourceCount(sourceMenus.size());
        result.setCreatedCount(createdCount);
        result.setOverwrittenCount(overwrittenCount);
        result.setTargetPeriodCount(dto.getTargetStartDates().size());
        return result;
    }

    private void copyMenuItems(String sourceMenuId, FoodMealMenu targetMenu) {
        List<FoodMealMenuItem> sourceItems = foodMealMenuItemService.listByMenuId(sourceMenuId);
        if (CollUtil.isEmpty(sourceItems)) {
            return;
        }
        int index = 0;
        for (FoodMealMenuItem sourceItem : sourceItems) {
            FoodMealMenuItem newItem = new FoodMealMenuItem();
            newItem.setMenuId(targetMenu.getId());
            newItem.setRestaurantId(targetMenu.getRestaurantId());
            newItem.setServiceAreaId(targetMenu.getServiceAreaId());
            newItem.setTemplateId(targetMenu.getTemplateId());
            newItem.setTemplateItemId(sourceItem.getTemplateItemId());
            newItem.setItemType(sourceItem.getItemType());
            newItem.setDishId(sourceItem.getDishId());
            newItem.setPackageId(sourceItem.getPackageId());
            newItem.setItemName(sourceItem.getItemName());
            newItem.setItemImage(sourceItem.getItemImage());
            newItem.setImagesJson(sourceItem.getImagesJson());
            newItem.setTagsJson(sourceItem.getTagsJson());
            newItem.setDescriptionHtml(sourceItem.getDescriptionHtml());
            newItem.setPublishStatus(sourceItem.getPublishStatus());
            newItem.setSalePrice(sourceItem.getSalePrice());
            newItem.setStockQuantity(sourceItem.getStockQuantity());
            newItem.setSoldQuantity(0);
            newItem.setLimitQuantityPerUser(sourceItem.getLimitQuantityPerUser());
            newItem.setSortNo(sourceItem.getSortNo() == null ? index : sourceItem.getSortNo());
            foodMealMenuItemService.save(newItem);
            copyItemSpecs(sourceItem.getId(), targetMenu, newItem);
            index++;
        }
    }

    private void copyItemSpecs(String sourceItemId, FoodMealMenu targetMenu, FoodMealMenuItem targetItem) {
        List<FoodMealMenuItemSpec> sourceSpecs = foodMealMenuItemSpecService.listByMenuItemId(sourceItemId);
        if (CollUtil.isEmpty(sourceSpecs)) {
            return;
        }
        int index = 0;
        for (FoodMealMenuItemSpec sourceSpec : sourceSpecs) {
            FoodMealMenuItemSpec newSpec = new FoodMealMenuItemSpec();
            newSpec.setMenuItemId(targetItem.getId());
            newSpec.setMenuId(targetMenu.getId());
            newSpec.setRestaurantId(targetMenu.getRestaurantId());
            newSpec.setServiceAreaId(targetMenu.getServiceAreaId());
            newSpec.setTemplateId(targetMenu.getTemplateId());
            newSpec.setTemplateItemId(targetItem.getTemplateItemId());
            newSpec.setTemplateSpecId(sourceSpec.getTemplateSpecId());
            newSpec.setSpecName(sourceSpec.getSpecName());
            newSpec.setSpecDescription(sourceSpec.getSpecDescription());
            newSpec.setSalePrice(sourceSpec.getSalePrice());
            newSpec.setStockQuantity(sourceSpec.getStockQuantity());
            newSpec.setSoldQuantity(0);
            newSpec.setLimitQuantityPerUser(sourceSpec.getLimitQuantityPerUser());
            newSpec.setSortNo(sourceSpec.getSortNo() == null ? index : sourceSpec.getSortNo());
            foodMealMenuItemSpecService.save(newSpec);
            index++;
        }
    }

    @Transactional(readOnly = false)
    public void delete(String ids) {
        for (String id : ids.split(",")) {
            if (StrUtil.isNotBlank(id)) {
                foodMealMenuItemSpecService.deleteByMenuId(id);
                foodMealMenuItemService.deleteByMenuId(id);
            }
        }
        foodMealMenuService.batchDelete(StrUtil.splitTrim(ids, ','));
    }

    @Transactional(readOnly = false)
    public void publish(String id) {
        ensureCanPublish(id);
        foodMealMenuService.updatePublishStatus(id, "published");
    }

    @Transactional(readOnly = false)
    public void offline(String id) {
        foodMealMenuService.updatePublishStatus(id, "offline");
    }

    @Transactional(readOnly = false)
    public void updateItemPublishStatus(String id, String publishStatus) {
        String status = normalizePublishStatus(publishStatus, null);
        FoodMealMenuItem item = foodMealMenuItemService.get(id);
        if (item == null) {
            throw new BizErrorException("菜单明细不存在");
        }
        foodMealMenuItemService.updatePublishStatus(id, status);
    }

    private String normalizePublishStatus(String publishStatus, String defaultStatus) {
        String status = StrUtil.blankToDefault(StrUtil.trim(publishStatus), StrUtil.blankToDefault(defaultStatus, "draft"));
        if (!PUBLISH_STATUSES.contains(status)) {
            throw new BizErrorException("发布状态不正确");
        }
        return status;
    }

    private void validateBatch(FoodMealMenuBatchSaveDTO dto) {
        if (dto.getStartDate().isAfter(dto.getEndDate())) {
            throw new BizErrorException("开始日期不能晚于结束日期");
        }
        dto.setWeekdays(dto.getWeekdays().stream().filter(Objects::nonNull).distinct().toList());
        if (CollUtil.isEmpty(dto.getWeekdays()) || dto.getWeekdays().stream().anyMatch(day -> day < 1 || day > 7)) {
            throw new BizErrorException("周几选择不正确");
        }
        dto.setServiceAreaIds(dto.getServiceAreaIds().stream().map(StrUtil::trim).filter(StrUtil::isNotBlank).distinct().toList());
        if (CollUtil.isEmpty(dto.getServiceAreaIds())) {
            throw new BizErrorException("请选择服务区域");
        }
        dto.setMealTypes(dto.getMealTypes().stream().map(StrUtil::trim).filter(StrUtil::isNotBlank).distinct().toList());
        if (CollUtil.isEmpty(dto.getMealTypes()) || dto.getMealTypes().stream().anyMatch(mealType -> !MEAL_TYPES.contains(mealType))) {
            throw new BizErrorException("餐别不正确");
        }
        dto.setScopeType(StrUtil.trim(dto.getScopeType()));
        if (!SCOPE_TYPES.contains(dto.getScopeType())) {
            throw new BizErrorException("适用范围不正确");
        }
        if ("company".equals(dto.getScopeType()) && StrUtil.isBlank(companyIdForBatch(dto))) {
            throw new BizErrorException("企业专属菜单必须填写企业ID");
        }
        String publishStatus = normalizePublishStatus(dto.getPublishStatus(), "draft");
        dto.setPublishStatus(publishStatus);
        if (CollUtil.isEmpty(batchDates(dto))) {
            throw new BizErrorException("日期范围内没有匹配的周几");
        }
    }

    private List<LocalDate> batchDates(FoodMealMenuBatchSaveDTO dto) {
        List<LocalDate> dates = new ArrayList<>();
        LocalDate date = dto.getStartDate();
        while (!date.isAfter(dto.getEndDate())) {
            if (dto.getWeekdays().contains(date.getDayOfWeek().getValue())) {
                dates.add(date);
            }
            date = date.plusDays(1);
        }
        return dates;
    }

    private Map<String, List<String>> batchTemplateMap(List<FoodMealMenuBatchTemplateDTO> templates) {
        Map<String, List<String>> templateMap = new HashMap<>();
        for (FoodMealMenuBatchTemplateDTO template : templates) {
            if (template == null) {
                continue;
            }
            String serviceAreaId = StrUtil.trim(template.getServiceAreaId());
            String mealType = StrUtil.trim(template.getMealType());
            String templateId = StrUtil.trim(template.getTemplateId());
            if (StrUtil.isBlank(serviceAreaId) || StrUtil.isBlank(mealType) || StrUtil.isBlank(templateId)) {
                continue;
            }
            if (!MEAL_TYPES.contains(mealType)) {
                throw new BizErrorException("餐别不正确");
            }
            String key = batchTemplateKey(serviceAreaId, mealType);
            if (!templateMap.containsKey(key)) {
                templateMap.put(key, new ArrayList<>());
            }
            if (!templateMap.get(key).contains(templateId)) {
                templateMap.get(key).add(templateId);
            }
        }
        return templateMap;
    }

    private String batchTemplateKey(String serviceAreaId, String mealType) {
        return serviceAreaId + "__" + mealType;
    }

    private String companyIdForBatch(FoodMealMenuBatchSaveDTO dto) {
        return "company".equals(dto.getScopeType()) ? StrUtil.trimToEmpty(dto.getCompanyId()) : "";
    }

    private FoodMealServiceArea validateMenu(FoodMealMenuSaveDTO dto, boolean skipConflictCheck) {
        FoodMealServiceArea serviceArea = foodMealServiceAreaService.getOnline(dto.getServiceAreaId());
        foodMealMenuTemplateService.getEnabled(dto.getTemplateId());
        if (dto.getMenuDate().isBefore(LocalDate.now())) {
            throw new BizErrorException("菜单日期不能早于今天");
        }
        if (!MEAL_TYPES.contains(dto.getMealType())) {
            throw new BizErrorException("餐别不正确");
        }
        if (!SCOPE_TYPES.contains(dto.getScopeType())) {
            throw new BizErrorException("适用范围不正确");
        }
        String companyId = companyIdForSave(dto);
        if ("company".equals(dto.getScopeType()) && StrUtil.isBlank(companyId)) {
            throw new BizErrorException("企业专属菜单必须填写企业ID");
        }
        String publishStatus = normalizePublishStatus(dto.getPublishStatus(), "draft");
        dto.setPublishStatus(publishStatus);
        if (!skipConflictCheck && foodMealMenuService.existsSameScope(dto.getId(), dto.getServiceAreaId(), dto.getMenuDate(), dto.getMealType(), dto.getScopeType(), companyId)) {
            throw new BizErrorException("同一服务区域、日期、餐别、适用范围的菜单已存在");
        }
        if (CollUtil.isNotEmpty(dto.getItems())) {
            validateItems(dto.getRestaurantId(), dto.getItems());
        }
        return serviceArea;
    }

    private void validateItems(String restaurantId, List<FoodMealMenuItemSaveDTO> items) {
        if (CollUtil.isEmpty(items)) {
            throw new BizErrorException("请至少配置一个菜单明细");
        }
        for (FoodMealMenuItemSaveDTO item : items) {
            if (item == null || !("dish".equals(item.getItemType()) || "package".equals(item.getItemType()))) {
                throw new BizErrorException("菜单明细类型不正确");
            }
            if ("dish".equals(item.getItemType()) && StrUtil.isBlank(item.getDishId())) {
                throw new BizErrorException("菜品明细必须选择菜品");
            }
            if ("package".equals(item.getItemType()) && StrUtil.isBlank(item.getPackageId())) {
                throw new BizErrorException("套餐明细必须选择套餐");
            }
            if (item.getSalePrice() == null || item.getSalePrice().compareTo(BigDecimal.ZERO) < 0) {
                throw new BizErrorException("菜单明细售价不能小于0");
            }
            if (item.getStockQuantity() != null && item.getStockQuantity() < 0) {
                throw new BizErrorException("菜单明细库存不能小于0");
            }
            if (item.getLimitQuantityPerUser() != null && item.getLimitQuantityPerUser() < 1) {
                throw new BizErrorException("菜单明细限购数量必须大于0");
            }
            validateJsonArray(item.getImagesJson(), "菜单明细轮播图");
            validateJsonArray(item.getTagsJson(), "菜单明细标签");
            validateSpecs(item.getSpecs());
            if (StrUtil.isNotBlank(restaurantId)) {
                if ("dish".equals(item.getItemType())) {
                    FoodDish dish = foodDishService.get(item.getDishId());
                    if (dish == null || !Objects.equals(dish.getRestaurantId(), restaurantId)) {
                        throw new BizErrorException("菜品不存在或不属于当前门店");
                    }
                } else {
                    FoodPackage pkg = foodPackageService.get(item.getPackageId());
                    if (pkg == null || !Objects.equals(pkg.getRestaurantId(), restaurantId)) {
                        throw new BizErrorException("套餐不存在或不属于当前门店");
                    }
                }
            } else if ("dish".equals(item.getItemType())) {
                if (foodDishService.get(item.getDishId()) == null) {
                    throw new BizErrorException("菜品不存在");
                }
            } else if (foodPackageService.get(item.getPackageId()) == null) {
                throw new BizErrorException("套餐不存在");
            }
        }
    }

    private List<FoodMealMenuItemSaveDTO> buildItemsFromTemplateIfEmpty(FoodMealMenuSaveDTO dto) {
        if (CollUtil.isNotEmpty(dto.getItems())) {
            return dto.getItems();
        }
        List<FoodMealMenuTemplateItem> templateItems = foodMealMenuTemplateItemService.listByTemplateId(dto.getTemplateId());
        if (CollUtil.isEmpty(templateItems)) {
            throw new BizErrorException("菜单模板未配置明细");
        }
        Map<String, List<FoodMealMenuTemplateItemSpec>> specMap = foodMealMenuTemplateItemSpecService
            .listByTemplateItemIds(templateItems.stream().map(FoodMealMenuTemplateItem::getId).toList())
            .stream().collect(Collectors.groupingBy(FoodMealMenuTemplateItemSpec::getTemplateItemId));
        return templateItems.stream().map(templateItem -> {
            FoodMealMenuItemSaveDTO item = new FoodMealMenuItemSaveDTO();
            item.setServiceAreaId(dto.getServiceAreaId());
            item.setTemplateId(dto.getTemplateId());
            item.setTemplateItemId(templateItem.getId());
            item.setItemType(templateItem.getItemType());
            item.setDishId(templateItem.getDishId());
            item.setPackageId(templateItem.getPackageId());
            item.setItemName(templateItem.getItemName());
            item.setItemImage(templateItem.getItemImage());
            item.setImagesJson(templateItem.getImagesJson());
            item.setTagsJson(templateItem.getTagsJson());
            item.setDescriptionHtml(templateItem.getDescriptionHtml());
            item.setSalePrice(templateItem.getSalePrice());
            item.setStockQuantity(templateItem.getStockQuantity());
            item.setLimitQuantityPerUser(templateItem.getLimitQuantityPerUser());
            item.setSortNo(templateItem.getSortNo());
            item.setSpecs(specMap.getOrDefault(templateItem.getId(), List.of()).stream().map(templateSpec -> {
                FoodMealMenuItemSpecSaveDTO spec = new FoodMealMenuItemSpecSaveDTO();
                spec.setServiceAreaId(dto.getServiceAreaId());
                spec.setTemplateId(dto.getTemplateId());
                spec.setTemplateItemId(templateItem.getId());
                spec.setTemplateSpecId(templateSpec.getId());
                spec.setSpecName(templateSpec.getSpecName());
                spec.setSpecDescription(templateSpec.getSpecDescription());
                spec.setSalePrice(templateSpec.getSalePrice());
                spec.setStockQuantity(templateSpec.getStockQuantity());
                spec.setLimitQuantityPerUser(templateSpec.getLimitQuantityPerUser());
                spec.setSortNo(templateSpec.getSortNo());
                return spec;
            }).toList());
            return item;
        }).toList();
    }

    private void saveItems(FoodMealMenu menu, List<FoodMealMenuItemSaveDTO> items) {
        foodMealMenuItemSpecService.deleteByMenuId(menu.getId());
        foodMealMenuItemService.deleteByMenuId(menu.getId());
        int index = 0;
        for (FoodMealMenuItemSaveDTO itemDto : items) {
            FoodMealMenuItem item = FoodMealMenuItemConvert.INSTANCE.toEntity(itemDto);
            item.setId(null);
            item.setMenuId(menu.getId());
            item.setRestaurantId(menu.getRestaurantId());
            item.setServiceAreaId(menu.getServiceAreaId());
            item.setTemplateId(menu.getTemplateId());
            item.setTemplateItemId(StrUtil.blankToDefault(item.getTemplateItemId(), itemDto.getTemplateItemId()));
            if (item.getSoldQuantity() == null) {
                item.setSoldQuantity(0);
            }
            item.setPublishStatus(normalizePublishStatus(item.getPublishStatus(), "published"));
            if (item.getSortNo() == null) {
                item.setSortNo(index);
            }
            fillItemSnapshot(item);
            foodMealMenuItemService.save(item);
            saveSpecs(menu, item, itemDto.getSpecs());
            index++;
        }
    }

    private void fillItemSnapshot(FoodMealMenuItem item) {
        if ("dish".equals(item.getItemType())) {
            FoodDish dish = foodDishService.get(item.getDishId());
            if (dish != null) {
                item.setPackageId(null);
                item.setItemName(dish.getName());
                item.setItemImage(dish.getImageUrl());
                item.setImagesJson(normalizeArrayJson(dish.getImagesJson()));
                item.setTagsJson(normalizeArrayJson(dish.getTagsJson()));
                item.setDescriptionHtml(dish.getDescriptionHtml());
                if (item.getSalePrice() == null) {
                    item.setSalePrice(dish.getPrice());
                }
            }
        } else {
            FoodPackage pkg = foodPackageService.get(item.getPackageId());
            if (pkg != null) {
                item.setDishId(null);
                item.setItemName(StrUtil.blankToDefault(item.getItemName(), pkg.getName()));
                item.setImagesJson(StrUtil.blankToDefault(item.getImagesJson(), pkg.getImagesJson()));
                if (item.getSalePrice() == null) {
                    item.setSalePrice(pkg.getPrice());
                }
            }
        }
    }

    private void saveSpecs(FoodMealMenu menu, FoodMealMenuItem item, List<FoodMealMenuItemSpecSaveDTO> specs) {
        if (CollUtil.isEmpty(specs)) {
            return;
        }
        int index = 0;
        for (FoodMealMenuItemSpecSaveDTO specDto : specs) {
            FoodMealMenuItemSpec spec = new FoodMealMenuItemSpec();
            spec.setMenuItemId(item.getId());
            spec.setMenuId(menu.getId());
            spec.setRestaurantId(menu.getRestaurantId());
            spec.setServiceAreaId(menu.getServiceAreaId());
            spec.setTemplateId(menu.getTemplateId());
            spec.setTemplateItemId(StrUtil.blankToDefault(specDto.getTemplateItemId(), item.getTemplateItemId()));
            spec.setTemplateSpecId(specDto.getTemplateSpecId());
            spec.setSpecName(StrUtil.trim(specDto.getSpecName()));
            spec.setSpecDescription(StrUtil.trimToNull(specDto.getSpecDescription()));
            spec.setSalePrice(specDto.getSalePrice());
            spec.setStockQuantity(specDto.getStockQuantity());
            spec.setSoldQuantity(specDto.getSoldQuantity() == null ? 0 : specDto.getSoldQuantity());
            spec.setLimitQuantityPerUser(specDto.getLimitQuantityPerUser());
            spec.setSortNo(specDto.getSortNo() == null ? index : specDto.getSortNo());
            foodMealMenuItemSpecService.save(spec);
            index++;
        }
    }

    private void validateJsonArray(String value, String fieldName) {
        if (StrUtil.isBlank(value)) {
            return;
        }
        try {
            JSONUtil.parseArray(value);
        } catch (BizErrorException e) {
            throw e;
        } catch (Exception e) {
            throw new BizErrorException(fieldName + "格式不正确");
        }
    }

    private void validateSpecs(List<FoodMealMenuItemSpecSaveDTO> specs) {
        if (CollUtil.isEmpty(specs)) {
            return;
        }
        for (FoodMealMenuItemSpecSaveDTO spec : specs) {
            if (spec == null || StrUtil.isBlank(spec.getSpecName())) {
                throw new BizErrorException("规格名称不能为空");
            }
            if (spec.getSalePrice() == null || spec.getSalePrice().compareTo(BigDecimal.ZERO) < 0) {
                throw new BizErrorException("规格售价不能小于0");
            }
            if (spec.getStockQuantity() != null && spec.getStockQuantity() < 0) {
                throw new BizErrorException("规格库存不能小于0");
            }
            if (spec.getSoldQuantity() != null && spec.getSoldQuantity() < 0) {
                throw new BizErrorException("规格已售数量不能小于0");
            }
            if (spec.getLimitQuantityPerUser() != null && spec.getLimitQuantityPerUser() < 1) {
                throw new BizErrorException("规格限购数量必须大于0");
            }
        }
    }

    private void enrichMenuItemDetails(List<FoodMealMenuItemListVO> itemVOList, List<FoodMealMenuItem> items) {
        if (CollUtil.isEmpty(itemVOList) || CollUtil.isEmpty(items)) {
            return;
        }
        Map<String, FoodMealMenuItem> itemMap = items.stream()
            .filter(item -> StrUtil.isNotBlank(item.getId()))
            .collect(Collectors.toMap(FoodMealMenuItem::getId, Function.identity(), (a, b) -> a));
        Map<String, FoodDish> dishMap = foodDishService.mapDishByIds(items.stream()
            .filter(item -> "dish".equals(item.getItemType()))
            .map(FoodMealMenuItem::getDishId)
            .filter(StrUtil::isNotBlank)
            .distinct()
            .toList());
        Map<String, List<FoodMealMenuItemSpec>> specMap = foodMealMenuItemSpecService.listByMenuItemIds(itemMap.keySet()).stream()
            .collect(Collectors.groupingBy(FoodMealMenuItemSpec::getMenuItemId));
        for (FoodMealMenuItemListVO vo : itemVOList) {
            FoodMealMenuItem item = itemMap.get(vo.getId());
            if (item == null) {
                continue;
            }
            FoodDish dish = "dish".equals(item.getItemType()) ? dishMap.get(item.getDishId()) : null;
            if (dish != null) {
                vo.setItemName(dish.getName());
                vo.setItemImage(dish.getImageUrl());
                vo.setTags(parseStringList(dish.getTagsJson()));
                List<String> images = parseStringList(dish.getImagesJson());
                vo.setImages(images.isEmpty() && StrUtil.isNotBlank(dish.getImageUrl()) ? List.of(dish.getImageUrl()) : images);
                vo.setDescriptionHtml(dish.getDescriptionHtml());
            } else {
                vo.setTags(parseStringList(item.getTagsJson()));
                List<String> images = parseStringList(item.getImagesJson());
                vo.setImages(images.isEmpty() && StrUtil.isNotBlank(item.getItemImage()) ? List.of(item.getItemImage()) : images);
                vo.setDescriptionHtml(item.getDescriptionHtml());
            }
            vo.setSpecs(specMap.getOrDefault(item.getId(), List.of()).stream().map(this::toSpecVO).toList());
        }
    }

    private String normalizeArrayJson(String value) {
        if (StrUtil.isBlank(value)) {
            return "[]";
        }
        try {
            return JSONUtil.parseArray(value).toString();
        } catch (Exception e) {
            return "[]";
        }
    }

    private List<String> parseStringList(String json) {
        if (StrUtil.isBlank(json)) {
            return List.of();
        }
        try {
            return JSONUtil.parseArray(json).stream()
                .map(value -> value == null ? "" : String.valueOf(value).trim())
                .filter(StrUtil::isNotBlank)
                .toList();
        } catch (Exception e) {
            return List.of();
        }
    }

    private FoodMealMenuItemListVO.Spec toSpecVO(FoodMealMenuItemSpec spec) {
        FoodMealMenuItemListVO.Spec vo = new FoodMealMenuItemListVO.Spec();
        vo.setId(spec.getId());
        vo.setName(spec.getSpecName());
        vo.setDescription(spec.getSpecDescription());
        vo.setSalePrice(spec.getSalePrice());
        vo.setStockQuantity(spec.getStockQuantity());
        vo.setSoldQuantity(spec.getSoldQuantity());
        vo.setRemainingQuantity(spec.getStockQuantity() == null ? null : Math.max(spec.getStockQuantity() - (spec.getSoldQuantity() == null ? 0 : spec.getSoldQuantity()), 0));
        vo.setSoldOut(vo.getRemainingQuantity() != null && vo.getRemainingQuantity() <= 0);
        vo.setLimitQuantityPerUser(spec.getLimitQuantityPerUser());
        vo.setSortNo(spec.getSortNo());
        return vo;
    }

    private void ensureCanPublish(String id) {
        FoodMealMenu menu = foodMealMenuService.get(id);
        if (menu == null) {
            throw new BizErrorException("菜单不存在");
        }
        if (CollUtil.isEmpty(foodMealMenuItemService.listByMenuId(id))) {
            throw new BizErrorException("菜单无明细，不能发布");
        }
    }

    private String companyIdForSave(FoodMealMenuSaveDTO dto) {
        return "company".equals(dto.getScopeType()) ? StrUtil.trimToEmpty(dto.getCompanyId()) : "";
    }

    private FoodMealMenuQueryVO prepareQuery(FoodMealMenuQueryVO query) {
        FoodMealMenuQueryVO prepared = query != null ? query : new FoodMealMenuQueryVO();
        String publishStatus = StrUtil.trim(prepared.getPublishStatus());
        if (StrUtil.isBlank(publishStatus)) {
            prepared.setPublishStatus(null);
            return prepared;
        }
        if (!PUBLISH_STATUSES.contains(publishStatus)) {
            return null;
        }
        prepared.setPublishStatus(publishStatus);
        return prepared;
    }

    private void enrichListDisplay(List<FoodMealMenuListVO> rows) {
        if (CollUtil.isEmpty(rows)) {
            return;
        }
        List<String> restaurantIds = rows.stream().map(FoodMealMenuListVO::getRestaurantId).filter(StrUtil::isNotBlank).distinct().toList();
        Map<String, String> restaurantNameMap = foodRestaurantService.mapRestaurantNameByIds(restaurantIds);
        List<String> serviceAreaIds = rows.stream().map(FoodMealMenuListVO::getServiceAreaId).filter(StrUtil::isNotBlank).distinct().toList();
        Map<String, String> serviceAreaNameMap = foodMealServiceAreaService.mapAreaNameByIds(serviceAreaIds);
        List<String> templateIds = rows.stream().map(FoodMealMenuListVO::getTemplateId).filter(StrUtil::isNotBlank).distinct().toList();
        Map<String, String> templateNameMap = foodMealMenuTemplateService.mapTemplateNameByIds(templateIds);
        List<String> menuIds = rows.stream().map(FoodMealMenuListVO::getId).filter(StrUtil::isNotBlank).toList();
        Map<String, List<FoodMealMenuItemListVO>> itemMap = menuIds.stream().collect(Collectors.toMap(Function.identity(), id -> {
            List<FoodMealMenuItem> items = foodMealMenuItemService.listByMenuId(id);
            List<FoodMealMenuItemListVO> itemVOList = FoodMealMenuItemConvert.INSTANCE.toListVO(items);
            enrichMenuItemDetails(itemVOList, items);
            return itemVOList;
        }));
        for (FoodMealMenuListVO row : rows) {
            row.setRestaurantName(restaurantNameMap.get(row.getRestaurantId()));
            row.setServiceAreaName(StrUtil.blankToDefault(row.getServiceAreaName(), serviceAreaNameMap.get(row.getServiceAreaId())));
            row.setTemplateName(StrUtil.blankToDefault(row.getTemplateName(), templateNameMap.get(row.getTemplateId())));
            List<FoodMealMenuItemListVO> items = itemMap.getOrDefault(row.getId(), new ArrayList<>());
            row.setItems(items);
            row.setItemCount(items.size());
        }
    }

    private void enrichInfoDisplay(FoodMealMenuInfoVO vo) {
        if (vo == null) {
            return;
        }
        if (StrUtil.isNotBlank(vo.getRestaurantId())) {
            Map<String, String> nameMap = foodRestaurantService.mapRestaurantNameByIds(List.of(vo.getRestaurantId()));
            vo.setRestaurantName(nameMap.get(vo.getRestaurantId()));
        }
        if (StrUtil.isNotBlank(vo.getServiceAreaId())) {
            Map<String, String> nameMap = foodMealServiceAreaService.mapAreaNameByIds(List.of(vo.getServiceAreaId()));
            vo.setServiceAreaName(StrUtil.blankToDefault(vo.getServiceAreaName(), nameMap.get(vo.getServiceAreaId())));
        }
        if (StrUtil.isNotBlank(vo.getTemplateId())) {
            Map<String, String> nameMap = foodMealMenuTemplateService.mapTemplateNameByIds(List.of(vo.getTemplateId()));
            vo.setTemplateName(StrUtil.blankToDefault(vo.getTemplateName(), nameMap.get(vo.getTemplateId())));
        }
    }

    private static PageRespVO<FoodMealMenuListVO> emptyPage(Page<FoodMealMenu> page) {
        PageRespVO<FoodMealMenuListVO> empty = new PageRespVO<>();
        empty.setRecords(Collections.emptyList());
        empty.setTotal(0L);
        empty.setPages(0L);
        empty.setPageNo(page != null ? page.getPageNo() : 1L);
        empty.setPageSize(page != null ? page.getPageSize() : 10L);
        return empty;
    }
}
