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.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.DraftFieldResponse;
import com.huimv.employment.service.draft.dto.DraftUpdateRequest;
import com.huimv.employment.service.draft.dto.DraftWithCostComparisonResponse;
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.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;
import java.util.stream.Collectors;

/**
 * 用工草稿查询与编辑。
 */
@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 String WORK_LOCATION_SOURCE_USER = "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 RegistrationBatchService registrationBatchService;
    private final ObjectMapper objectMapper;

    public DraftService(FeEmploymentDraftMapper feEmploymentDraftMapper,
                        FeDraftFieldMapper feDraftFieldMapper,
                        FeDraftCostSnapshotMapper feDraftCostSnapshotMapper,
                        FeEmploymentOrderMapper feEmploymentOrderMapper,
                        FeRegistrationBatchMapper feRegistrationBatchMapper,
                        EnterpriseService enterpriseService,
                        ConversationService conversationService,
                        DraftCostCalculationEngine draftCostCalculationEngine,
                        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.registrationBatchService = registrationBatchService;
        this.objectMapper = objectMapper;
    }

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

    /**
     * 公开查询草稿详情（无需登录）：按小程序码 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, listFields(draft.getId()));
    }

    @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, draft, now);
        refreshMissingFields(draft, updatedFieldKeys, now);

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

    /**
     * 草稿就绪（pending 且 missingFields 为空）后测算成本方案，供微信端展示「确认此方案」。
     */
    @Transactional(rollbackFor = Exception.class)
    public McpDraftCalculateResponse calculateCostComparison(Long userId, Long draftId) {
        FeEmploymentDraft draft = requireOwnedReady(userId, draftId);
        List<DraftCostCalculationEngine.CalculatedScheme> schemes = draftCostCalculationEngine.calculate(draft);
        persistCostSnapshots(draft, schemes);
        updateDraftEstimates(draft, schemes, LocalDateTime.now());
        McpDraftCalculateResponse response = draftCostCalculationEngine.toApiResponse(schemes);
        response.setDraftId(draft.getId());
        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);
        FeEnterprise enterprise = enterpriseService.findEnterpriseByUserId(userId);
        if (enterprise == null) {
            throw new BizException(ErrorCode.BAD_REQUEST, "请先完成企业登记");
        }
        List<FeEmploymentDraft> drafts = feEmploymentDraftMapper.selectList(
                new LambdaQueryWrapper<FeEmploymentDraft>()
                        .eq(FeEmploymentDraft::getConversationId, conversationId)
                        .eq(FeEmploymentDraft::getEnterpriseId, enterprise.getId())
                        .orderByAsc(FeEmploymentDraft::getId));
        List<DraftWithCostComparisonResponse> result = new ArrayList<>();
        if (drafts == null || drafts.isEmpty()) {
            return result;
        }
        LocalDateTime now = LocalDateTime.now();
        for (FeEmploymentDraft draft : drafts) {
            DraftDetailResponse detail = toDetail(draft, listFields(draft.getId()));
            DraftWithCostComparisonResponse item = copyToWithCost(detail);
            try {
                List<DraftCostCalculationEngine.CalculatedScheme> schemes =
                        draftCostCalculationEngine.calculate(draft);
                if (STATUS_PENDING.equals(draft.getStatus())) {
                    persistCostSnapshots(draft, schemes);
                    updateDraftEstimates(draft, schemes, now);
                }
                McpDraftCalculateResponse cost = draftCostCalculationEngine.toApiResponse(schemes);
                cost.setDraftId(draft.getId());
                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.setScenario(source.getScenario());
        target.setTitle(source.getTitle());
        target.setStatus(source.getStatus());
        target.setWorkerCount(source.getWorkerCount());
        target.setWorkDays(source.getWorkDays());
        target.setWorkType(source.getWorkType());
        target.setWorkContent(source.getWorkContent());
        target.setWorkLocation(source.getWorkLocation());
        target.setWorkLocationSource(source.getWorkLocationSource());
        target.setSettlementMode(source.getSettlementMode());
        target.setDailyWage(source.getDailyWage());
        target.setInsuranceRequired(source.getInsuranceRequired());
        target.setInsuranceLevel(source.getInsuranceLevel());
        target.setCheckInRequired(source.getCheckInRequired());
        target.setQrValidDays(source.getQrValidDays());
        target.setWorkStartDate(source.getWorkStartDate());
        target.setWorkEndDate(source.getWorkEndDate());
        target.setJobRequirements(source.getJobRequirements());
        target.setMissingFields(source.getMissingFields());
        target.setRiskPrompts(source.getRiskPrompts());
        target.setEstimatedTotal(source.getEstimatedTotal());
        target.setEstimatedPerCapita(source.getEstimatedPerCapita());
        target.setSelectedPlanId(source.getSelectedPlanId());
        target.setQrCodeUrl(source.getQrCodeUrl());
        target.setQrToken(source.getQrToken());
        target.setFields(source.getFields());
        target.setCreateTime(source.getCreateTime());
        target.setUpdateTime(source.getUpdateTime());
        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<DraftCostCalculationEngine.CalculatedScheme> schemes = draftCostCalculationEngine.calculate(draft);
            persistCostSnapshots(draft, schemes);
            updateDraftEstimates(draft, schemes, LocalDateTime.now());
            snapshot = findLatestSnapshot(draft.getId(), schemeCode);
        }
        if (snapshot == null) {
            throw new BizException(ErrorCode.BAD_REQUEST, "方案测算失败，请稍后重试");
        }

        LocalDateTime now = LocalDateTime.now();
        // 1) 转正式订单
        FeEmploymentOrder order = buildOrderFromDraft(draft, snapshot, 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);

        // 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::getEstimatedTotal, snapshot.getEmployerTotalOutflow())
                .set(FeEmploymentDraft::getEstimatedPerCapita, snapshot.getEmployerAvgCostPerWorker())
                .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 void updateDraftEstimates(FeEmploymentDraft draft,
                                      List<DraftCostCalculationEngine.CalculatedScheme> schemes,
                                      LocalDateTime now) {
        DraftCostCalculationEngine.CalculatedScheme recommended = schemes.stream()
                .filter(DraftCostCalculationEngine.CalculatedScheme::isRecommended)
                .findFirst()
                .orElse(schemes.get(0));
        // 仅刷新人均预估；保留 estimated_total 作为劳务费基数（total_amount），避免二次测算被污染
        FeEmploymentDraft update = new FeEmploymentDraft();
        update.setId(draft.getId());
        update.setEstimatedPerCapita(recommended.getEmployerAvgCostPerWorker());
        update.setUpdateTime(now);
        feEmploymentDraftMapper.updateById(update);
    }

    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 FeEmploymentOrder buildOrderFromDraft(FeEmploymentDraft draft,
                                                  FeDraftCostSnapshot snapshot,
                                                  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.setWorkerCount(draft.getWorkerCount());
        order.setWorkDays(draft.getWorkDays());
        order.setWorkType(draft.getWorkType());
        order.setWorkLocation(draft.getWorkLocation());
        order.setWorkStartDate(draft.getWorkStartDate());
        order.setWorkEndDate(draft.getWorkEndDate());
        order.setOrderStatus(ORDER_STATUS_CONFIRMED);
        order.setCurrentStepCode(STEP_PLAN_CONFIRMED);
        order.setRegisteredCount(0);
        order.setPendingCount(draft.getWorkerCount());
        order.setAbnormalCount(0);
        order.setTotalOutflow(snapshot.getEmployerTotalOutflow());
        order.setCreateTime(now);
        order.setUpdateTime(now);
        order.setDelFlag(0);
        return order;
    }

    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 "[]";
        }
    }

    private FeEmploymentDraft requireOwned(Long userId, Long draftId) {
        FeEnterprise enterprise = enterpriseService.findEnterpriseByUserId(userId);
        if (enterprise == null) {
            throw new BizException(ErrorCode.BAD_REQUEST, "请先完成企业登记");
        }
        if (draftId == null) {
            throw new BizException(ErrorCode.NOT_FOUND, "草稿不存在");
        }
        FeEmploymentDraft draft = feEmploymentDraftMapper.selectById(draftId);
        if (draft == 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, "请求体不能为空");
        }
        LocalDate startDate = request.getWorkStartDate() != null ? request.getWorkStartDate() : draft.getWorkStartDate();
        LocalDate endDate = request.getWorkEndDate() != null ? request.getWorkEndDate() : draft.getWorkEndDate();
        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.getWorkerCount() != null) {
            draft.setWorkerCount(request.getWorkerCount());
            updatedFieldKeys.add("worker_count");
        }
        if (request.getWorkDays() != null) {
            draft.setWorkDays(request.getWorkDays());
            updatedFieldKeys.add("work_days");
        }
        if (request.getWorkType() != null) {
            draft.setWorkType(trimToNull(request.getWorkType()));
            updatedFieldKeys.add("work_type");
        }
        if (request.getWorkContent() != null) {
            draft.setWorkContent(trimToNull(request.getWorkContent()));
            updatedFieldKeys.add("work_content");
        }
        if (request.getWorkLocation() != null) {
            draft.setWorkLocation(trimToNull(request.getWorkLocation()));
            draft.setWorkLocationSource(WORK_LOCATION_SOURCE_USER);
            updatedFieldKeys.add("work_location");
        }
        if (request.getSettlementMode() != null) {
            draft.setSettlementMode(trimToNull(request.getSettlementMode()));
            updatedFieldKeys.add("settlement_mode");
        }
        if (request.getDailyWage() != null) {
            draft.setDailyWage(request.getDailyWage());
            updatedFieldKeys.add("daily_wage");
        }
        if (request.getInsuranceRequired() != null) {
            draft.setInsuranceRequired(request.getInsuranceRequired());
        }
        if (request.getInsuranceLevel() != null) {
            draft.setInsuranceLevel(trimToNull(request.getInsuranceLevel()));
        }
        if (request.getCheckInRequired() != null) {
            draft.setCheckInRequired(request.getCheckInRequired());
        }
        if (request.getQrValidDays() != null) {
            draft.setQrValidDays(request.getQrValidDays());
        }
        if (request.getWorkStartDate() != null) {
            draft.setWorkStartDate(request.getWorkStartDate());
        }
        if (request.getWorkEndDate() != null) {
            draft.setWorkEndDate(request.getWorkEndDate());
        }
        if (request.getJobRequirements() != null) {
            draft.setJobRequirements(trimToNull(request.getJobRequirements()));
            updatedFieldKeys.add("job_requirements");
        }
        if (request.getSelectedPlanId() != null) {
            draft.setSelectedPlanId(request.getSelectedPlanId());
        }
    }

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

    private void refreshMissingFields(FeEmploymentDraft draft, Set<String> updatedFieldKeys, LocalDateTime now) {
        if (updatedFieldKeys.isEmpty()) {
            return;
        }
        List<String> missing = parseStringList(draft.getMissingFields());
        if (missing.isEmpty()) {
            return;
        }
        List<String> remaining = missing.stream()
                .filter(key -> !updatedFieldKeys.contains(key))
                .collect(Collectors.toList());
        if (remaining.size() == missing.size()) {
            return;
        }
        try {
            draft.setMissingFields(objectMapper.writeValueAsString(remaining));
            draft.setUpdateTime(now);
            feEmploymentDraftMapper.updateById(draft);
        } 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 List<FeDraftField> listFields(Long draftId) {
        return feDraftFieldMapper.selectList(new LambdaQueryWrapper<FeDraftField>()
                .eq(FeDraftField::getDraftId, draftId)
                .orderByAsc(FeDraftField::getId));
    }

    private DraftDetailResponse toDetail(FeEmploymentDraft draft, List<FeDraftField> fields) {
        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.setScenario(draft.getScenario());
        response.setTitle(draft.getTitle());
        response.setStatus(draft.getStatus());
        response.setWorkerCount(draft.getWorkerCount());
        response.setWorkDays(draft.getWorkDays());
        response.setWorkType(draft.getWorkType());
        response.setWorkContent(draft.getWorkContent());
        response.setWorkLocation(draft.getWorkLocation());
        response.setWorkLocationSource(draft.getWorkLocationSource());
        response.setSettlementMode(draft.getSettlementMode());
        response.setDailyWage(draft.getDailyWage());
        response.setInsuranceRequired(draft.getInsuranceRequired());
        response.setInsuranceLevel(draft.getInsuranceLevel());
        response.setCheckInRequired(draft.getCheckInRequired());
        response.setQrValidDays(draft.getQrValidDays());
        response.setWorkStartDate(draft.getWorkStartDate());
        response.setWorkEndDate(draft.getWorkEndDate());
        response.setJobRequirements(draft.getJobRequirements());
        response.setMissingFields(parseStringList(draft.getMissingFields()));
        response.setRiskPrompts(parseStringList(draft.getRiskPrompts()));
        response.setEstimatedTotal(draft.getEstimatedTotal());
        response.setEstimatedPerCapita(draft.getEstimatedPerCapita());
        response.setSelectedPlanId(draft.getSelectedPlanId());
        FeRegistrationBatch batch = findRegistrationBatchByDraftId(draft.getId());
        response.setQrCodeUrl(batch != null ? batch.getQrCodeUrl() : null);
        response.setQrToken(batch != null ? batch.getQrToken() : null);
        response.setFields(fields.stream().map(this::toFieldResponse).collect(Collectors.toList()));
        response.setCreateTime(draft.getCreateTime());
        response.setUpdateTime(draft.getUpdateTime());
        return response;
    }

    /** 草稿确认后关联订单 → 登记批次；未确认则为 null。 */
    private FeRegistrationBatch findRegistrationBatchByDraftId(Long draftId) {
        if (draftId == null) {
            return null;
        }
        FeEmploymentOrder order = feEmploymentOrderMapper.selectOne(new LambdaQueryWrapper<FeEmploymentOrder>()
                .eq(FeEmploymentOrder::getDraftId, draftId)
                .orderByDesc(FeEmploymentOrder::getId)
                .last("LIMIT 1"));
        if (order == null) {
            return null;
        }
        return feRegistrationBatchMapper.selectOne(new LambdaQueryWrapper<FeRegistrationBatch>()
                .eq(FeRegistrationBatch::getOrderId, order.getId())
                .orderByDesc(FeRegistrationBatch::getId)
                .last("LIMIT 1"));
    }

    private DraftFieldResponse toFieldResponse(FeDraftField entity) {
        DraftFieldResponse response = new DraftFieldResponse();
        response.setFieldKey(entity.getFieldKey());
        response.setFieldLabel(entity.getFieldLabel());
        response.setFieldValue(entity.getFieldValue());
        response.setValueJson(entity.getValueJson());
        response.setSource(entity.getSource());
        response.setEditable(entity.getEditable());
        response.setIsDefault(entity.getIsDefault());
        return response;
    }

    private String resolveFieldValue(FeEmploymentDraft draft, String fieldKey) {
        switch (fieldKey) {
            case "worker_count":
                return draft.getWorkerCount() != null ? String.valueOf(draft.getWorkerCount()) : null;
            case "work_days":
                return draft.getWorkDays() != null ? String.valueOf(draft.getWorkDays()) : null;
            case "work_type":
                return draft.getWorkType();
            case "work_content":
                return draft.getWorkContent();
            case "work_location":
                return draft.getWorkLocation();
            case "settlement_mode":
                return draft.getSettlementMode();
            case "daily_wage":
                return draft.getDailyWage() != null ? draft.getDailyWage().toPlainString() : null;
            case "job_requirements":
                return draft.getJobRequirements();
            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;
    }
}
