package com.huimv.employment.service.registration;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.huimv.employment.common.exception.BizException;
import com.huimv.employment.common.exception.ErrorCode;
import com.huimv.employment.dao.entity.FeContract;
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.entity.FeSettlementOrder;
import com.huimv.employment.dao.entity.FeWorkerRegistration;
import com.huimv.employment.dao.entity.FeWorkflowDefinition;
import com.huimv.employment.dao.entity.FeWorkflowStep;
import com.huimv.employment.dao.mapper.FeContractMapper;
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.dao.mapper.FeSettlementOrderMapper;
import com.huimv.employment.dao.mapper.FeWorkerRegistrationMapper;
import com.huimv.employment.dao.mapper.FeWorkflowDefinitionMapper;
import com.huimv.employment.dao.mapper.FeWorkflowStepMapper;
import com.huimv.employment.service.config.RegistrationQrProperties;
import com.huimv.employment.service.conversation.ConversationService;
import com.huimv.employment.service.enterprise.EnterpriseService;
import com.huimv.employment.service.order.OrderWorkItemService;
import com.huimv.employment.service.registration.dto.RegistrationBatchDetailResponse;
import com.huimv.employment.service.registration.dto.RegistrationBatchProgressResponse;
import com.huimv.employment.service.registration.dto.RegistrationBatchStatsResponse;
import com.huimv.employment.service.registration.dto.RegistrationBatchStepResponse;
import com.huimv.employment.service.registration.dto.RegistrationBatchWorkerItemResponse;
import com.huimv.employment.service.wechat.WeChatMiniAppService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

/**
 * 登记批次：确认方案后创建批次与小程序码；企业端查询进度详情。
 * <p>办理流程步骤来自启用中的 {@code fe_workflow_definition} + {@code fe_workflow_step}。</p>
 */
@Service
public class RegistrationBatchService {

    private static final Logger log = LoggerFactory.getLogger(RegistrationBatchService.class);

    private static final String STATUS_ACTIVE = "active";
    private static final String STEP_REGISTRATION = "registration";
    private static final String PUBLIC_PATH_TEMPLATE = "/api/v1/public/registration-qr/%s.png";
    private static final Pattern SAFE_SCENE = Pattern.compile("^[A-Za-z0-9_-]{1,32}$");
    private static final Pattern NUMERIC_ID = Pattern.compile("^\\d+$");
    private static final DateTimeFormatter STEP_TIME_FMT = DateTimeFormatter.ofPattern("MM-dd HH:mm");

    private static final String STEP_STATUS_DONE = "done";
    private static final String STEP_STATUS_ACTIVE = "active";
    private static final String STEP_STATUS_PENDING = "pending";

    /** 产品六步：有临时工 / 无临时工第三步互斥 */
    private static final String DISPLAY_DRAFT = "draft";
    private static final String DISPLAY_ENTERPRISE_AUTH = "enterprise_auth";
    private static final String DISPLAY_WORKER_REGISTRATION = "worker_registration";
    private static final String DISPLAY_WORKER_RECRUITMENT = "worker_recruitment";
    private static final String DISPLAY_TASK_PUBLISH = "task_publish";
    private static final String DISPLAY_SETTLEMENT = "settlement";
    private static final String DISPLAY_INVOICE_ARCHIVE = "invoice_archive";
    private static final String WORKER_SOURCE_RECRUIT = "recruit";
    private static final String ENTERPRISE_AUTH_VERIFIED = "verified";

    private static final String REG_PENDING = "pending";
    private static final String REG_REGISTERING = "registering";
    private static final String REG_UNDER_REVIEW = "under_review";
    private static final String REG_CONTRACT_SIGNING = "contract_signing";
    private static final String REG_COMPLETED = "completed";
    private static final String REG_VERIFY_FAILED = "verify_failed";
    private static final String REG_REJECTED = "rejected";

    private static final String WORKFLOW_CODE_EMPLOYMENT = "employment_compliance";

    private final FeRegistrationBatchMapper feRegistrationBatchMapper;
    private final FeEmploymentOrderMapper feEmploymentOrderMapper;
    private final FeEmploymentDraftMapper feEmploymentDraftMapper;
    private final FeWorkerRegistrationMapper feWorkerRegistrationMapper;
    private final FeContractMapper feContractMapper;
    private final FeSettlementOrderMapper feSettlementOrderMapper;
    private final FeWorkflowDefinitionMapper feWorkflowDefinitionMapper;
    private final FeWorkflowStepMapper feWorkflowStepMapper;
    private final EnterpriseService enterpriseService;
    private final ConversationService conversationService;
    private final WeChatMiniAppService weChatMiniAppService;
    private final RegistrationQrProperties qrProperties;
    private final OrderWorkItemService orderWorkItemService;

    public RegistrationBatchService(FeRegistrationBatchMapper feRegistrationBatchMapper,
                                    FeEmploymentOrderMapper feEmploymentOrderMapper,
                                    FeEmploymentDraftMapper feEmploymentDraftMapper,
                                    FeWorkerRegistrationMapper feWorkerRegistrationMapper,
                                    FeContractMapper feContractMapper,
                                    FeSettlementOrderMapper feSettlementOrderMapper,
                                    FeWorkflowDefinitionMapper feWorkflowDefinitionMapper,
                                    FeWorkflowStepMapper feWorkflowStepMapper,
                                    EnterpriseService enterpriseService,
                                    ConversationService conversationService,
                                    WeChatMiniAppService weChatMiniAppService,
                                    RegistrationQrProperties qrProperties,
                                    OrderWorkItemService orderWorkItemService) {
        this.feRegistrationBatchMapper = feRegistrationBatchMapper;
        this.feEmploymentOrderMapper = feEmploymentOrderMapper;
        this.feEmploymentDraftMapper = feEmploymentDraftMapper;
        this.feWorkerRegistrationMapper = feWorkerRegistrationMapper;
        this.feContractMapper = feContractMapper;
        this.feSettlementOrderMapper = feSettlementOrderMapper;
        this.feWorkflowDefinitionMapper = feWorkflowDefinitionMapper;
        this.feWorkflowStepMapper = feWorkflowStepMapper;
        this.enterpriseService = enterpriseService;
        this.conversationService = conversationService;
        this.weChatMiniAppService = weChatMiniAppService;
        this.qrProperties = qrProperties;
        this.orderWorkItemService = orderWorkItemService;
    }

    /**
     * 一单一主批次：落批次 + 生成小程序码 + 推进订单步骤到 registration。
     */
    public FeRegistrationBatch createForOrder(FeEmploymentOrder order, FeEmploymentDraft draft, LocalDateTime now) {
        if (order == null || order.getId() == null) {
            throw new BizException(ErrorCode.BAD_REQUEST, "订单不存在，无法创建登记批次");
        }
        if (!StringUtils.hasText(order.getOrderNo()) || !SAFE_SCENE.matcher(order.getOrderNo().trim()).matches()) {
            throw new BizException(ErrorCode.BAD_REQUEST, "订单编号无效，无法作为小程序码 scene");
        }
        String scene = order.getOrderNo().trim();
        int validDays = resolveValidDays(draft);
        String page = normalizePage(qrProperties.getPage());
        String registrationUrl = page + "?scene=" + scene;

        FeRegistrationBatch batch = new FeRegistrationBatch();
        batch.setBatchNo("tmp_" + scene);
        batch.setOrderId(order.getId());
        batch.setEnterpriseId(order.getEnterpriseId());
        int expected = orderWorkItemService.sumWorkerCountByOrderId(order.getId());
        batch.setExpectedCount(expected);
        batch.setRegisteredCount(0);
        batch.setPendingCount(expected);
        batch.setInProgressCount(0);
        batch.setCompletedCount(0);
        batch.setAbnormalCount(0);
        batch.setRegistrationUrl(registrationUrl);
        batch.setQrToken(scene);
        batch.setValidFrom(now);
        batch.setValidUntil(now.plusDays(validDays));
        batch.setStatus(STATUS_ACTIVE);
        batch.setCreateTime(now);
        batch.setUpdateTime(now);
        batch.setDelFlag(0);

        try {
            feRegistrationBatchMapper.insert(batch);
        } catch (DuplicateKeyException ex) {
            throw new BizException(ErrorCode.BAD_REQUEST, "登记批次编号冲突，请重试");
        }
        batch.setBatchNo(String.format("bat_%d", batch.getId()));
        feRegistrationBatchMapper.updateById(batch);

        attachQrCode(batch, scene, now);
        advanceOrderToRegistration(order.getId(), now);
        return feRegistrationBatchMapper.selectById(batch.getId());
    }

    /**
     * 按草稿 ID 查询办理流程与当前进度（草稿 → 订单 → 登记批次）。
     * <p>按草稿 user_id 归属校验；企业未认证时也可查询已确认草稿的进度。</p>
     */
    public RegistrationBatchDetailResponse getDetailByDraftId(Long userId, Long draftId) {
        if (draftId == null) {
            throw new BizException(ErrorCode.BAD_REQUEST, "draft_id 不能为空");
        }
        FeEmploymentDraft draft = feEmploymentDraftMapper.selectById(draftId);
        if (draft == null || draft.getUserId() == null || !draft.getUserId().equals(userId)) {
            throw new BizException(ErrorCode.NOT_FOUND, "草稿不存在");
        }
        FeEnterprise enterprise = enterpriseService.findEnterpriseByUserId(userId);
        if (draft.getEnterpriseId() != null && enterprise != null
                && !draft.getEnterpriseId().equals(enterprise.getId())) {
            throw new BizException(ErrorCode.NOT_FOUND, "草稿不存在");
        }
        FeEmploymentOrder order = feEmploymentOrderMapper.selectOne(new LambdaQueryWrapper<FeEmploymentOrder>()
                .eq(FeEmploymentOrder::getDraftId, draftId)
                .orderByDesc(FeEmploymentOrder::getId)
                .last("LIMIT 1"));
        if (order == null) {
            throw new BizException(ErrorCode.NOT_FOUND, "尚未确认方案，暂无办理进度");
        }
        if (order.getUserId() != null && !order.getUserId().equals(userId)) {
            throw new BizException(ErrorCode.NOT_FOUND, "草稿不存在");
        }
        FeRegistrationBatch batch = feRegistrationBatchMapper.selectOne(new LambdaQueryWrapper<FeRegistrationBatch>()
                .eq(FeRegistrationBatch::getOrderId, order.getId())
                .orderByDesc(FeRegistrationBatch::getId)
                .last("LIMIT 1"));
        if (batch == null) {
            throw new BizException(ErrorCode.NOT_FOUND, "登记批次不存在");
        }
        if (batch.getEnterpriseId() != null && enterprise != null
                && !batch.getEnterpriseId().equals(enterprise.getId())) {
            throw new BizException(ErrorCode.NOT_FOUND, "登记批次不存在");
        }
        return toDetailResponse(batch, order, draft);
    }

    /**
     * 按会话查询其下全部草稿的登记进度详情。
     * <p>元素结构与 {@link #getDetailByDraftId} 相同；尚未确认方案、无批次的草稿跳过。</p>
     */
    public List<RegistrationBatchDetailResponse> listDetailsByConversation(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<RegistrationBatchDetailResponse> result = new ArrayList<>();
        if (drafts == null || drafts.isEmpty()) {
            return result;
        }
        for (FeEmploymentDraft draft : drafts) {
            try {
                result.add(getDetailByDraftId(userId, draft.getId()));
            } catch (BizException ignored) {
                // 未确认方案或无批次时跳过
            }
        }
        return result;
    }

    /**
     * 企业端登记进度详情：按批次主键 ID 或业务编号（如 bat_1）。
     */
    public RegistrationBatchDetailResponse getDetail(Long userId, String batchKey) {
        FeEnterprise enterprise = enterpriseService.findEnterpriseByUserId(userId);
        if (enterprise == null) {
            throw new BizException(ErrorCode.NOT_FOUND, "企业信息不存在");
        }
        FeRegistrationBatch batch = requireOwnedBatch(enterprise.getId(), batchKey);
        FeEmploymentOrder order = feEmploymentOrderMapper.selectById(batch.getOrderId());
        if (order == null) {
            throw new BizException(ErrorCode.NOT_FOUND, "用工订单不存在");
        }
        FeEmploymentDraft draft = order.getDraftId() != null
                ? feEmploymentDraftMapper.selectById(order.getDraftId()) : null;
        return toDetailResponse(batch, order, draft);
    }

    private RegistrationBatchDetailResponse toDetailResponse(FeRegistrationBatch batch,
                                                             FeEmploymentOrder order,
                                                             FeEmploymentDraft draft) {
        List<FeWorkerRegistration> registrations = feWorkerRegistrationMapper.selectList(
                new LambdaQueryWrapper<FeWorkerRegistration>()
                        .eq(FeWorkerRegistration::getBatchId, batch.getId())
                        .orderByAsc(FeWorkerRegistration::getId));

        RegistrationBatchStatsResponse stats = buildStats(batch, registrations);
        RegistrationBatchProgressResponse progress = buildProgress(stats);
        String workerSource = resolveWorkerSource(order, draft);
        FeEnterprise enterprise = loadEnterprise(order, draft);
        boolean enterpriseVerified = isEnterpriseVerified(enterprise);
        String displayStepCode = resolveDisplayStepCode(
                order.getCurrentStepCode(), enterpriseVerified, workerSource, stats);
        List<RegistrationBatchStepResponse> steps = buildSteps(
                order, draft, batch, registrations, progress, displayStepCode, enterprise);
        List<RegistrationBatchWorkerItemResponse> workers = buildWorkers(registrations);

        String title = StringUtils.hasText(order.getTitle()) ? order.getTitle().trim() : "用工批次";
        String batchNo = batch.getBatchNo();

        RegistrationBatchDetailResponse response = new RegistrationBatchDetailResponse();
        response.setDraftId(draft != null ? draft.getId() : order.getDraftId());
        response.setBatchId(batch.getId());
        response.setBatchNo(batchNo);
        response.setTitle(title);
        response.setSubtitle(title + (StringUtils.hasText(batchNo) ? " " + batchNo : ""));
        response.setOrderId(order.getId());
        response.setOrderNo(order.getOrderNo());
        response.setStatus(batch.getStatus());
        response.setQrCodeUrl(batch.getQrCodeUrl());
        response.setRegistrationUrl(batch.getRegistrationUrl());
        response.setQrToken(batch.getQrToken());
        response.setValidFrom(batch.getValidFrom());
        response.setValidUntil(batch.getValidUntil());
        // 进度展示用有效节点：登记招满后展示 task_publish（库内仍可能是 registration）
        response.setCurrentStepCode(displayStepCode);
        response.setStats(stats);
        response.setProgress(progress);
        response.setSteps(steps);
        response.setWorkers(workers);
        return response;
    }

    /** 按订单编号（scene）读取本地小程序码 PNG；不存在返回 null。 */
    public byte[] readQrPng(String scene) {
        if (!StringUtils.hasText(scene) || !SAFE_SCENE.matcher(scene.trim()).matches()) {
            return null;
        }
        Path file = resolveQrFile(scene.trim());
        if (!Files.isRegularFile(file)) {
            return null;
        }
        try {
            return Files.readAllBytes(file);
        } catch (IOException ex) {
            log.warn("读取小程序码失败 scene={}", scene, ex);
            return null;
        }
    }

    private FeRegistrationBatch requireOwnedBatch(Long enterpriseId, String batchKey) {
        if (!StringUtils.hasText(batchKey)) {
            throw new BizException(ErrorCode.BAD_REQUEST, "batch_id 不能为空");
        }
        String key = batchKey.trim();
        FeRegistrationBatch batch = null;
        if (NUMERIC_ID.matcher(key).matches()) {
            try {
                batch = feRegistrationBatchMapper.selectById(Long.parseLong(key));
            } catch (NumberFormatException ignored) {
                batch = null;
            }
        }
        if (batch == null) {
            batch = feRegistrationBatchMapper.selectOne(new LambdaQueryWrapper<FeRegistrationBatch>()
                    .eq(FeRegistrationBatch::getBatchNo, key)
                    .orderByDesc(FeRegistrationBatch::getId)
                    .last("LIMIT 1"));
        }
        if (batch == null || !enterpriseId.equals(batch.getEnterpriseId())) {
            throw new BizException(ErrorCode.NOT_FOUND, "登记批次不存在");
        }
        return batch;
    }

    private RegistrationBatchStatsResponse buildStats(FeRegistrationBatch batch,
                                                      List<FeWorkerRegistration> registrations) {
        int expected = batch.getExpectedCount() != null ? batch.getExpectedCount() : 0;
        int completed = 0;
        int inProgress = 0;
        int abnormal = 0;
        int appliedPending = 0;
        if (registrations != null) {
            for (FeWorkerRegistration reg : registrations) {
                String status = reg.getRegStatus();
                if (REG_COMPLETED.equals(status) || REG_CONTRACT_SIGNING.equals(status)) {
                    completed++;
                } else if (REG_VERIFY_FAILED.equals(status) || REG_REJECTED.equals(status)) {
                    abnormal++;
                } else if (REG_PENDING.equals(status)) {
                    appliedPending++;
                } else {
                    inProgress++;
                }
            }
        }
        int applied = registrations != null ? registrations.size() : 0;
        int pending = Math.max(0, expected - applied) + appliedPending;
        inProgress = inProgress + abnormal;
        if (expected <= 0) {
            expected = pending + inProgress + completed;
        }
        int registered = Math.max(0, expected - pending);

        RegistrationBatchStatsResponse stats = new RegistrationBatchStatsResponse();
        stats.setPendingCount(pending);
        stats.setInProgressCount(inProgress);
        stats.setCompletedCount(completed);
        stats.setAbnormalCount(abnormal);
        stats.setExpectedCount(expected);
        stats.setRegisteredCount(registered);
        return stats;
    }

    private RegistrationBatchProgressResponse buildProgress(RegistrationBatchStatsResponse stats) {
        int total = stats.getExpectedCount() != null ? stats.getExpectedCount() : 0;
        int current = stats.getRegisteredCount() != null ? stats.getRegisteredCount() : 0;
        int percent = total > 0 ? (int) Math.round(current * 100.0 / total) : 0;
        if (percent > 100) {
            percent = 100;
        }
        RegistrationBatchProgressResponse progress = new RegistrationBatchProgressResponse();
        progress.setCurrent(current);
        progress.setTotal(total);
        progress.setPercent(percent);
        return progress;
    }

    private List<RegistrationBatchStepResponse> buildSteps(FeEmploymentOrder order,
                                                           FeEmploymentDraft draft,
                                                           FeRegistrationBatch batch,
                                                           List<FeWorkerRegistration> registrations,
                                                           RegistrationBatchProgressResponse progress,
                                                           String displayCode,
                                                           FeEnterprise enterprise) {
        String workerSource = resolveWorkerSource(order, draft);
        List<FeWorkflowStep> flowSteps = filterStepsByWorkerSource(listActiveWorkflowSteps(), workerSource);
        if (flowSteps.isEmpty()) {
            log.warn("未配置启用的 fe_workflow_step，进度页办理流程为空");
            return Collections.emptyList();
        }

        String internalCode = StringUtils.hasText(order.getCurrentStepCode())
                ? order.getCurrentStepCode().trim() : STEP_REGISTRATION;
        int currentIndex = indexOfStep(flowSteps, displayCode);
        if (currentIndex < 0) {
            currentIndex = indexOfStep(flowSteps, DISPLAY_WORKER_REGISTRATION);
        }
        if (currentIndex < 0) {
            currentIndex = indexOfStep(flowSteps, DISPLAY_WORKER_RECRUITMENT);
        }
        if (currentIndex < 0) {
            currentIndex = 0;
        }

        StepTimeContext timeCtx = buildStepTimeContext(order, draft, batch, registrations, enterprise);

        List<RegistrationBatchStepResponse> steps = new ArrayList<>();
        // 仅办结（completed）时当前开票归档节点也显示 done；支付后停在 invoice_archive 为 active
        boolean terminalCompleted = "completed".equals(internalCode);
        for (int i = 0; i < flowSteps.size(); i++) {
            FeWorkflowStep def = flowSteps.get(i);
            String code = def.getStepCode();
            RegistrationBatchStepResponse step = new RegistrationBatchStepResponse();
            step.setStepCode(code);
            step.setTitle(def.getStepName());
            if (i < currentIndex || (terminalCompleted && i == currentIndex)) {
                step.setStatus(STEP_STATUS_DONE);
                // 仅已完成节点展示真实业务时间；未发生的节点不回退伪造时间
                step.setTime(formatStepTime(timeCtx.resolve(code)));
            } else if (i == currentIndex) {
                step.setStatus(STEP_STATUS_ACTIVE);
                if ((DISPLAY_WORKER_REGISTRATION.equals(code) || DISPLAY_WORKER_RECRUITMENT.equals(code)
                        || STEP_REGISTRATION.equals(code))
                        && progress != null) {
                    step.setHighlight(String.format("进行中 %d/%d",
                            progress.getCurrent() != null ? progress.getCurrent() : 0,
                            progress.getTotal() != null ? progress.getTotal() : 0));
                }
            } else {
                step.setStatus(STEP_STATUS_PENDING);
            }
            steps.add(step);
        }
        return steps;
    }

    /**
     * 进度条当前展示节点：登记/招聘人数达标后，即使库内仍为 registration，也前进到 task_publish。
     */
    private static String resolveDisplayStepCode(String internalCode,
                                                 boolean enterpriseVerified,
                                                 String workerSource,
                                                 RegistrationBatchStatsResponse stats) {
        String displayCode = mapToDisplayStepCode(internalCode, enterpriseVerified, workerSource);
        if ((DISPLAY_WORKER_REGISTRATION.equals(displayCode) || DISPLAY_WORKER_RECRUITMENT.equals(displayCode))
                && isWorkerPhaseComplete(stats)) {
            return DISPLAY_TASK_PUBLISH;
        }
        return displayCode;
    }

    /** 计划人数已全部就绪（completed / contract_signing 计入完成）。 */
    private static boolean isWorkerPhaseComplete(RegistrationBatchStatsResponse stats) {
        if (stats == null) {
            return false;
        }
        int expected = stats.getExpectedCount() != null ? stats.getExpectedCount() : 0;
        int completed = stats.getCompletedCount() != null ? stats.getCompletedCount() : 0;
        return expected > 0 && completed >= expected;
    }

    private FeEnterprise loadEnterprise(FeEmploymentOrder order, FeEmploymentDraft draft) {
        Long enterpriseId = order != null ? order.getEnterpriseId() : null;
        if (enterpriseId == null && draft != null) {
            enterpriseId = draft.getEnterpriseId();
        }
        if (enterpriseId == null) {
            return null;
        }
        try {
            return enterpriseService.requireById(enterpriseId);
        } catch (BizException ignored) {
            return null;
        }
    }

    private static boolean isEnterpriseVerified(FeEnterprise enterprise) {
        return enterprise != null && ENTERPRISE_AUTH_VERIFIED.equals(enterprise.getAuthStatus());
    }

    private static String resolveWorkerSource(FeEmploymentOrder order, FeEmploymentDraft draft) {
        if (order != null && StringUtils.hasText(order.getWorkerSource())) {
            return order.getWorkerSource().trim();
        }
        if (draft != null && StringUtils.hasText(draft.getWorkerSource())) {
            return draft.getWorkerSource().trim();
        }
        return "existing";
    }

    /**
     * 按 worker_source 只保留一条第三步：existing→人员登记，recruit→临时工招聘。
     */
    private static List<FeWorkflowStep> filterStepsByWorkerSource(List<FeWorkflowStep> flowSteps,
                                                                  String workerSource) {
        if (flowSteps == null || flowSteps.isEmpty()) {
            return Collections.emptyList();
        }
        boolean recruit = WORKER_SOURCE_RECRUIT.equals(workerSource);
        List<FeWorkflowStep> filtered = new ArrayList<>();
        for (FeWorkflowStep step : flowSteps) {
            String code = step.getStepCode();
            if (DISPLAY_WORKER_RECRUITMENT.equals(code) && !recruit) {
                continue;
            }
            if (DISPLAY_WORKER_REGISTRATION.equals(code) && recruit) {
                continue;
            }
            filtered.add(step);
        }
        return filtered;
    }

    /**
     * 将订单内部步骤码映射为产品六步展示码（状态机仍用内部码，进度条用展示码）。
     * <p>未企业认证（auth_status≠verified）时停在 enterprise_auth，不向前推进。</p>
     */
    private static String mapToDisplayStepCode(String internalCode,
                                               boolean enterpriseVerified,
                                               String workerSource) {
        String branch = WORKER_SOURCE_RECRUIT.equals(workerSource)
                ? DISPLAY_WORKER_RECRUITMENT : DISPLAY_WORKER_REGISTRATION;
        if (!enterpriseVerified) {
            if (DISPLAY_DRAFT.equals(internalCode) || "draft_created".equals(internalCode)) {
                return DISPLAY_DRAFT;
            }
            return DISPLAY_ENTERPRISE_AUTH;
        }
        if (!StringUtils.hasText(internalCode)) {
            return branch;
        }
        switch (internalCode) {
            case DISPLAY_DRAFT:
            case "draft_created":
                return DISPLAY_DRAFT;
            case "plan_confirmed":
            case DISPLAY_ENTERPRISE_AUTH:
                return branch;
            case STEP_REGISTRATION:
            case "enterprise_audit":
            case "contract_signing":
            case DISPLAY_WORKER_REGISTRATION:
            case DISPLAY_WORKER_RECRUITMENT:
                return branch;
            case DISPLAY_TASK_PUBLISH:
                return DISPLAY_TASK_PUBLISH;
            case "settlement_pending":
            case DISPLAY_SETTLEMENT:
                return DISPLAY_SETTLEMENT;
            case "settlement_paid":
                // 历史已支付码：进度上已离开结算打款，进入开票归档
            case DISPLAY_INVOICE_ARCHIVE:
                return DISPLAY_INVOICE_ARCHIVE;
            case "completed":
                return DISPLAY_INVOICE_ARCHIVE;
            default:
                return branch;
        }
    }

    /**
     * 预聚合各合规节点真实业务时间；未发生的节点保持 null，禁止用草稿时间伪造。
     */
    private StepTimeContext buildStepTimeContext(FeEmploymentOrder order,
                                                 FeEmploymentDraft draft,
                                                 FeRegistrationBatch batch,
                                                 List<FeWorkerRegistration> registrations,
                                                 FeEnterprise enterprise) {
        StepTimeContext ctx = new StepTimeContext();
        if (order != null) {
            ctx.orderCreated = order.getCreateTime();
        }
        if (draft != null) {
            ctx.draftCreated = draft.getCreateTime();
            ctx.planConfirmed = draft.getConfirmedAt();
        }
        if (ctx.draftCreated == null) {
            ctx.draftCreated = ctx.orderCreated;
        }

        // 仅 verified 才记企业认证完成时间
        if (isEnterpriseVerified(enterprise)) {
            ctx.enterpriseAuth = enterprise.getAuthTime() != null
                    ? enterprise.getAuthTime() : enterprise.getUpdateTime();
        }

        // 人员登记/招聘完成时间：仅取 reg_status=completed 的最晚提交时间；无人完成则不展示
        if (registrations != null) {
            for (FeWorkerRegistration reg : registrations) {
                if (reg == null) {
                    continue;
                }
                if (reg.getReviewedAt() != null) {
                    ctx.workerReviewed = earliest(ctx.workerReviewed, reg.getReviewedAt());
                }
                if (!REG_COMPLETED.equals(reg.getRegStatus())) {
                    continue;
                }
                LocalDateTime joinedAt = reg.getSubmittedAt() != null
                        ? reg.getSubmittedAt()
                        : (reg.getCreateTime() != null ? reg.getCreateTime() : reg.getReviewedAt());
                ctx.registration = latest(ctx.registration, joinedAt);
            }
        }

        if (order != null && order.getId() != null) {
            List<FeContract> contracts = feContractMapper.selectList(
                    new LambdaQueryWrapper<FeContract>()
                            .eq(FeContract::getOrderId, order.getId())
                            .ne(FeContract::getSignStatus, "voided"));
            if (contracts != null) {
                for (FeContract c : contracts) {
                    if (c.getCreateTime() != null) {
                        ctx.contractSigningStart = earliest(ctx.contractSigningStart, c.getCreateTime());
                    }
                    if ("signed".equals(c.getSignStatus()) && c.getSignedAt() != null) {
                        ctx.contractSigned = latest(ctx.contractSigned, c.getSignedAt());
                    }
                }
            }

            FeSettlementOrder settlement = feSettlementOrderMapper.selectOne(
                    new LambdaQueryWrapper<FeSettlementOrder>()
                            .eq(FeSettlementOrder::getOrderId, order.getId())
                            .ne(FeSettlementOrder::getSettlementStatus, "cancelled")
                            .orderByDesc(FeSettlementOrder::getId)
                            .last("LIMIT 1"));
            if (settlement != null) {
                ctx.settlementPending = settlement.getCreateTime();
                ctx.settlementPaid = settlement.getPaidAt() != null
                        ? settlement.getPaidAt() : settlement.getConfirmedAt();
            }

            if ("completed".equals(order.getCurrentStepCode())) {
                ctx.completed = order.getUpdateTime() != null ? order.getUpdateTime() : order.getCreateTime();
            }
            // 任务发布时间：有合同签署用签署时间；否则回退人员登记完成时间 / 订单更新时间
            String step = order.getCurrentStepCode();
            if (DISPLAY_TASK_PUBLISH.equals(step) || "settlement_pending".equals(step)
                    || "settlement_paid".equals(step) || "completed".equals(step)
                    || DISPLAY_SETTLEMENT.equals(step) || DISPLAY_INVOICE_ARCHIVE.equals(step)) {
                ctx.workStarted = ctx.contractSigned != null
                        ? ctx.contractSigned
                        : (ctx.registration != null ? ctx.registration : order.getUpdateTime());
            }
        }
        return ctx;
    }

    private static LocalDateTime earliest(LocalDateTime a, LocalDateTime b) {
        if (a == null) {
            return b;
        }
        if (b == null) {
            return a;
        }
        return a.isBefore(b) ? a : b;
    }

    private static LocalDateTime latest(LocalDateTime a, LocalDateTime b) {
        if (a == null) {
            return b;
        }
        if (b == null) {
            return a;
        }
        return a.isAfter(b) ? a : b;
    }

    private static String formatStepTime(LocalDateTime time) {
        return time != null ? STEP_TIME_FMT.format(time) : null;
    }

    private static final class StepTimeContext {
        private LocalDateTime orderCreated;
        private LocalDateTime draftCreated;
        private LocalDateTime planConfirmed;
        private LocalDateTime enterpriseAuth;
        private LocalDateTime registration;
        private LocalDateTime workerReviewed;
        private LocalDateTime contractSigningStart;
        private LocalDateTime contractSigned;
        private LocalDateTime workStarted;
        private LocalDateTime settlementPending;
        private LocalDateTime settlementPaid;
        private LocalDateTime completed;

        private LocalDateTime resolve(String stepCode) {
            if (!StringUtils.hasText(stepCode)) {
                return null;
            }
            // 只返回该节点真实业务时间；没有发生过则 null（禁止用草稿时间冒充）
            if (DISPLAY_DRAFT.equals(stepCode) || "draft_created".equals(stepCode)) {
                return draftCreated != null ? draftCreated : orderCreated;
            }
            if (DISPLAY_ENTERPRISE_AUTH.equals(stepCode)) {
                return enterpriseAuth;
            }
            if ("plan_confirmed".equals(stepCode)) {
                return planConfirmed;
            }
            if (DISPLAY_WORKER_REGISTRATION.equals(stepCode)
                    || DISPLAY_WORKER_RECRUITMENT.equals(stepCode)
                    || "registration".equals(stepCode)) {
                return registration;
            }
            if ("enterprise_audit".equals(stepCode)) {
                return workerReviewed;
            }
            if ("contract_signing".equals(stepCode)) {
                return contractSigned != null ? contractSigned : contractSigningStart;
            }
            if (DISPLAY_TASK_PUBLISH.equals(stepCode)) {
                return workStarted;
            }
            if (DISPLAY_SETTLEMENT.equals(stepCode) || "settlement_pending".equals(stepCode)) {
                return settlementPending;
            }
            if ("settlement_paid".equals(stepCode)) {
                return settlementPaid;
            }
            if (DISPLAY_INVOICE_ARCHIVE.equals(stepCode) || "completed".equals(stepCode)) {
                return completed;
            }
            return null;
        }
    }

    /**
     * 读取当前启用合规流程下的步骤定义（按 step_order 升序）。
     */
    private List<FeWorkflowStep> listActiveWorkflowSteps() {
        FeWorkflowDefinition definition = feWorkflowDefinitionMapper.selectOne(
                new LambdaQueryWrapper<FeWorkflowDefinition>()
                        .eq(FeWorkflowDefinition::getWorkflowCode, WORKFLOW_CODE_EMPLOYMENT)
                        .eq(FeWorkflowDefinition::getIsActive, Boolean.TRUE)
                        .orderByDesc(FeWorkflowDefinition::getVersion)
                        .last("LIMIT 1"));
        if (definition == null) {
            // 兼容初始化脚本未显式筛选时：取 employment_compliance 最新版
            definition = feWorkflowDefinitionMapper.selectOne(
                    new LambdaQueryWrapper<FeWorkflowDefinition>()
                            .eq(FeWorkflowDefinition::getWorkflowCode, WORKFLOW_CODE_EMPLOYMENT)
                            .orderByDesc(FeWorkflowDefinition::getVersion)
                            .last("LIMIT 1"));
        }
        if (definition == null) {
            definition = feWorkflowDefinitionMapper.selectOne(
                    new LambdaQueryWrapper<FeWorkflowDefinition>()
                            .eq(FeWorkflowDefinition::getIsActive, Boolean.TRUE)
                            .orderByDesc(FeWorkflowDefinition::getVersion)
                            .last("LIMIT 1"));
        }
        if (definition == null) {
            return Collections.emptyList();
        }
        List<FeWorkflowStep> steps = feWorkflowStepMapper.selectList(
                new LambdaQueryWrapper<FeWorkflowStep>()
                        .eq(FeWorkflowStep::getWorkflowId, definition.getId())
                        .orderByAsc(FeWorkflowStep::getStepOrder)
                        .orderByAsc(FeWorkflowStep::getId));
        return steps != null ? steps : Collections.<FeWorkflowStep>emptyList();
    }

    private static int indexOfStep(List<FeWorkflowStep> flowSteps, String stepCode) {
        if (!StringUtils.hasText(stepCode) || flowSteps == null) {
            return -1;
        }
        for (int i = 0; i < flowSteps.size(); i++) {
            if (stepCode.equals(flowSteps.get(i).getStepCode())) {
                return i;
            }
        }
        return -1;
    }

    private List<RegistrationBatchWorkerItemResponse> buildWorkers(List<FeWorkerRegistration> registrations) {
        List<RegistrationBatchWorkerItemResponse> workers = new ArrayList<>();
        if (registrations == null) {
            return workers;
        }
        for (FeWorkerRegistration reg : registrations) {
            RegistrationBatchWorkerItemResponse item = new RegistrationBatchWorkerItemResponse();
            item.setRegistrationId(reg.getId());
            item.setWorkerId(reg.getWorkerId());
            item.setRealName(reg.getRealName());
            item.setConfirmedWorkType(reg.getConfirmedWorkType());
            item.setThirdPartySource(reg.getThirdPartySource());
            item.setMobileMask(reg.getMobileMask());
            item.setRegStatus(reg.getRegStatus());
            item.setStatusTag(toStatusTag(reg.getRegStatus()));
            item.setStatusText(toStatusText(reg.getRegStatus()));
            item.setFailReason(reg.getFailReason());
            item.setSubmittedAt(reg.getSubmittedAt());
            workers.add(item);
        }
        return workers;
    }

    private static String toStatusTag(String regStatus) {
        if (REG_COMPLETED.equals(regStatus)) {
            return "done";
        }
        if (REG_CONTRACT_SIGNING.equals(regStatus)) {
            return "contract";
        }
        if (REG_VERIFY_FAILED.equals(regStatus) || REG_REJECTED.equals(regStatus)) {
            return "fail";
        }
        if (REG_UNDER_REVIEW.equals(regStatus)) {
            return "review";
        }
        if (REG_REGISTERING.equals(regStatus)) {
            return "registering";
        }
        return "pending";
    }

    private static String toStatusText(String regStatus) {
        if (REG_COMPLETED.equals(regStatus)) {
            return "已完成";
        }
        if (REG_CONTRACT_SIGNING.equals(regStatus)) {
            return "待签约";
        }
        if (REG_REJECTED.equals(regStatus)) {
            return "已驳回";
        }
        if (REG_VERIFY_FAILED.equals(regStatus)) {
            return "核验失败";
        }
        if (REG_UNDER_REVIEW.equals(regStatus)) {
            return "审核中";
        }
        if (REG_REGISTERING.equals(regStatus)) {
            return "登记中";
        }
        return "待登记";
    }

    private void attachQrCode(FeRegistrationBatch batch, String scene, LocalDateTime now) {
        try {
            byte[] png;
            try {
                png = weChatMiniAppService.createRegistrationWxaCode(scene);
            } catch (BizException ex) {
                if (!qrProperties.isFallbackMockOnError()) {
                    throw ex;
                }
                log.error("订单 orderId={} 微信小程序码失败，降级 Mock PNG: {}",
                        batch.getOrderId(), ex.getMessage());
                png = weChatMiniAppService.createRegistrationWxaCodeForFallback(scene);
            }
            Path file = resolveQrFile(scene);
            Files.createDirectories(file.getParent());
            Files.write(file, png);

            String qrCodeUrl = buildPublicQrUrl(scene);
            FeRegistrationBatch update = new FeRegistrationBatch();
            update.setId(batch.getId());
            update.setQrCodeUrl(qrCodeUrl);
            update.setUpdateTime(now);
            feRegistrationBatchMapper.updateById(update);
            batch.setQrCodeUrl(qrCodeUrl);
        } catch (BizException ex) {
            log.error("订单 orderId={} 生成登记小程序码失败: {}", batch.getOrderId(), ex.getMessage());
            throw ex;
        } catch (Exception ex) {
            log.error("订单 orderId={} 写入登记小程序码失败", batch.getOrderId(), ex);
            throw new BizException(ErrorCode.INTERNAL_ERROR, "写入登记小程序码失败");
        }
    }

    private void advanceOrderToRegistration(Long orderId, LocalDateTime now) {
        FeEmploymentOrder update = new FeEmploymentOrder();
        update.setId(orderId);
        update.setCurrentStepCode(STEP_REGISTRATION);
        update.setUpdateTime(now);
        feEmploymentOrderMapper.updateById(update);
    }

    private int resolveValidDays(FeEmploymentDraft draft) {
        return Math.max(1, qrProperties.getDefaultValidDays());
    }

    private static String normalizePage(String page) {
        if (!StringUtils.hasText(page)) {
            return "pages/worker/job-offer";
        }
        String p = page.trim();
        if (p.startsWith("/")) {
            p = p.substring(1);
        }
        int q = p.indexOf('?');
        if (q >= 0) {
            p = p.substring(0, q);
        }
        return p;
    }

    private Path resolveQrFile(String scene) {
        return Paths.get(qrProperties.getStoragePath()).toAbsolutePath().normalize()
                .resolve(scene + ".png");
    }

    private String buildPublicQrUrl(String scene) {
        String relative = String.format(PUBLIC_PATH_TEMPLATE, scene);
        String prefix = qrProperties.getPublicUrlPrefix();
        if (!StringUtils.hasText(prefix)) {
            return relative;
        }
        String base = prefix.endsWith("/") ? prefix.substring(0, prefix.length() - 1) : prefix.trim();
        return base + relative;
    }
}
