package com.hotwj.platform.reportStatistics.service.impl;

import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hotwj.platform.reportStatistics.domain.bo.ReportLedgerQueryBo;
import com.hotwj.platform.reportStatistics.domain.vo.*;
import com.hotwj.platform.reportStatistics.mapper.LedgerReportMapper;
import com.hotwj.platform.reportStatistics.pdf.OfficialInformationSealPostProcessor;
import com.hotwj.platform.reportStatistics.provider.cover.DailyTrainingCoverProvider;
import com.hotwj.platform.reportStatistics.provider.security.*;
import com.hotwj.platform.reportStatistics.service.IReportDataProvider;
import com.hotwj.platform.reportStatistics.service.ISecurityManageFilePrintService;
import com.hotwj.platform.reportStatistics.service.ReportPrintJob;
import com.hotwj.platform.reportStatistics.service.ReportRenderService;
import com.hotwj.platform.reportStatistics.verify.service.QrCodeService;
import com.hotwj.platform.reportStatistics.verify.service.ReportVerifyTicketService;
import com.hotwj.platform.securityManagement.hiddenDangerInspection.domain.vo.HotHiddenDangerInspectionVo;
import com.hotwj.platform.securityManagement.hiddenDangerInspection.service.IHotHiddenDangerInspectionService;
import com.hotwj.platform.securityManagement.safetyInvestment.domain.vo.HotSafetyInvestmentVo;
import com.hotwj.platform.securityManagement.safetyInvestment.service.IHotSafetyInvestmentService;
import com.hotwj.platform.securityManagement.securityMeeting.domain.vo.HotSecurityMeetingVo;
import com.hotwj.platform.securityManagement.securityMeeting.service.IHotSecurityMeetingService;
import com.hotwj.platform.securityManagement.serviceQualityComplaint.domain.vo.HotServiceQualityComplaintVo;
import com.hotwj.platform.securityManagement.serviceQualityComplaint.service.IHotServiceQualityComplaintService;
import com.hotwj.platform.securityManagement.training.service.IHotTrainingService;
import com.hotwj.platform.securityManagement.violationInfo.domain.vo.HotViolationInfoVo;
import com.hotwj.platform.securityManagement.violationInfo.service.IHotViolationInfoService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.service.OssService;
import org.dromara.common.core.utils.StringUtils;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 安全生产管理档案打印服务实现类
 *
 * @author shihongwei
 * @date 2026-01-08
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class SecurityManageFilePrintServiceImpl implements ISecurityManageFilePrintService {

    private final LedgerReportMapper ledgerReportMapper;
    private final ReportRenderService reportRenderService;
    private final IHotTrainingService trainingService;
    private final IHotViolationInfoService violationInfoService;
    private final IHotHiddenDangerInspectionService hiddenDangerInspectionService;
    private final IHotSecurityMeetingService securityMeetingService;
    private final IHotSafetyInvestmentService safetyInvestmentService;
    private final IHotServiceQualityComplaintService serviceQualityComplaintService;
    private final OssService ossService;
    private final OfficialInformationSealPostProcessor officialInformationSealPostProcessor;
    private final ReportVerifyTicketService reportVerifyTicketService;
    private final QrCodeService qrCodeService;

    private final List<IReportDataProvider> allProviders;
    private Map<Class<? extends IReportDataProvider>, IReportDataProvider> providerMap;
    private Map<String, IReportDataProvider> providerTypeMap;

    @PostConstruct
    public void init() {
        providerMap = new HashMap<>();
        providerTypeMap = new HashMap<>();
        for (IReportDataProvider provider : allProviders) {
            providerMap.put(provider.getClass(), provider);
            if (StringUtils.isNotBlank(provider.getReportType())) {
                providerTypeMap.put(provider.getReportType(), provider);
            }
        }
    }

    private <T extends IReportDataProvider> T getProvider(Class<T> clazz) {
        return (T) providerMap.get(clazz);
    }

    @Override
    public byte[] generatePreJobTrainingPdf(ReportLedgerQueryBo bo) {
        List<PreJobTrainingLedgerVo> list = ledgerReportMapper.selectPreJobTrainingList(bo);
        if (list == null || list.isEmpty()) {
            throw new RuntimeException("暂无数据可打印");
        }

        PreJobTrainingProvider provider = getProvider(PreJobTrainingProvider.class);

        ReportPrintJob job = reportRenderService.createJob(key -> providerTypeMap.get(key))
                .withCompanyId(bo.getCompanyId())
                .addCover(provider, Map.of("companyId", bo.getCompanyId(), "coverName", "从业人员岗前培训考核卡"))
                .addBatch(provider, StringUtils.splitList(bo.getIds()), "driverId");

        return job.render();
    }

    @Override
    public byte[] generateSafetyEducationTrainingPdf(ReportLedgerQueryBo bo) {
        List<Long> trainingIds = resolveSafetyEducationTrainingIds(bo);
        if (trainingIds.isEmpty()) {
            throw new RuntimeException("没有可打印的培训计划");
        }

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(bo.getCompanyId());

        // Cover (Only once)
        DailyTrainingCoverProvider coverProvider = getProvider(DailyTrainingCoverProvider.class);
        job.add(coverProvider, Map.of("companyId", bo.getCompanyId()));

        SafetyEducationTrainingProvider provider = getProvider(SafetyEducationTrainingProvider.class);

        for (Long trainingId : trainingIds) {
            if (trainingId == null) continue;

            Map<String, Object> baseParams = new HashMap<>();
            baseParams.put("companyId", bo.getCompanyId());
            baseParams.put("trainingId", trainingId);

            // Detail
            Map<String, Object> detailParams = new HashMap<>(baseParams);
            detailParams.put("mode", "detail");
            job.add(provider, detailParams);

            // SignIn
            Map<String, Object> signInParams = new HashMap<>(baseParams);
            signInParams.put("mode", "signIn");
            job.add(provider, signInParams);
        }

        return job.render();
    }

    @Override
    public byte[] generateTrainingPlanMonthlyStatPdf(Long companyId, Long trainingId, List<String> userIds) {
        if (companyId == null) throw new RuntimeException("公司ID不能为空");
        if (trainingId == null) throw new RuntimeException("培训ID不能为空");

        Long ticketId = reportVerifyTicketService.createTicketId();
        String verifyUrl = reportVerifyTicketService.buildVerifyUrl("/public/verify/training/monthly", ticketId);
        String verifyQrBase64 = qrCodeService.generatePngDataUrl(verifyUrl, 300, 300);

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(companyId);

        TrainingPlanMonthlyStatProvider provider = getProvider(TrainingPlanMonthlyStatProvider.class);
        Map<String, Object> params = new HashMap<>();
        params.put("companyId", companyId);
        params.put("trainingId", trainingId);
        params.put("userIds", userIds);
        params.put("verifyUrl", verifyUrl);
        params.put("verifyQrBase64", verifyQrBase64);
        job.add(provider, params);
        job.useSeal(false);

        String snapshotHtml = reportRenderService.renderJobToHtml(job);
        String businessParamsJson = cn.hutool.json.JSONUtil.toJsonStr(params);
        reportVerifyTicketService.issueHtmlSnapshotTicket(ticketId, "TP_MONTHLY", companyId, businessParamsJson, snapshotHtml);

        return officialInformationSealPostProcessor.process(job.render());
    }

    @Override
    public byte[] generateStudyRecordPdf(Long companyId, Long trainingId, List<String> userIds) {
        if (companyId == null) throw new RuntimeException("公司ID不能为空");
        if (trainingId == null) throw new RuntimeException("培训ID不能为空");
        if (userIds == null || userIds.isEmpty()) throw new RuntimeException("请先选择要打印的人员");

        Long ticketId = reportVerifyTicketService.createTicketId();
        String verifyUrl = reportVerifyTicketService.buildVerifyUrl("/public/verify/training/studyRecord", ticketId);
        String verifyQrBase64 = qrCodeService.generatePngDataUrl(verifyUrl, 300, 300);

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(companyId);

        StudyRecordProvider provider = getProvider(StudyRecordProvider.class);

        for (String userId : userIds) {
            if (StringUtils.isBlank(userId)) continue;
            Map<String, Object> params = new HashMap<>();
            params.put("companyId", companyId);
            params.put("trainingId", trainingId);
            params.put("userId", userId.trim());
            params.put("verifyUrl", verifyUrl);
            params.put("verifyQrBase64", verifyQrBase64);
            job.add(provider, params);
        }

        job.useSeal(false);

        Map<String, Object> businessParams = new HashMap<>();
        businessParams.put("companyId", companyId);
        businessParams.put("trainingId", trainingId);
        businessParams.put("userIds", userIds);
        String snapshotHtml = reportRenderService.renderJobToHtml(job);
        reportVerifyTicketService.issueHtmlSnapshotTicket(ticketId, "STUDY_RECORD", companyId, cn.hutool.json.JSONUtil.toJsonStr(businessParams), snapshotHtml);

        return officialInformationSealPostProcessor.process(job.render());
    }

    @Override
    public byte[] generateSafetyFireHazardInspectionPdf(ReportLedgerQueryBo bo) {
        if (bo.getCompanyId() == null) throw new RuntimeException("公司ID不能为空");
        List<Long> ids = parseIds(bo.getIds());
        if (ids.isEmpty()) throw new RuntimeException("请选择要打印的隐患排查项");

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(bo.getCompanyId());

        SafetyFireHazardInspectionProvider provider = getProvider(SafetyFireHazardInspectionProvider.class);

        // Cover
        Map<String, Object> coverParams = new HashMap<>();
        coverParams.put("mode", "cover");
        coverParams.put("companyId", bo.getCompanyId());
        job.add(provider, coverParams);

        boolean hasRecord = false;
        for (Long id : ids) {
            if (id == null) continue;
            HotHiddenDangerInspectionVo inspection = hiddenDangerInspectionService.queryById(id);
            if (inspection == null || Objects.equals(inspection.getIsDeleted(), 1L)) continue;

            Map<String, Object> params = new HashMap<>();
            params.put("companyId", bo.getCompanyId());
            params.put("inspection", inspection);

            // Detail
            params.put("mode", "detail");
            job.add(provider, new HashMap<>(params));

            // Photo
            params.put("mode", "photo");
            job.add(provider, new HashMap<>(params));

            hasRecord = true;
        }

        if (!hasRecord) throw new RuntimeException("没有可打印的隐患排查记录");

        return job.render();
    }

    @Override
    public byte[] generateViolationInvestigationPdf(ReportLedgerQueryBo bo) {
        if (bo.getCompanyId() == null) throw new RuntimeException("公司ID不能为空");

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(bo.getCompanyId());

        ViolationInvestigationProvider provider = getProvider(ViolationInvestigationProvider.class);

        // Cover
        Map<String, Object> coverParams = new HashMap<>();
        coverParams.put("mode", "cover");
        coverParams.put("companyId", bo.getCompanyId());
        job.add(provider, coverParams);

        boolean multiPlan = StringUtils.isNotBlank(bo.getPlanName()) && bo.getPlanName().contains(",");

        if (multiPlan) {
            String[] plans = bo.getPlanName().split(",");
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M");
            ZoneId zoneId = ZoneId.systemDefault();
            Date originalStart = bo.getStartDate();
            Date originalEnd = bo.getEndDate();
            for (String raw : plans) {
                String p = raw == null ? "" : raw.trim();
                if (p.isEmpty()) continue;
                try {
                    YearMonth ym = YearMonth.parse(p, formatter);
                    LocalDate start = ym.atDay(1);
                    LocalDate end = ym.atEndOfMonth();
                    Date startDate = Date.from(start.atStartOfDay(zoneId).toInstant());
                    Date endDate = Date.from(end.atTime(23, 59, 59).atZone(zoneId).toInstant());
                    bo.setStartDate(startDate);
                    bo.setEndDate(endDate);
                    addViolationInvestigationDetail(job, provider, bo);
                } catch (Exception e) {
                    log.warn("解析违法违章排查记录计划名称失败: {}", p, e);
                }
            }
            bo.setStartDate(originalStart);
            bo.setEndDate(originalEnd);
        } else {
            if (bo.getStartDate() == null && bo.getEndDate() == null && StringUtils.isNotBlank(bo.getPlanName())) {
                try {
                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M");
                    YearMonth ym = YearMonth.parse(bo.getPlanName(), formatter);
                    LocalDate start = ym.atDay(1);
                    LocalDate end = ym.atEndOfMonth();
                    ZoneId zoneId = ZoneId.systemDefault();
                    bo.setStartDate(Date.from(start.atStartOfDay(zoneId).toInstant()));
                    bo.setEndDate(Date.from(end.atTime(23, 59, 59).atZone(zoneId).toInstant()));
                } catch (Exception e) {
                    log.warn("解析违法违章排查记录计划名称失败: {}", bo.getPlanName(), e);
                }
            }
            addViolationInvestigationDetail(job, provider, bo);
        }

        return job.render();
    }

    private void addViolationInvestigationDetail(ReportPrintJob job, ViolationInvestigationProvider provider, ReportLedgerQueryBo bo) {
        String investigateDate = buildDateRangeLabel(bo.getStartDate(), bo.getEndDate());
        Page<GovViolationInfoDetailVo> page = new Page<>(1, 9999);
        List<GovViolationInfoDetailVo> details = ledgerReportMapper.selectGovViolationInfoDetailPage(page, bo);
        String recorderName = "";
        if (CollUtil.isNotEmpty(details)) {
            recorderName = details
                    .stream()
                    .map(GovViolationInfoDetailVo::getRecorderName)
                    .filter(StringUtils::isNotBlank)
                    .distinct()
                    .collect(Collectors.joining("、"));
        }
        Map<String, Object> params = new HashMap<>();
        params.put("mode", "detail");
        params.put("companyId", bo.getCompanyId());
        params.put("managerName", recorderName);
        params.put("investigateDate", investigateDate);
        params.put("details", details);
        job.add(provider, params);
    }

    @Override
    public byte[] generateSafetyProductionInputPdf(ReportLedgerQueryBo bo) {
        if (bo.getCompanyId() == null) throw new RuntimeException("公司ID不能为空");
        List<Long> ids = parseIds(bo.getIds());
        if (ids.isEmpty()) throw new RuntimeException("请选择要打印的安全生产投入记录");

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(bo.getCompanyId());

        SafetyProductionInputProvider provider = getProvider(SafetyProductionInputProvider.class);

        List<HotSafetyInvestmentVo> records = new ArrayList<>();
        for (Long id : ids) {
            if (id == null) continue;
            HotSafetyInvestmentVo vo = safetyInvestmentService.queryById(id);
            if (vo == null || Objects.equals(vo.getIsDeleted(), 1L)) continue;
            records.add(vo);
        }
        if (records.isEmpty()) throw new RuntimeException("没有可打印的安全生产投入记录");

        String year = bo.getKeyword();
        if (StringUtils.isBlank(year)) {
            Date time = records.get(0).getInvestmentTime();
            if (time != null) {
                year = cn.hutool.core.date.DateUtil.format(time, "yyyy");
            } else {
                year = cn.hutool.core.date.DateUtil.format(new Date(), "yyyy");
            }
        }
        BigDecimal totalAmount = records.stream()
                .map(HotSafetyInvestmentVo::getInvestmentAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        StringBuilder receiptIds = new StringBuilder();
        for (HotSafetyInvestmentVo vo : records) {
            if (StringUtils.isBlank(vo.getReceiptAttachments())) continue;
            if (receiptIds.length() > 0) receiptIds.append(",");
            receiptIds.append(vo.getReceiptAttachments());
        }
        List<String> receiptImages = resolveInspectionImages(receiptIds.toString());

        Map<String, Object> baseParams = new HashMap<>();
        baseParams.put("companyId", bo.getCompanyId());
        baseParams.put("year", year);

        // Cover
        Map<String, Object> coverParams = new HashMap<>(baseParams);
        coverParams.put("mode", "cover");
        job.add(provider, coverParams);

        // Detail
        Map<String, Object> detailParams = new HashMap<>(baseParams);
        detailParams.put("mode", "detail");
        detailParams.put("totalAmount", totalAmount);
        detailParams.put("records", records);
        job.add(provider, detailParams);

        // Receipt
        Map<String, Object> receiptParams = new HashMap<>(baseParams);
        receiptParams.put("mode", "receipt");
        receiptParams.put("receiptImages", receiptImages);
        job.add(provider, receiptParams);

        return job.render();
    }

    @Override
    public byte[] generateViolationHandlingPdf(ReportLedgerQueryBo bo) {
        if (bo.getCompanyId() == null) throw new RuntimeException("公司ID不能为空");

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(bo.getCompanyId());

        ViolationHandlingProvider provider = getProvider(ViolationHandlingProvider.class);

        // Cover
        Map<String, Object> coverParams = new HashMap<>();
        coverParams.put("mode", "cover");
        coverParams.put("companyId", bo.getCompanyId());
        job.add(provider, coverParams);

        List<Long> ids = parseIds(bo.getIds());
        List<GovViolationHandlingLedgerVo> list = new ArrayList<>();
        if (!ids.isEmpty()) {
            for (Long id : ids) {
                if (id == null) continue;
                GovViolationHandlingLedgerVo vo = new GovViolationHandlingLedgerVo();
                vo.setId(id);
                list.add(vo);
            }
        } else {
            Page<GovViolationHandlingLedgerVo> page = new Page<>(1, 1000);
            List<GovViolationHandlingLedgerVo> rows = ledgerReportMapper.selectGovViolationHandlingPage(page, null, bo);
            if (rows != null) list.addAll(rows);
        }

        if (list.isEmpty()) throw new RuntimeException("没有可打印的违章处理记录");

        for (GovViolationHandlingLedgerVo vo : list) {
            if (vo == null || vo.getId() == null) continue;
            HotViolationInfoVo info = violationInfoService.queryById(vo.getId());
            if (info == null) continue;

            Map<String, Object> params = new HashMap<>();
            params.put("companyId", bo.getCompanyId());
            params.put("info", info);

            // Detail
            params.put("mode", "detail");
            job.add(provider, new HashMap<>(params));

            // Attachment
            params.put("mode", "attachment");
            job.add(provider, new HashMap<>(params));
        }

        return job.render();
    }

    @Override
    public byte[] generateGpsViolationPdf(ReportLedgerQueryBo bo) {
        if (bo.getCompanyId() == null) throw new RuntimeException("公司ID不能为空");

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(bo.getCompanyId());

        GpsViolationProvider provider = getProvider(GpsViolationProvider.class);

        // Cover
        Map<String, Object> coverParams = new HashMap<>();
        coverParams.put("mode", "cover");
        coverParams.put("companyId", bo.getCompanyId());
        job.add(provider, coverParams);

        List<Long> ids = parseIds(bo.getIds());
        List<GovGpsViolationLedgerVo> list = new ArrayList<>();
        if (!ids.isEmpty()) {
            for (Long id : ids) {
                if (id == null) continue;
                GovGpsViolationLedgerVo vo = new GovGpsViolationLedgerVo();
                vo.setId(id);
                list.add(vo);
            }
        } else {
            Page<GovGpsViolationLedgerVo> page = new Page<>(1, 1000);
            List<GovGpsViolationLedgerVo> rows = ledgerReportMapper.selectGovGpsViolationPage(page, null, bo);
            if (rows != null) list.addAll(rows);
        }

        if (list.isEmpty()) throw new RuntimeException("没有可打印的车辆动态监控违法行为记录");

        for (GovGpsViolationLedgerVo vo : list) {
            if (vo == null || vo.getId() == null) continue;
            HotViolationInfoVo info = violationInfoService.queryById(vo.getId());
            if (info == null) continue;

            Map<String, Object> params = new HashMap<>();
            params.put("companyId", bo.getCompanyId());
            params.put("info", info);

            // Detail
            params.put("mode", "detail");
            job.add(provider, new HashMap<>(params));

            // Attachment
            params.put("mode", "attachment");
            job.add(provider, new HashMap<>(params));
        }

        return job.render();
    }

    @Override
    public byte[] generateServiceQualityPdf(ReportLedgerQueryBo bo) {
        if (bo.getCompanyId() == null) throw new RuntimeException("公司ID不能为空");
        List<Long> ids = parseIds(bo.getIds());
        if (ids.isEmpty()) throw new RuntimeException("请选择要打印的服务质量记录");

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(bo.getCompanyId());

        ServiceQualityProvider provider = getProvider(ServiceQualityProvider.class);

        // Cover
        Map<String, Object> coverParams = new HashMap<>();
        coverParams.put("mode", "cover");
        coverParams.put("companyId", bo.getCompanyId());
        job.add(provider, coverParams);

        boolean hasRecord = false;
        for (Long id : ids) {
            if (id == null) continue;
            HotServiceQualityComplaintVo complaint = serviceQualityComplaintService.queryById(id);
            if (complaint == null || Objects.equals(complaint.getIsDeleted(), 1L)) continue;

            Map<String, Object> params = new HashMap<>();
            params.put("mode", "detail");
            params.put("companyId", bo.getCompanyId());
            params.put("complaint", complaint);
            job.add(provider, params);
            hasRecord = true;
        }

        if (!hasRecord) throw new RuntimeException("没有可打印的服务质量记录");

        return job.render();
    }

    @Override
    public byte[] generateSafetyMeetingPdf(ReportLedgerQueryBo bo) {
        if (bo.getCompanyId() == null) throw new RuntimeException("公司ID不能为空");
        List<Long> ids = parseIds(bo.getIds());
        if (ids.isEmpty()) throw new RuntimeException("会议ID不能为空");

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(bo.getCompanyId());

        SafetyMeetingProvider provider = getProvider(SafetyMeetingProvider.class);

        // Cover
        Map<String, Object> coverParams = new HashMap<>();
        coverParams.put("mode", "cover");
        coverParams.put("companyId", bo.getCompanyId());
        job.add(provider, coverParams);

        boolean hasMeeting = false;
        for (Long meetingId : ids) {
            if (meetingId == null) continue;
            HotSecurityMeetingVo meeting = securityMeetingService.queryById(meetingId);
            if (meeting == null) continue;

            Map<String, Object> params = new HashMap<>();
            params.put("companyId", bo.getCompanyId());
            params.put("meetingId", meetingId);

            // Detail
            params.put("mode", "detail");
            job.add(provider, new HashMap<>(params));

            // SignIn
            params.put("mode", "signIn");
            job.add(provider, new HashMap<>(params));

            hasMeeting = true;
        }

        if (!hasMeeting) throw new RuntimeException("没有可打印的会议记录");

        return job.render();
    }

    @Override
    public byte[] generateStudyDetailPdf(Long companyId, Long trainingId, List<String> userIds) {
        if (companyId == null) throw new RuntimeException("公司ID不能为空");
        if (trainingId == null) throw new RuntimeException("培训ID不能为空");
        if (userIds == null || userIds.isEmpty()) throw new RuntimeException("请先选择要打印的人员");

        Long ticketId = reportVerifyTicketService.createTicketId();
        String verifyUrl = reportVerifyTicketService.buildVerifyUrl("/public/verify/training/studyDetail", ticketId);
        String verifyQrBase64 = qrCodeService.generatePngDataUrl(verifyUrl, 300, 300);

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(companyId);

        StudyDetailProvider provider = getProvider(StudyDetailProvider.class);

        for (String userId : userIds) {
            if (StringUtils.isBlank(userId)) continue;
            Map<String, Object> params = new HashMap<>();
            params.put("companyId", companyId);
            params.put("trainingId", trainingId);
            params.put("userId", userId.trim());
            params.put("verifyUrl", verifyUrl);
            params.put("verifyQrBase64", verifyQrBase64);
            job.add(provider, params);
        }

        job.useSeal(false);

        Map<String, Object> businessParams = new HashMap<>();
        businessParams.put("companyId", companyId);
        businessParams.put("trainingId", trainingId);
        businessParams.put("userIds", userIds);
        String snapshotHtml = reportRenderService.renderJobToHtml(job);
        reportVerifyTicketService.issueHtmlSnapshotTicket(ticketId, "STUDY_DETAIL", companyId, cn.hutool.json.JSONUtil.toJsonStr(businessParams), snapshotHtml);

        return officialInformationSealPostProcessor.process(job.render());
    }

    @Override
    public byte[] generateOfflineTrainingPhotoPdf(Long companyId, List<Long> ids, Integer type) {
        if (companyId == null) throw new RuntimeException("公司ID不能为空");
        if (ids == null || ids.isEmpty()) throw new RuntimeException("请先选择要打印的记录");

        ReportPrintJob job = reportRenderService.createJob()
                .withCompanyId(companyId);

        boolean hasData = false;

        if (type == 2) {
            // 线下会议
            OfflineMeetingProvider provider = getProvider(OfflineMeetingProvider.class);
            for (Long meetingId : ids) {
                if (meetingId == null) continue;
                Map<String, Object> params = new HashMap<>();
                params.put("meetingId", meetingId);
                job.add(provider, params);
                hasData = true;
            }
        } else {
            // 默认线下培训
            OfflineTrainingProvider provider = getProvider(OfflineTrainingProvider.class);
            for (Long trainingId : ids) {
                if (trainingId == null) continue;
                Map<String, Object> params = new HashMap<>();
                params.put("trainingId", trainingId);
                job.add(provider, params);
                hasData = true;
            }
        }

        if (!hasData) throw new RuntimeException("没有可打印的记录");

        return job.render();
    }

    // Helper methods

    private List<Long> resolveSafetyEducationTrainingIds(ReportLedgerQueryBo bo) {
        List<Long> result = new ArrayList<>();
        if (bo == null) return result;
        if (StringUtils.isNotBlank(bo.getIds())) {
            return parseIds(bo.getIds());
        }
        Page<GovSafetyEducationTrainingVo> page = new Page<>(1, 1000);
        List<GovSafetyEducationTrainingVo> rows = ledgerReportMapper.selectGovSafetyEducationTrainingPage(page, bo);
        if (rows != null) {
            for (GovSafetyEducationTrainingVo vo : rows) {
                if (vo != null && vo.getId() != null) {
                    result.add(vo.getId());
                }
            }
        }
        return result;
    }

    private List<Long> parseIds(String ids) {
        List<Long> result = new ArrayList<>();
        if (StringUtils.isBlank(ids)) return result;
        for (String s : ids.split(",")) {
            String t = s == null ? "" : s.trim();
            if (!t.isEmpty()) {
                try {
                    result.add(Long.parseLong(t));
                } catch (Exception ignored) {
                }
            }
        }
        return result;
    }

    private String buildDateRangeLabel(Date startDate, Date endDate) {
        if (startDate == null && endDate == null) return "";
        String pattern = "yyyy-MM-dd";
        if (startDate != null && endDate != null) {
            String start = cn.hutool.core.date.DateUtil.format(startDate, pattern);
            String end = cn.hutool.core.date.DateUtil.format(endDate, pattern);
            if (start.equals(end)) return start;
            return start + " 至 " + end;
        }
        if (startDate != null) return cn.hutool.core.date.DateUtil.format(startDate, pattern);
        return cn.hutool.core.date.DateUtil.format(endDate, pattern);
    }

    private List<String> resolveTrainingPhotoImages(String photoAttachments) {
        List<String> result = new ArrayList<>();
        if (StringUtils.isBlank(photoAttachments)) return result;
        String urls = photoAttachments;
        if (!(photoAttachments.contains("http://") || photoAttachments.contains("https://"))) {
            urls = ossService.selectUrlByIds(photoAttachments);
        }
        return splitUrls(urls);
    }

    private List<String> resolveInspectionImages(String attachmentUrl) {
        List<String> result = new ArrayList<>();
        if (StringUtils.isBlank(attachmentUrl)) return result;
        String urls = attachmentUrl;
        if (!(attachmentUrl.contains("http://") || attachmentUrl.contains("https://"))) {
            urls = ossService.selectUrlByIds(attachmentUrl);
        }
        return splitUrls(urls);
    }

    private List<String> splitUrls(String urls) {
        List<String> result = new ArrayList<>();
        if (StringUtils.isBlank(urls)) return result;
        for (String s : urls.split(",")) {
            String t = s == null ? "" : s.trim();
            if (!t.isEmpty()) result.add(t);
        }
        return result;
    }
}
