package com.huimv.employment.service.draft;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huimv.employment.common.exception.BizException;
import com.huimv.employment.common.exception.ErrorCode;
import com.huimv.employment.dao.entity.FeDraftCostSnapshot;
import com.huimv.employment.dao.entity.FeDraftField;
import com.huimv.employment.dao.entity.FeDraftWorkItem;
import com.huimv.employment.dao.entity.FeEmploymentDraft;
import com.huimv.employment.dao.entity.FeEmploymentOrder;
import com.huimv.employment.dao.entity.FeEnterprise;
import com.huimv.employment.dao.entity.FeRegistrationBatch;
import com.huimv.employment.dao.mapper.FeDraftCostSnapshotMapper;
import com.huimv.employment.dao.mapper.FeDraftFieldMapper;
import com.huimv.employment.dao.mapper.FeEmploymentDraftMapper;
import com.huimv.employment.dao.mapper.FeEmploymentOrderMapper;
import com.huimv.employment.dao.mapper.FeRegistrationBatchMapper;
import com.huimv.employment.service.draft.dto.DraftConfirmRequest;
import com.huimv.employment.service.draft.dto.DraftConfirmResponse;
import com.huimv.employment.service.draft.dto.DraftDetailResponse;
import com.huimv.employment.service.draft.dto.DraftUpdateRequest;
import com.huimv.employment.service.draft.dto.DraftWithCostComparisonResponse;
import com.huimv.employment.service.draft.dto.DraftWorkerGroupItem;
import com.huimv.employment.service.draft.dto.McpDraftCalculateResponse;
import com.huimv.employment.service.enterprise.EnterpriseService;
import com.huimv.employment.service.conversation.ConversationService;
import com.huimv.employment.service.order.OrderWorkItemService;
import com.huimv.employment.service.registration.RegistrationBatchService;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * 用工草稿查询与编辑。
 */
@Service
public class DraftService {

    private static final String STATUS_PENDING = "pending";
    private static final String STATUS_CONFIRMED = "confirmed";
    private static final String ORDER_STATUS_CONFIRMED = "confirmed";
    private static final String STEP_PLAN_CONFIRMED = "plan_confirmed";
    private static final String CONFIRM_SUCCESS = "success";
    private static final String SOURCE_USER_INPUT = "user_input";

    private static final Set<String> ALLOWED_SETTLEMENT_MODES = Collections.unmodifiableSet(
            new HashSet<>(Arrays.asList("daily", "piece", "monthly")));
    private static final Set<String> ALLOWED_INSURANCE_LEVELS = Collections.unmodifiableSet(
            new HashSet<>(Arrays.asList("basic", "standard", "premium")));
    private static final Map<String, String> FIELD_LABELS;

    static {
        Map<String, String> labels = new HashMap<>();
        labels.put("worker_count", "用工人数");
        labels.put("work_days", "工作天数");
        labels.put("work_type", "岗位类型");
        labels.put("work_content", "工作内容");
        labels.put("work_location", "工作地点");
        labels.put("settlement_mode", "结算方式");
        labels.put("daily_wage", "日薪标准");
        labels.put("job_requirements", "岗位要求");
        FIELD_LABELS = Collections.unmodifiableMap(labels);
    }

    private final FeEmploymentDraftMapper feEmploymentDraftMapper;
    private final FeDraftFieldMapper feDraftFieldMapper;
    private final FeDraftCostSnapshotMapper feDraftCostSnapshotMapper;
    private final FeEmploymentOrderMapper feEmploymentOrderMapper;
    private final FeRegistrationBatchMapper feRegistrationBatchMapper;
    private final EnterpriseService enterpriseService;
    private final ConversationService conversationService;
    private final DraftCostCalculationEngine draftCostCalculationEngine;
    private final DraftWorkItemService draftWorkItemService;
    private final OrderWorkItemService orderWorkItemService;
    private final RegistrationBatchService registrationBatchService;
    private final ObjectMapper objectMapper;

    public DraftService(FeEmploymentDraftMapper feEmploymentDraftMapper,
                        FeDraftFieldMapper feDraftFieldMapper,
                        FeDraftCostSnapshotMapper feDraftCostSnapshotMapper,
                        FeEmploymentOrderMapper feEmploymentOrderMapper,
                        FeRegistrationBatchMapper feRegistrationBatchMapper,
                        EnterpriseService enterpriseService,
                        ConversationService conversationService,
                        DraftCostCalculationEngine draftCostCalculationEngine,
                        DraftWorkItemService draftWorkItemService,
                        OrderWorkItemService orderWorkItemService,
                        RegistrationBatchService registrationBatchService,
                        ObjectMapper objectMapper) {
        this.feEmploymentDraftMapper = feEmploymentDraftMapper;
        this.feDraftFieldMapper = feDraftFieldMapper;
        this.feDraftCostSnapshotMapper = feDraftCostSnapshotMapper;
        this.feEmploymentOrderMapper = feEmploymentOrderMapper;
        this.feRegistrationBatchMapper = feRegistrationBatchMapper;
        this.enterpriseService = enterpriseService;
        this.conversationService = conversationService;
        this.draftCostCalculationEngine = draftCostCalculationEngine;
        this.draftWorkItemService = draftWorkItemService;
        this.orderWorkItemService = orderWorkItemService;
        this.registrationBatchService = registrationBatchService;
        this.objectMapper = objectMapper;
    }

    public DraftDetailResponse getDetail(Long userId, Long draftId) {
        FeEmploymentDraft draft = requireOwned(userId, draftId);
        return toDetail(draft);
    }

    /**
     * 公开查询草稿详情（无需登录）：按小程序码 scene（订单编号）定位订单 → 草稿。
     */
    public DraftDetailResponse getPublicDetailByScene(String scene) {
        if (!StringUtils.hasText(scene)) {
            throw new BizException(ErrorCode.BAD_REQUEST, "scene 不能为空");
        }
        String sceneValue = scene.trim();
        FeEmploymentOrder order = feEmploymentOrderMapper.selectOne(new LambdaQueryWrapper<FeEmploymentOrder>()
                .eq(FeEmploymentOrder::getOrderNo, sceneValue)
                .orderByDesc(FeEmploymentOrder::getId)
                .last("LIMIT 1"));
        if (order == null) {
            FeRegistrationBatch batch = feRegistrationBatchMapper.selectOne(new LambdaQueryWrapper<FeRegistrationBatch>()
                    .eq(FeRegistrationBatch::getQrToken, sceneValue)
                    .orderByDesc(FeRegistrationBatch::getId)
                    .last("LIMIT 1"));
            if (batch != null) {
                order = feEmploymentOrderMapper.selectById(batch.getOrderId());
            }
        }
        if (order == null || order.getDraftId() == null) {
            throw new BizException(ErrorCode.NOT_FOUND, "未找到对应用工信息");
        }
        FeEmploymentDraft draft = feEmploymentDraftMapper.selectById(order.getDraftId());
        if (draft == null) {
            throw new BizException(ErrorCode.NOT_FOUND, "草稿不存在");
        }
        return toDetail(draft);
    }

    @Transactional(rollbackFor = Exception.class)
    public DraftDetailResponse update(Long userId, Long draftId, DraftUpdateRequest request) {
        FeEmploymentDraft draft = requireOwned(userId, draftId);
        assertEditable(draft);
        validateUpdateRequest(request, draft);

        LocalDateTime now = LocalDateTime.now();
        Set<String> updatedFieldKeys = new HashSet<>();
        applyUpdates(draft, request, updatedFieldKeys, now);

        draft.setUpdateTime(now);
        feEmploymentDraftMapper.updateById(draft);
        syncFieldMetadata(draft.getId(), updatedFieldKeys, now);
        recalculateMissingFields(draft.getId(), now);

        return toDetail(feEmploymentDraftMapper.selectById(draftId));
    }

    /**
     * 草稿就绪（pending 且 missingFields 为空）后测算成本方案，供微信端展示「确认此方案」。
     */
    @Transactional(rollbackFor = Exception.class)
    public McpDraftCalculateResponse calculateCostComparison(Long userId, Long draftId) {
        FeEmploymentDraft draft = requireOwnedReady(userId, draftId);
        List<FeDraftWorkItem> items = draftWorkItemService.listByDraftId(draft.getId());
        List<DraftCostCalculationEngine.CalculatedScheme> schemes =
                draftCostCalculationEngine.calculate(draft, items);
        persistCostSnapshots(draft, schemes);
        McpDraftCalculateResponse response = draftCostCalculationEngine.toApiResponse(schemes, items);
        response.setDraftId(draft.getId());
        response.setDraftSummary(draftCostCalculationEngine.buildDraftSummary(draft, items));
        return response;
    }

    /**
     * 按会话查询其下全部草稿信息，并附带成本测算。
     * <p>外层为草稿详情；{@code cost_comparison} 与
     * {@link #calculateCostComparison} / {@code GET .../drafts/{id}/cost-comparison} 实体相同。
     * 无法测算时 {@code cost_comparison} 为 null，草稿本身仍返回。
     * 草稿可不绑企业，按会话归属校验即可。</p>
     */
    @Transactional(rollbackFor = Exception.class)
    public List<DraftWithCostComparisonResponse> listCostComparisonsByConversation(Long userId, Long conversationId) {
        conversationService.requireOwned(userId, conversationId);
        List<FeEmploymentDraft> drafts = feEmploymentDraftMapper.selectList(
                new LambdaQueryWrapper<FeEmploymentDraft>()
                        .eq(FeEmploymentDraft::getConversationId, conversationId)
                        .eq(FeEmploymentDraft::getUserId, userId)
                        .orderByAsc(FeEmploymentDraft::getId));
        List<DraftWithCostComparisonResponse> result = new ArrayList<>();
        if (drafts == null || drafts.isEmpty()) {
            return result;
        }
        for (FeEmploymentDraft draft : drafts) {
            DraftDetailResponse detail = toDetail(draft);
            DraftWithCostComparisonResponse item = copyToWithCost(detail);
            try {
                List<FeDraftWorkItem> items = draftWorkItemService.listByDraftId(draft.getId());
                List<DraftCostCalculationEngine.CalculatedScheme> schemes =
                        draftCostCalculationEngine.calculate(draft, items);
                if (STATUS_PENDING.equals(draft.getStatus())) {
                    persistCostSnapshots(draft, schemes);
                }
                McpDraftCalculateResponse cost = draftCostCalculationEngine.toApiResponse(schemes, items);
                cost.setDraftId(draft.getId());
                cost.setDraftSummary(draftCostCalculationEngine.buildDraftSummary(draft, items));
                item.setCostComparison(cost);
            } catch (BizException ignored) {
                item.setCostComparison(null);
            }
            result.add(item);
        }
        return result;
    }

    private static DraftWithCostComparisonResponse copyToWithCost(DraftDetailResponse source) {
        DraftWithCostComparisonResponse target = new DraftWithCostComparisonResponse();
        target.setId(source.getId());
        target.setDraftNo(source.getDraftNo());
        target.setEnterpriseId(source.getEnterpriseId());
        target.setUserId(source.getUserId());
        target.setConversationId(source.getConversationId());
        target.setTitle(source.getTitle());
        target.setStatus(source.getStatus());
        target.setWorkerSource(source.getWorkerSource());
        target.setMissingFields(source.getMissingFields());
        target.setRiskPrompts(source.getRiskPrompts());
        target.setSelectedPlanId(source.getSelectedPlanId());
        target.setConfirmedAt(source.getConfirmedAt());
        target.setConfirmedBy(source.getConfirmedBy());
        target.setCreateTime(source.getCreateTime());
        target.setUpdateTime(source.getUpdateTime());
        target.setWorkerGroups(source.getWorkerGroups());
        target.setQrCodeUrl(source.getQrCodeUrl());
        target.setQrToken(source.getQrToken());
        target.setOrderId(source.getOrderId());
        target.setOrderNo(source.getOrderNo());
        return target;
    }

    /**
     * 微信端确认此方案：选定 SCHEME_A/SCHEME_B，转正式订单。
     * <p>若尚未测算，将自动先测算再确认。</p>
     */
    @Transactional(rollbackFor = Exception.class)
    public DraftConfirmResponse confirmScheme(Long userId, Long draftId, DraftConfirmRequest request) {
        if (request == null || !StringUtils.hasText(request.getSelectedSchemeCode())) {
            throw new BizException(ErrorCode.BAD_REQUEST, "selected_scheme_code 不能为空");
        }
        FeEmploymentDraft draft = requireOwnedReady(userId, draftId);
        String schemeCode = request.getSelectedSchemeCode().trim().toUpperCase();
        if (!draftCostCalculationEngine.isAllowedSchemeCode(schemeCode)) {
            throw new BizException(ErrorCode.BAD_REQUEST, "不支持的方案编码：" + request.getSelectedSchemeCode());
        }

        FeDraftCostSnapshot snapshot = findLatestSnapshot(draft.getId(), schemeCode);
        if (snapshot == null) {
            List<FeDraftWorkItem> items = draftWorkItemService.listByDraftId(draft.getId());
            List<DraftCostCalculationEngine.CalculatedScheme> schemes =
                    draftCostCalculationEngine.calculate(draft, items);
            persistCostSnapshots(draft, schemes);
            snapshot = findLatestSnapshot(draft.getId(), schemeCode);
        }
        if (snapshot == null) {
            throw new BizException(ErrorCode.BAD_REQUEST, "方案测算失败，请稍后重试");
        }

        LocalDateTime now = LocalDateTime.now();
        List<FeDraftWorkItem> items = draftWorkItemService.listByDraftId(draft.getId());
        // 1) 转正式订单
        FeEmploymentOrder order = buildOrderFromDraft(draft, snapshot, items, userId, now);
        order.setOrderNo("tmp_" + UUID.randomUUID().toString().replace("-", "").substring(0, 20));
        try {
            feEmploymentOrderMapper.insert(order);
        } catch (DuplicateKeyException ex) {
            throw new BizException(ErrorCode.BAD_REQUEST, "订单编号冲突，请重试");
        }
        order.setOrderNo(generateOrderNo(order.getId()));
        feEmploymentOrderMapper.updateById(order);
        orderWorkItemService.copyFromDraftItems(order.getId(), items, now);

        // 2) 同步将草稿状态更新为 confirmed（与订单同一事务）
        int updated = feEmploymentDraftMapper.update(null, new LambdaUpdateWrapper<FeEmploymentDraft>()
                .eq(FeEmploymentDraft::getId, draft.getId())
                .eq(FeEmploymentDraft::getStatus, STATUS_PENDING)
                .set(FeEmploymentDraft::getStatus, STATUS_CONFIRMED)
                .set(FeEmploymentDraft::getConfirmedAt, now)
                .set(FeEmploymentDraft::getConfirmedBy, userId)
                .set(FeEmploymentDraft::getSelectedPlanId, snapshot.getPlanId())
                .set(FeEmploymentDraft::getUpdateTime, now));
        if (updated != 1) {
            throw new BizException(ErrorCode.DRAFT_NOT_EDITABLE);
        }

        // 3) 创建登记批次并立刻写入小程序码（fe_registration_batch.qr_code_url）
        FeRegistrationBatch batch = registrationBatchService.createForOrder(order, draft, now);

        DraftConfirmResponse response = new DraftConfirmResponse();
        response.setStatus(CONFIRM_SUCCESS);
        response.setDraftStatus(STATUS_CONFIRMED);
        response.setOrderId(order.getOrderNo());
        response.setDraftId(draft.getId());
        response.setSelectedSchemeCode(schemeCode);
        response.setBatchId(batch.getBatchNo());
        response.setRegistrationUrl(batch.getRegistrationUrl());
        response.setQrCodeUrl(batch.getQrCodeUrl());
        response.setQrToken(batch.getQrToken());
        return response;
    }

    private FeEmploymentDraft requireOwnedReady(Long userId, Long draftId) {
        FeEmploymentDraft draft = requireOwned(userId, draftId);
        assertEditable(draft);
        List<String> missing = parseStringList(draft.getMissingFields());
        if (!missing.isEmpty()) {
            throw new BizException(ErrorCode.BAD_REQUEST,
                    "草稿信息不完整，请先补全字段：" + String.join(", ", missing));
        }
        return draft;
    }

    private void persistCostSnapshots(FeEmploymentDraft draft,
                                      List<DraftCostCalculationEngine.CalculatedScheme> schemes) {
        feDraftCostSnapshotMapper.delete(new LambdaQueryWrapper<FeDraftCostSnapshot>()
                .eq(FeDraftCostSnapshot::getDraftId, draft.getId()));
        LocalDateTime now = LocalDateTime.now();
        for (DraftCostCalculationEngine.CalculatedScheme scheme : schemes) {
            FeDraftCostSnapshot snapshot = new FeDraftCostSnapshot();
            snapshot.setDraftId(draft.getId());
            snapshot.setPlanId(scheme.getPlanId());
            snapshot.setPlanCode(scheme.getCode());
            snapshot.setPlanName(scheme.getTitle());
            snapshot.setEmployerTotalOutflow(scheme.getEmployerTotalOutflow());
            snapshot.setEmployerAvgCostPerWorker(scheme.getEmployerAvgCostPerWorker());
            snapshot.setWorkerTakeHomePerWorker(scheme.getWorkerTakeHomePerWorker());
            snapshot.setIsRecommended(scheme.isRecommended());
            snapshot.setComplianceTags(writeJsonArray(scheme.getTags()));
            snapshot.setCalcVersion(draftCostCalculationEngine.calcVersion());
            snapshot.setCreateTime(now);
            feDraftCostSnapshotMapper.insert(snapshot);
        }
    }

    private FeEmploymentOrder buildOrderFromDraft(FeEmploymentDraft draft,
                                                  FeDraftCostSnapshot snapshot,
                                                  List<FeDraftWorkItem> items,
                                                  Long userId,
                                                  LocalDateTime now) {
        FeEmploymentOrder order = new FeEmploymentOrder();
        order.setEnterpriseId(draft.getEnterpriseId());
        order.setUserId(draft.getUserId() != null ? draft.getUserId() : userId);
        order.setDraftId(draft.getId());
        order.setSelectedPlanId(snapshot.getPlanId());
        order.setTitle(StringUtils.hasText(draft.getTitle()) ? draft.getTitle() : "用工订单");
        order.setOrderStatus(ORDER_STATUS_CONFIRMED);
        order.setWorkerSource(draft.getWorkerSource());
        order.setCurrentStepCode(STEP_PLAN_CONFIRMED);
        order.setCreateTime(now);
        order.setUpdateTime(now);
        order.setDelFlag(0);
        return order;
    }

    private FeDraftCostSnapshot findLatestSnapshot(Long draftId, String schemeCode) {
        return feDraftCostSnapshotMapper.selectOne(new LambdaQueryWrapper<FeDraftCostSnapshot>()
                .eq(FeDraftCostSnapshot::getDraftId, draftId)
                .eq(FeDraftCostSnapshot::getPlanCode, schemeCode)
                .orderByDesc(FeDraftCostSnapshot::getCreateTime)
                .last("LIMIT 1"));
    }

    private static String generateOrderNo(Long orderId) {
        return String.format("ord_%d", orderId);
    }

    private String writeJsonArray(List<String> values) {
        try {
            return objectMapper.writeValueAsString(values);
        } catch (Exception ex) {
            return "[]";
        }
    }

    /**
     * 校验当前用户可访问该草稿：按 user_id 归属（草稿可不绑企业）。
     * 若草稿已绑定企业，则同时要求与当前用户企业一致。
     */
    private FeEmploymentDraft requireOwned(Long userId, Long draftId) {
        if (draftId == null) {
            throw new BizException(ErrorCode.NOT_FOUND, "草稿不存在");
        }
        FeEmploymentDraft draft = feEmploymentDraftMapper.selectById(draftId);
        if (draft == null || draft.getUserId() == null || !draft.getUserId().equals(userId)) {
            throw new BizException(ErrorCode.NOT_FOUND, "草稿不存在");
        }
        if (draft.getEnterpriseId() != null) {
            FeEnterprise enterprise = enterpriseService.findEnterpriseByUserId(userId);
            if (enterprise == null || !enterprise.getId().equals(draft.getEnterpriseId())) {
                throw new BizException(ErrorCode.NOT_FOUND, "草稿不存在");
            }
        }
        return draft;
    }

    private void assertEditable(FeEmploymentDraft draft) {
        if (!STATUS_PENDING.equals(draft.getStatus())) {
            throw new BizException(ErrorCode.DRAFT_NOT_EDITABLE);
        }
    }

    private void validateUpdateRequest(DraftUpdateRequest request, FeEmploymentDraft draft) {
        if (request == null) {
            throw new BizException(ErrorCode.BAD_REQUEST, "请求体不能为空");
        }
        List<FeDraftWorkItem> items = draftWorkItemService.listByDraftId(draft.getId());
        FeDraftWorkItem first = items.isEmpty() ? null : items.get(0);
        LocalDate startDate = request.getWorkStartDate() != null
                ? request.getWorkStartDate()
                : (first != null ? first.getWorkStartDate() : null);
        LocalDate endDate = request.getWorkEndDate() != null
                ? request.getWorkEndDate()
                : (first != null ? first.getWorkEndDate() : null);
        if (startDate != null && endDate != null && endDate.isBefore(startDate)) {
            throw new BizException(ErrorCode.BAD_REQUEST, "结束日期不能早于开工日期");
        }
        if (StringUtils.hasText(request.getSettlementMode())
                && !ALLOWED_SETTLEMENT_MODES.contains(request.getSettlementMode().trim())) {
            throw new BizException(ErrorCode.BAD_REQUEST, "不支持的结算方式：" + request.getSettlementMode());
        }
        if (StringUtils.hasText(request.getInsuranceLevel())
                && !ALLOWED_INSURANCE_LEVELS.contains(request.getInsuranceLevel().trim())) {
            throw new BizException(ErrorCode.BAD_REQUEST, "不支持的保险档次：" + request.getInsuranceLevel());
        }
    }

    private void applyUpdates(FeEmploymentDraft draft,
                              DraftUpdateRequest request,
                              Set<String> updatedFieldKeys,
                              LocalDateTime now) {
        if (request.getTitle() != null) {
            draft.setTitle(trimToNull(request.getTitle()));
        }
        if (request.getSelectedPlanId() != null) {
            draft.setSelectedPlanId(request.getSelectedPlanId());
        }
        if (hasWorkFieldUpdates(request)) {
            List<FeDraftWorkItem> existing = draftWorkItemService.listByDraftId(draft.getId());
            DraftWorkerGroupItem group = existing.isEmpty()
                    ? new DraftWorkerGroupItem()
                    : draftWorkItemService.toApiGroup(existing.get(0));
            group.setGroupId("grp_01");
            if (request.getWorkerCount() != null) {
                group.setWorkerCount(request.getWorkerCount());
                updatedFieldKeys.add("worker_count");
            }
            if (request.getWorkDays() != null) {
                group.setWorkDays(request.getWorkDays());
                updatedFieldKeys.add("work_days");
            }
            if (request.getWorkType() != null) {
                group.setJobType(trimToNull(request.getWorkType()));
                updatedFieldKeys.add("work_type");
            }
            if (request.getWorkContent() != null) {
                group.setWorkContent(trimToNull(request.getWorkContent()));
                updatedFieldKeys.add("work_content");
            }
            if (request.getWorkLocation() != null) {
                group.setRegion(trimToNull(request.getWorkLocation()));
                updatedFieldKeys.add("work_location");
            }
            if (request.getSettlementMode() != null) {
                group.setSettlementCycle(mapSettlementModeToCycle(request.getSettlementMode()));
                updatedFieldKeys.add("settlement_mode");
            }
            if (request.getDailyWage() != null) {
                group.setUnitAmount(request.getDailyWage());
                updatedFieldKeys.add("daily_wage");
            }
            if (request.getWorkStartDate() != null) {
                group.setWorkStartDate(request.getWorkStartDate());
            }
            if (request.getWorkEndDate() != null) {
                group.setWorkEndDate(request.getWorkEndDate());
            }
            if (request.getJobRequirements() != null) {
                updatedFieldKeys.add("job_requirements");
            }
            draftWorkItemService.replaceAll(draft.getId(), Collections.singletonList(group), now);
        }
    }

    private static boolean hasWorkFieldUpdates(DraftUpdateRequest request) {
        return request.getWorkerCount() != null
                || request.getWorkDays() != null
                || request.getWorkType() != null
                || request.getWorkContent() != null
                || request.getWorkLocation() != null
                || request.getSettlementMode() != null
                || request.getDailyWage() != null
                || request.getWorkStartDate() != null
                || request.getWorkEndDate() != null
                || request.getJobRequirements() != null;
    }

    private static String mapSettlementModeToCycle(String settlementMode) {
        if (!StringUtils.hasText(settlementMode)) {
            return null;
        }
        String mode = settlementMode.trim().toLowerCase();
        if ("monthly".equals(mode)) {
            return "monthly";
        }
        if ("weekly".equals(mode)) {
            return "weekly";
        }
        if ("daily".equals(mode) || "piece".equals(mode)) {
            // piece 在明细表用 daily 周期存储
            return "daily";
        }
        // 未识别值不默认成 daily，避免「未提供结算方式」却写进库并清空 missing_fields
        return null;
    }

    private void syncFieldMetadata(Long draftId,
                                   Set<String> updatedFieldKeys,
                                   LocalDateTime now) {
        List<FeDraftWorkItem> items = draftWorkItemService.listByDraftId(draftId);
        for (String fieldKey : updatedFieldKeys) {
            String fieldValue = resolveFieldValue(items, fieldKey);
            upsertUserField(draftId, fieldKey, FIELD_LABELS.get(fieldKey), fieldValue, now);
        }
    }

    private void recalculateMissingFields(Long draftId, LocalDateTime now) {
        FeEmploymentDraft draft = feEmploymentDraftMapper.selectById(draftId);
        if (draft == null) {
            return;
        }
        List<String> missing = new ArrayList<>();
        if (!StringUtils.hasText(draft.getWorkerSource())) {
            missing.add("worker_source");
        }
        List<FeDraftWorkItem> items = draftWorkItemService.listByDraftId(draftId);
        missing.addAll(draftWorkItemService.collectMissingGroupFields(items));
        try {
            FeEmploymentDraft update = new FeEmploymentDraft();
            update.setId(draftId);
            update.setMissingFields(objectMapper.writeValueAsString(missing));
            update.setUpdateTime(now);
            feEmploymentDraftMapper.updateById(update);
        } catch (Exception ex) {
            throw new BizException(ErrorCode.INTERNAL_ERROR);
        }
    }

    private void upsertUserField(Long draftId, String fieldKey, String fieldLabel, String fieldValue, LocalDateTime now) {
        FeDraftField existing = feDraftFieldMapper.selectOne(new LambdaQueryWrapper<FeDraftField>()
                .eq(FeDraftField::getDraftId, draftId)
                .eq(FeDraftField::getFieldKey, fieldKey)
                .last("LIMIT 1"));
        if (existing != null) {
            if (Boolean.FALSE.equals(existing.getEditable())) {
                throw new BizException(ErrorCode.BAD_REQUEST, "字段不可编辑：" + fieldKey);
            }
            FeDraftField update = new FeDraftField();
            update.setId(existing.getId());
            update.setFieldValue(fieldValue);
            update.setSource(SOURCE_USER_INPUT);
            update.setIsDefault(false);
            update.setUpdateTime(now);
            feDraftFieldMapper.updateById(update);
            return;
        }
        FeDraftField created = new FeDraftField();
        created.setDraftId(draftId);
        created.setFieldKey(fieldKey);
        created.setFieldLabel(fieldLabel);
        created.setFieldValue(fieldValue);
        created.setSource(SOURCE_USER_INPUT);
        created.setEditable(true);
        created.setIsDefault(false);
        created.setCreateTime(now);
        created.setUpdateTime(now);
        try {
            feDraftFieldMapper.insert(created);
        } catch (DuplicateKeyException ex) {
            FeDraftField retry = feDraftFieldMapper.selectOne(new LambdaQueryWrapper<FeDraftField>()
                    .eq(FeDraftField::getDraftId, draftId)
                    .eq(FeDraftField::getFieldKey, fieldKey)
                    .last("LIMIT 1"));
            if (retry != null) {
                if (Boolean.FALSE.equals(retry.getEditable())) {
                    throw new BizException(ErrorCode.BAD_REQUEST, "字段不可编辑：" + fieldKey);
                }
                FeDraftField update = new FeDraftField();
                update.setId(retry.getId());
                update.setFieldValue(fieldValue);
                update.setSource(SOURCE_USER_INPUT);
                update.setIsDefault(false);
                update.setUpdateTime(now);
                feDraftFieldMapper.updateById(update);
            }
        }
    }

    private DraftDetailResponse toDetail(FeEmploymentDraft draft) {
        List<FeDraftWorkItem> items = draftWorkItemService.listByDraftId(draft.getId());
        List<DraftWorkerGroupItem> groups = draftWorkItemService.toApiGroups(items);

        DraftDetailResponse response = new DraftDetailResponse();
        response.setId(draft.getId());
        response.setDraftNo(draft.getDraftNo());
        response.setEnterpriseId(draft.getEnterpriseId());
        response.setUserId(draft.getUserId());
        response.setConversationId(draft.getConversationId());
        response.setTitle(draft.getTitle());
        response.setStatus(draft.getStatus());
        response.setWorkerSource(draft.getWorkerSource());
        response.setMissingFields(parseStringList(draft.getMissingFields()));
        response.setRiskPrompts(parseStringList(draft.getRiskPrompts()));
        response.setSelectedPlanId(draft.getSelectedPlanId());
        response.setConfirmedAt(draft.getConfirmedAt());
        response.setConfirmedBy(draft.getConfirmedBy());
        response.setCreateTime(draft.getCreateTime());
        response.setUpdateTime(draft.getUpdateTime());
        // 用工事实在明细表
        response.setWorkerGroups(groups);
        FeEmploymentOrder order = findOrderByDraftId(draft.getId());
        response.setOrderId(order != null ? order.getId() : null);
        response.setOrderNo(order != null ? order.getOrderNo() : null);
        FeRegistrationBatch batch = order != null ? findRegistrationBatchByOrderId(order.getId()) : null;
        response.setBatchId(batch != null ? batch.getId() : null);
        response.setBatchNo(batch != null ? batch.getBatchNo() : null);
        response.setQrCodeUrl(batch != null ? batch.getQrCodeUrl() : null);
        response.setQrToken(batch != null ? batch.getQrToken() : null);
        return response;
    }

    /** 草稿确认后生成的订单；未确认则为 null。 */
    private FeEmploymentOrder findOrderByDraftId(Long draftId) {
        if (draftId == null) {
            return null;
        }
        return feEmploymentOrderMapper.selectOne(new LambdaQueryWrapper<FeEmploymentOrder>()
                .eq(FeEmploymentOrder::getDraftId, draftId)
                .orderByDesc(FeEmploymentOrder::getId)
                .last("LIMIT 1"));
    }

    /** 订单关联登记批次；无批次则为 null。 */
    private FeRegistrationBatch findRegistrationBatchByOrderId(Long orderId) {
        if (orderId == null) {
            return null;
        }
        return feRegistrationBatchMapper.selectOne(new LambdaQueryWrapper<FeRegistrationBatch>()
                .eq(FeRegistrationBatch::getOrderId, orderId)
                .orderByDesc(FeRegistrationBatch::getId)
                .last("LIMIT 1"));
    }

    private String resolveFieldValue(List<FeDraftWorkItem> items, String fieldKey) {
        FeDraftWorkItem first = items == null || items.isEmpty() ? null : items.get(0);
        switch (fieldKey) {
            case "worker_count":
                int count = draftWorkItemService.sumWorkerCount(items);
                return count > 0 ? String.valueOf(count) : null;
            case "work_days":
                return first != null && first.getWorkDays() != null ? first.getWorkDays().toPlainString() : null;
            case "work_type":
                return first != null ? first.getJobType() : null;
            case "work_content":
                return first != null ? first.getWorkContent() : null;
            case "work_location":
                return first != null ? first.getWorkLocation() : null;
            case "settlement_mode":
                return first != null ? first.getSettlementCycle() : null;
            case "daily_wage":
            case "unit_amount":
                return first != null && first.getUnitAmount() != null ? first.getUnitAmount().toPlainString() : null;
            case "job_requirements":
                return null;
            default:
                return null;
        }
    }

    private List<String> parseStringList(String json) {
        if (!StringUtils.hasText(json)) {
            return Collections.emptyList();
        }
        try {
            List<String> parsed = objectMapper.readValue(json.trim(), new TypeReference<List<String>>() {
            });
            return parsed != null ? parsed : Collections.emptyList();
        } catch (Exception ex) {
            return Collections.emptyList();
        }
    }

    private static String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
}
