package com.writeoff.module.template.service;

import com.writeoff.common.api.PageResult;
import com.writeoff.common.exception.BusinessException;
import com.writeoff.module.file.service.OssService;
import com.writeoff.module.template.dto.CreateTemplateRequest;
import com.writeoff.module.template.dto.CreateTemplateVersionRequest;
import com.writeoff.module.template.dto.RollbackTemplateRequest;
import com.writeoff.module.template.dto.TemplateUploadSignRequest;
import com.writeoff.module.template.model.TemplateDownloadLogInfo;
import com.writeoff.module.template.model.TemplateFlowLinkInfo;
import com.writeoff.module.template.model.TemplateInfo;
import com.writeoff.module.template.model.TemplateTypeOption;
import com.writeoff.module.template.model.TemplateVersionInfo;
import com.writeoff.security.AuthContext;
import com.writeoff.security.PermissionService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.sql.PreparedStatement;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

@Service
public class TemplateService {
    private final JdbcTemplate jdbcTemplate;
    private final OssService ossService;
    private final PermissionService permissionService;
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final Set<String> SUPPORTED_TEMPLATE_TYPES = new HashSet<String>(Arrays.asList(
            "AGENDA", "SIGN_IN", "INVITATION", "OTHER"
    ));

    private static final RowMapper<TemplateInfo> TEMPLATE_ROW_MAPPER = (rs, n) -> new TemplateInfo(
            rs.getLong("id"),
            rs.getString("template_name"),
            rs.getString("template_type"),
            rs.getString("scope_type"),
            rs.getObject("project_id") == null ? null : rs.getLong("project_id"),
            rs.getObject("meeting_id") == null ? null : rs.getLong("meeting_id"),
            rs.getObject("scope_id") == null ? null : rs.getLong("scope_id"),
            rs.getString("biz_scene"),
            rs.getString("status"),
            rs.getInt("current_version_no"),
            rs.getString("current_object_key"),
            rs.getString("effective_from"),
            rs.getString("effective_to"),
            rs.getInt("watermark_enabled") == 1,
            rs.getInt("download_rate_limit_per_hour"),
            rs.getString("created_at"),
            rs.getString("updated_at")
    );

    private static final RowMapper<TemplateVersionInfo> VERSION_ROW_MAPPER = (rs, n) -> new TemplateVersionInfo(
            rs.getLong("id"),
            rs.getLong("template_id"),
            rs.getInt("version_no"),
            rs.getString("object_key"),
            rs.getString("version_status"),
            rs.getInt("is_effective") == 1,
            rs.getString("change_log"),
            rs.getString("rollback_reason"),
            rs.getString("created_at")
    );
    private static final RowMapper<TemplateDownloadLogInfo> DOWNLOAD_LOG_ROW_MAPPER = (rs, n) -> new TemplateDownloadLogInfo(
            rs.getLong("id"),
            rs.getLong("template_id"),
            rs.getString("template_name"),
            rs.getInt("version_no"),
            rs.getLong("user_id"),
            rs.getString("user_name"),
            rs.getString("user_phone"),
            rs.getString("object_key"),
            rs.getString("download_type"),
            rs.getString("watermark_text"),
            rs.getObject("project_id") == null ? null : rs.getLong("project_id"),
            rs.getObject("meeting_id") == null ? null : rs.getLong("meeting_id"),
            rs.getString("ip"),
            rs.getString("user_agent"),
            rs.getString("downloaded_at")
    );
    private static final RowMapper<TemplateTypeOption> TYPE_OPTION_ROW_MAPPER = (rs, n) -> new TemplateTypeOption(
            rs.getString("type_code"),
            rs.getString("type_name"),
            rs.getString("status"),
            rs.getInt("sort_no")
    );
    private static final Set<String> SUPPORTED_SCENES = new HashSet<String>(Arrays.asList(
            "MEETING_RECOMMEND", "AUDIT_NOTIFY", "SETTLEMENT"
    ));

    public TemplateService(JdbcTemplate jdbcTemplate, OssService ossService, PermissionService permissionService) {
        this.jdbcTemplate = jdbcTemplate;
        this.ossService = ossService;
        this.permissionService = permissionService;
    }

    public PageResult<TemplateInfo> list(String templateName,
                                         String templateType,
                                         String status,
                                         String scopeType,
                                         String bizScene,
                                         Boolean watermarkEnabled,
                                         String effectiveStatus,
                                         int pageNo,
                                         int pageSize) {
        int safePage = Math.max(pageNo, 1);
        int safeSize = Math.min(Math.max(pageSize, 1), 100);
        int offset = (safePage - 1) * safeSize;
        String normalizedTemplateName = trimToNull(templateName);
        String normalizedTemplateType = normalizeOptionalTemplateType(templateType);
        String normalizedStatus = normalizeOptionalTemplateStatus(status);
        String normalizedScopeType = normalizeOptionalScope(scopeType);
        String normalizedBizScene = normalizeOptionalBizScene(bizScene);
        String normalizedEffectiveStatus = normalizeOptionalEffectiveStatus(effectiveStatus);
        StringBuilder whereSql = new StringBuilder(" WHERE t.tenant_id=? AND t.is_deleted=0");
        List<Object> whereArgs = new ArrayList<Object>();
        whereArgs.add(tenantId());
        if (normalizedTemplateName != null) {
            whereSql.append(" AND t.template_name LIKE ?");
            whereArgs.add("%" + normalizedTemplateName + "%");
        }
        if (normalizedTemplateType != null) {
            whereSql.append(" AND t.template_type=?");
            whereArgs.add(normalizedTemplateType);
        }
        if (normalizedStatus != null) {
            whereSql.append(" AND t.status=?");
            whereArgs.add(normalizedStatus);
        }
        if (normalizedScopeType != null) {
            whereSql.append(" AND t.scope_type=?");
            whereArgs.add(normalizedScopeType);
        }
        if (normalizedBizScene != null) {
            whereSql.append(" AND t.biz_scene=?");
            whereArgs.add(normalizedBizScene);
        }
        if (watermarkEnabled != null) {
            whereSql.append(" AND t.watermark_enabled=?");
            whereArgs.add(Boolean.TRUE.equals(watermarkEnabled) ? 1 : 0);
        }
        appendEffectiveStatusFilter(whereSql, normalizedEffectiveStatus);

        Integer total = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM template t" + whereSql,
                Integer.class,
                whereArgs.toArray()
        );
        long totalCount = total == null ? 0 : total;

        List<Object> dataArgs = new ArrayList<Object>(whereArgs);
        dataArgs.add(safeSize);
        dataArgs.add(offset);
        List<TemplateInfo> list = jdbcTemplate.query(
                templateSelectSql() +
                        whereSql +
                        " ORDER BY t.updated_at DESC, t.id DESC LIMIT ? OFFSET ?",
                TEMPLATE_ROW_MAPPER,
                dataArgs.toArray()
        );
        return new PageResult<>(list, totalCount, safePage, safeSize);
    }

    public List<TemplateInfo> listPublishedOptions(String bizScene) {
        String normalizedBizScene = normalizeOptionalBizScene(bizScene);
        StringBuilder sql = new StringBuilder(templateSelectSql())
                .append(" WHERE t.tenant_id=? AND t.is_deleted=0 AND t.status='PUBLISHED'");
        List<Object> args = new ArrayList<Object>();
        args.add(tenantId());
        if (normalizedBizScene != null) {
            sql.append(" AND t.biz_scene=?");
            args.add(normalizedBizScene);
        }
        sql.append(" AND ").append(effectiveNowSql("t"));
        sql.append(" ORDER BY t.updated_at DESC, t.id DESC");
        return jdbcTemplate.query(sql.toString(), TEMPLATE_ROW_MAPPER, args.toArray());
    }

    public List<TemplateInfo> listMatchedForMeeting(Long meetingId, Long projectId) {
        return jdbcTemplate.query(
                "SELECT t.*, tv.object_key AS current_object_key " +
                        "FROM template t " +
                        "LEFT JOIN template_version tv ON tv.tenant_id=t.tenant_id AND tv.template_id=t.id AND tv.version_no=t.current_version_no " +
                        "WHERE t.tenant_id=? AND t.is_deleted=0 AND t.status='PUBLISHED' AND t.biz_scene='MEETING_RECOMMEND' " +
                        "AND " + effectiveNowSql("t") + " AND (" +
                        "t.scope_type='ALL' OR (t.scope_type='PROJECT' AND t.project_id=?) OR (t.scope_type='MEETING' AND t.meeting_id=?)) " +
                        "ORDER BY CASE t.scope_type WHEN 'MEETING' THEN 1 WHEN 'PROJECT' THEN 2 ELSE 3 END, t.id DESC",
                TEMPLATE_ROW_MAPPER,
                tenantId(),
                projectId,
                meetingId
        );
    }

    public List<TemplateTypeOption> listTypeOptions() {
        return jdbcTemplate.query(
                "SELECT type_code, type_name, status, sort_no FROM template_type_option WHERE tenant_id=? ORDER BY sort_no ASC",
                TYPE_OPTION_ROW_MAPPER,
                tenantId()
        );
    }

    public void enableTypeOption(String typeCode) {
        String normalized = normalizeTemplateType(typeCode);
        assertTypeOptionExists(normalized);
        jdbcTemplate.update(
                "UPDATE template_type_option SET status='ENABLED', updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND type_code=?",
                tenantId(),
                normalized
        );
    }

    public void disableTypeOption(String typeCode) {
        String normalized = normalizeTemplateType(typeCode);
        assertTypeOptionExists(normalized);
        jdbcTemplate.update(
                "UPDATE template_type_option SET status='DISABLED', updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND type_code=?",
                tenantId(),
                normalized
        );
    }

    @Transactional(rollbackFor = Exception.class)
    public TemplateInfo create(CreateTemplateRequest request) {
        String scopeType = normalizeScope(request.getScopeType());
        String templateType = normalizeTemplateType(request.getTemplateType());
        String effectiveFrom = normalizeOptionalDateTime(request.getEffectiveFrom(), "effectiveFrom");
        String effectiveTo = normalizeOptionalDateTime(request.getEffectiveTo(), "effectiveTo");
        Integer downloadRateLimitPerHour = normalizeDownloadRateLimit(request.getDownloadRateLimitPerHour());
        assertEffectiveRangeValid(effectiveFrom, effectiveTo);
        assertTypeOptionEnabled(templateType);
        Long userId = safeUserId();
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(
                    "INSERT INTO template (tenant_id, template_name, template_type, scope_type, project_id, meeting_id, scope_id, biz_scene, status, current_version_no, effective_from, effective_to, watermark_enabled, download_rate_limit_per_hour, created_by, updated_by) " +
                            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'DRAFT', 1, STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), ?, ?, ?, ?)",
                    Statement.RETURN_GENERATED_KEYS
            );
            ps.setLong(1, tenantId());
            ps.setString(2, request.getTemplateName());
            ps.setString(3, templateType);
            ps.setString(4, scopeType);
            ps.setObject(5, request.getProjectId());
            ps.setObject(6, request.getMeetingId());
            ps.setObject(7, request.getScopeId());
            ps.setString(8, normalizeBizScene(request.getBizScene()));
            ps.setString(9, effectiveFrom);
            ps.setString(10, effectiveTo);
            ps.setInt(11, Boolean.TRUE.equals(request.getWatermarkEnabled()) ? 1 : 0);
            ps.setInt(12, downloadRateLimitPerHour);
            ps.setLong(13, userId);
            ps.setLong(14, userId);
            return ps;
        }, keyHolder);
        Long templateId = keyHolder.getKey() == null ? null : keyHolder.getKey().longValue();
        Long validTemplateId = templateId == null ? 0L : templateId;
        jdbcTemplate.update(
                "INSERT INTO template_version (tenant_id, template_id, version_no, object_key, version_status, is_effective, change_log, rollback_reason, created_by) " +
                        "VALUES (?, ?, 1, ?, 'DRAFT', 0, ?, NULL, ?)",
                tenantId(),
                validTemplateId,
                request.getObjectKey(),
                request.getChangeLog(),
                userId
        );
        return findById(validTemplateId);
    }

    public List<TemplateVersionInfo> versions(Long templateId) {
        assertTemplateExists(templateId);
        return jdbcTemplate.query(
                "SELECT * FROM template_version WHERE tenant_id=? AND template_id=? ORDER BY version_no DESC",
                VERSION_ROW_MAPPER,
                tenantId(),
                templateId
        );
    }

    public Map<String, Object> presignUpload(TemplateUploadSignRequest request) {
        String fileName = request.getFileName().trim();
        String ext = "";
        int idx = fileName.lastIndexOf('.');
        if (idx > 0 && idx < fileName.length() - 1) {
            ext = "." + fileName.substring(idx + 1).toLowerCase();
        }
        String templateType = normalizeTemplateType(request.getTemplateType());
        assertTypeOptionEnabled(templateType);
        String typeFolder = templateType.toLowerCase();
        Long userId = safeUserId();
        String objectKey = "template/" + typeFolder + "/" + userId + "/" + UUID.randomUUID().toString().replace("-", "") + ext;
        String contentType = normalizeContentType(request.getContentType());
        String putUrl = ossService.generateUploadUrl(objectKey, contentType);
        Map<String, Object> result = new LinkedHashMap<String, Object>();
        result.put("objectKey", objectKey);
        result.put("uploadUrl", putUrl);
        result.put("contentType", contentType);
        result.put("method", "PUT");
        result.put("templateType", templateType);
        return result;
    }

    @Transactional(rollbackFor = Exception.class)
    public TemplateInfo addVersion(Long templateId, CreateTemplateVersionRequest request) {
        TemplateInfo template = findById(templateId);
        assertTemplateEditable(template);
        Integer nextVersionNo = jdbcTemplate.queryForObject(
                "SELECT IFNULL(MAX(version_no), 0) + 1 FROM template_version WHERE tenant_id=? AND template_id=? FOR UPDATE",
                Integer.class,
                tenantId(),
                templateId
        );
        int versionNo = nextVersionNo == null ? 1 : nextVersionNo;
        jdbcTemplate.update(
                "INSERT INTO template_version (tenant_id, template_id, version_no, object_key, version_status, is_effective, change_log, rollback_reason, created_by) " +
                        "VALUES (?, ?, ?, ?, 'DRAFT', 0, ?, NULL, ?)",
                tenantId(),
                templateId,
                versionNo,
                request.getObjectKey(),
                request.getChangeLog(),
                safeUserId()
        );
        jdbcTemplate.update(
                "UPDATE template SET current_version_no=?, status='DRAFT', updated_by=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                versionNo,
                safeUserId(),
                tenantId(),
                templateId
        );
        return findById(templateId);
    }

    @Transactional(rollbackFor = Exception.class)
    public TemplateInfo publish(Long templateId) {
        TemplateInfo template = findById(templateId);
        assertEffectiveRangeValid(template.getEffectiveFrom(), template.getEffectiveTo());
        if ("ARCHIVED".equalsIgnoreCase(template.getStatus())) {
            throw new BusinessException(10003, "模板已归档，不能再次发布");
        }
        assertTypeOptionEnabled(template.getTemplateType());
        assertTemplateCurrentVersionReady(template);
        jdbcTemplate.update(
                "UPDATE template SET status='PUBLISHED', updated_by=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                safeUserId(),
                tenantId(),
                templateId
        );
        jdbcTemplate.update(
                "UPDATE template_version SET version_status='HISTORY', is_effective=0 WHERE tenant_id=? AND template_id=?",
                tenantId(),
                templateId
        );
        jdbcTemplate.update(
                "UPDATE template_version SET version_status='PUBLISHED', is_effective=1 WHERE tenant_id=? AND template_id=? AND version_no=?",
                tenantId(),
                templateId,
                template.getCurrentVersionNo()
        );
        return findById(templateId);
    }

    public TemplateInfo disable(Long templateId) {
        assertTemplateExists(templateId);
        jdbcTemplate.update(
                "UPDATE template SET status='DISABLED', updated_by=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                safeUserId(),
                tenantId(),
                templateId
        );
        return findById(templateId);
    }

    public TemplateInfo archive(Long templateId) {
        assertTemplateExists(templateId);
        jdbcTemplate.update(
                "UPDATE template SET status='ARCHIVED', updated_by=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                safeUserId(),
                tenantId(),
                templateId
        );
        return findById(templateId);
    }

    @Transactional(rollbackFor = Exception.class)
    public TemplateInfo rollback(Long templateId, RollbackTemplateRequest request) {
        TemplateInfo template = findById(templateId);
        if ("ARCHIVED".equalsIgnoreCase(template.getStatus())) {
            throw new BusinessException(10003, "已归档模板不允许回滚");
        }
        Integer versionCount = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM template_version WHERE tenant_id=? AND template_id=? AND version_no=?",
                Integer.class,
                tenantId(),
                templateId,
                request.getVersionNo()
        );
        if (versionCount == null || versionCount == 0) {
            throw new BusinessException(10003, "目标版本不存在");
        }
        String rollbackReason = normalizeRequiredText(request.getRollbackReason(), "rollbackReason");
        jdbcTemplate.update(
                "UPDATE template SET current_version_no=?, status='PUBLISHED', updated_by=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                request.getVersionNo(),
                safeUserId(),
                tenantId(),
                templateId
        );
        jdbcTemplate.update(
                "UPDATE template_version SET version_status='HISTORY', is_effective=0 WHERE tenant_id=? AND template_id=?",
                tenantId(),
                templateId
        );
        jdbcTemplate.update(
                "UPDATE template_version SET version_status='PUBLISHED', is_effective=1, rollback_reason=? WHERE tenant_id=? AND template_id=? AND version_no=?",
                rollbackReason,
                tenantId(),
                templateId,
                request.getVersionNo()
        );
        return findById(templateId);
    }

    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> download(Long templateId, String ip, String userAgent) {
        TemplateInfo template = findById(templateId);
        if ("DISABLED".equalsIgnoreCase(template.getStatus()) || "ARCHIVED".equalsIgnoreCase(template.getStatus())) {
            throw new BusinessException(10003, "模板已停用，无法下载");
        }
        if (template.getCurrentObjectKey() == null || template.getCurrentObjectKey().trim().isEmpty()) {
            throw new BusinessException(10003, "模板当前版本文件不存在");
        }
        String signedUrl = ossService.generateDownloadUrl(template.getCurrentObjectKey());
        Long userId = safeUserId();
        assertTemplateDownloadAllowed(template);
        assertTemplateEffectiveNow(template, "下载");
        assertDownloadRateLimit(template, userId);
        jdbcTemplate.update(
                "INSERT INTO template_download_log (tenant_id, template_id, version_no, user_id, object_key, download_type, watermark_text, project_id, meeting_id, ip, user_agent) " +
                        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                tenantId(),
                template.getId(),
                template.getCurrentVersionNo(),
                userId,
                template.getCurrentObjectKey(),
                "NORMAL",
                null,
                template.getProjectId(),
                template.getMeetingId(),
                ip,
                userAgent
        );
        Map<String, Object> result = new LinkedHashMap<String, Object>();
        result.put("templateId", template.getId());
        result.put("versionNo", template.getCurrentVersionNo());
        result.put("objectKey", template.getCurrentObjectKey());
        result.put("signedUrl", signedUrl);
        return result;
    }

    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> downloadWatermark(Long templateId, String watermarkText, String ip, String userAgent) {
        TemplateInfo template = findById(templateId);
        if ("DISABLED".equalsIgnoreCase(template.getStatus()) || "ARCHIVED".equalsIgnoreCase(template.getStatus())) {
            throw new BusinessException(10003, "模板已停用，无法下载");
        }
        if (template.getCurrentObjectKey() == null || template.getCurrentObjectKey().trim().isEmpty()) {
            throw new BusinessException(10003, "模板当前版本文件不存在");
        }
        String signedUrl = ossService.generateDownloadUrl(template.getCurrentObjectKey());
        Long userId = safeUserId();
        assertTemplateDownloadAllowed(template);
        if (!template.getWatermarkEnabled()) {
            throw new BusinessException(10003, "模板未启用水印下载");
        }
        assertTemplateEffectiveNow(template, "水印下载");
        assertDownloadRateLimit(template, userId);
        jdbcTemplate.update(
                "INSERT INTO template_download_log (tenant_id, template_id, version_no, user_id, object_key, download_type, watermark_text, project_id, meeting_id, ip, user_agent) " +
                        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                tenantId(),
                template.getId(),
                template.getCurrentVersionNo(),
                userId,
                template.getCurrentObjectKey(),
                "WATERMARK",
                watermarkText == null ? null : watermarkText.trim(),
                template.getProjectId(),
                template.getMeetingId(),
                ip,
                userAgent
        );
        Map<String, Object> result = new LinkedHashMap<String, Object>();
        result.put("templateId", template.getId());
        result.put("versionNo", template.getCurrentVersionNo());
        result.put("objectKey", template.getCurrentObjectKey());
        result.put("signedUrl", signedUrl);
        result.put("watermarkText", watermarkText == null ? "" : watermarkText.trim());
        return result;
    }

    public Map<String, Object> versionDiff(Long templateId, Integer leftVersionNo, Integer rightVersionNo) {
        assertTemplateExists(templateId);
        Integer latest = jdbcTemplate.queryForObject(
                "SELECT IFNULL(MAX(version_no), 0) FROM template_version WHERE tenant_id=? AND template_id=?",
                Integer.class,
                tenantId(),
                templateId
        );
        int right = rightVersionNo == null ? (latest == null ? 1 : latest) : rightVersionNo;
        int left = leftVersionNo == null ? Math.max(1, right - 1) : leftVersionNo;
        TemplateVersionInfo leftV = findVersion(templateId, left);
        TemplateVersionInfo rightV = findVersion(templateId, right);

        Map<String, Object> diff = new LinkedHashMap<String, Object>();
        diff.put("templateId", templateId);
        diff.put("leftVersionNo", left);
        diff.put("rightVersionNo", right);
        diff.put("leftObjectKey", leftV.getObjectKey());
        diff.put("rightObjectKey", rightV.getObjectKey());
        diff.put("leftStatus", leftV.getVersionStatus());
        diff.put("rightStatus", rightV.getVersionStatus());
        diff.put("leftChangeLog", leftV.getChangeLog());
        diff.put("rightChangeLog", rightV.getChangeLog());
        diff.put("objectKeyChanged", !safeEquals(leftV.getObjectKey(), rightV.getObjectKey()));
        diff.put("statusChanged", !safeEquals(leftV.getVersionStatus(), rightV.getVersionStatus()));
        diff.put("changeLogChanged", !safeEquals(leftV.getChangeLog(), rightV.getChangeLog()));
        return diff;
    }

    public PageResult<TemplateDownloadLogInfo> listDownloadLogs(Long templateId,
                                                                String templateName,
                                                                Long userId,
                                                                String userKeyword,
                                                                Integer versionNo,
                                                                String downloadType,
                                                                String ip,
                                                                String downloadedFrom,
                                                                String downloadedTo,
                                                                int pageNo,
                                                                int pageSize) {
        int safePage = Math.max(pageNo, 1);
        int safeSize = Math.min(Math.max(pageSize, 1), 100);
        int offset = (safePage - 1) * safeSize;
        String normalizedTemplateName = trimToNull(templateName);
        String normalizedUserKeyword = trimToNull(userKeyword);
        String normalizedDownloadType = normalizeOptionalDownloadType(downloadType);
        String normalizedIp = trimToNull(ip);
        String normalizedDownloadedFrom = trimToNull(downloadedFrom);
        String normalizedDownloadedTo = trimToNull(downloadedTo);
        Long currentUserId = userId();
        boolean canReadAllLogs = currentUserId != null
                && permissionService.hasPermission(currentUserId, "template.download.log.read.all");
        Long scopedUserId = canReadAllLogs ? userId : currentUserId;
        String scopedUserKeyword = canReadAllLogs ? normalizedUserKeyword : null;
        StringBuilder whereSql = new StringBuilder(
                " FROM template_download_log l " +
                        "LEFT JOIN template t ON t.tenant_id=l.tenant_id AND t.id=l.template_id " +
                        "LEFT JOIN sys_user u ON u.tenant_id=l.tenant_id AND u.id=l.user_id " +
                        "WHERE l.tenant_id=?"
        );
        List<Object> whereArgs = new ArrayList<Object>();
        whereArgs.add(tenantId());
        if (templateId != null) {
            whereSql.append(" AND l.template_id=?");
            whereArgs.add(templateId);
        }
        if (normalizedTemplateName != null) {
            whereSql.append(" AND t.template_name LIKE ?");
            whereArgs.add("%" + normalizedTemplateName + "%");
        }
        if (scopedUserId != null) {
            whereSql.append(" AND l.user_id=?");
            whereArgs.add(scopedUserId);
        }
        if (scopedUserKeyword != null) {
            whereSql.append(" AND (u.user_name LIKE ? OR u.phone LIKE ?)");
            String like = "%" + scopedUserKeyword + "%";
            whereArgs.add(like);
            whereArgs.add(like);
        }
        if (versionNo != null) {
            whereSql.append(" AND l.version_no=?");
            whereArgs.add(versionNo);
        }
        if (normalizedDownloadType != null) {
            whereSql.append(" AND l.download_type=?");
            whereArgs.add(normalizedDownloadType);
        }
        if (normalizedIp != null) {
            whereSql.append(" AND l.ip LIKE ?");
            whereArgs.add("%" + normalizedIp + "%");
        }
        if (normalizedDownloadedFrom != null) {
            whereSql.append(" AND l.downloaded_at >= STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s')");
            whereArgs.add(normalizedDownloadedFrom);
        }
        if (normalizedDownloadedTo != null) {
            whereSql.append(" AND l.downloaded_at <= STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s')");
            whereArgs.add(normalizedDownloadedTo);
        }

        Integer total = jdbcTemplate.queryForObject(
                "SELECT COUNT(1)" + whereSql,
                Integer.class,
                whereArgs.toArray()
        );
        long totalCount = total == null ? 0 : total;

        List<Object> dataArgs = new ArrayList<Object>(whereArgs);
        dataArgs.add(safeSize);
        dataArgs.add(offset);
        List<TemplateDownloadLogInfo> list = jdbcTemplate.query(
                "SELECT l.id, l.template_id, COALESCE(t.template_name, '') AS template_name, " +
                        "l.version_no, l.user_id, COALESCE(u.user_name, '') AS user_name, COALESCE(u.phone, '') AS user_phone, " +
                        "l.object_key, l.download_type, COALESCE(l.watermark_text, '') AS watermark_text, " +
                        "l.project_id, l.meeting_id, l.ip, l.user_agent, DATE_FORMAT(l.downloaded_at, '%Y-%m-%d %H:%i:%s') AS downloaded_at" +
                        whereSql +
                        " ORDER BY l.downloaded_at DESC, l.id DESC LIMIT ? OFFSET ?",
                DOWNLOAD_LOG_ROW_MAPPER,
                dataArgs.toArray()
        );
        return new PageResult<TemplateDownloadLogInfo>(list, totalCount, safePage, safeSize);
    }

    public List<Map<String, String>> flowSceneOptions() {
        return Arrays.asList(
                scene("MEETING_RECOMMEND", "会议推荐模板"),
                scene("AUDIT_NOTIFY", "审核通知模板"),
                scene("SETTLEMENT", "结算模板")
        );
    }

    public List<TemplateFlowLinkInfo> listFlowLinks() {
        return jdbcTemplate.query(
                "SELECT fl.scene_code, fl.template_id, t.template_name, t.status, t.current_version_no " +
                        "FROM template_flow_link fl " +
                        "LEFT JOIN template t ON t.tenant_id=fl.tenant_id AND t.id=fl.template_id " +
                        "WHERE fl.tenant_id=? ORDER BY fl.scene_code ASC",
                (rs, n) -> new TemplateFlowLinkInfo(
                        rs.getString("scene_code"),
                        sceneName(rs.getString("scene_code")),
                        rs.getLong("template_id"),
                        rs.getString("template_name"),
                        rs.getString("status"),
                        rs.getInt("current_version_no")
                ),
                tenantId()
        );
    }

    public TemplateFlowLinkInfo bindFlowLink(String sceneCode, Long templateId) {
        String scene = normalizeBizScene(sceneCode);
        TemplateInfo template = findById(templateId);
        assertPublishedTemplate(template, "流程绑定");
        assertTemplateSceneMatches(scene, template);
        assertTemplateEffectiveNow(template, "流程绑定");
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM template_flow_link WHERE tenant_id=? AND scene_code=?",
                Integer.class,
                tenantId(),
                scene
        );
        if (count == null || count == 0) {
            jdbcTemplate.update(
                    "INSERT INTO template_flow_link (tenant_id, scene_code, template_id, created_by, updated_by) VALUES (?, ?, ?, ?, ?)",
                    tenantId(),
                    scene,
                    templateId,
                    safeUserId(),
                    safeUserId()
            );
        } else {
            jdbcTemplate.update(
                    "UPDATE template_flow_link SET template_id=?, updated_by=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND scene_code=?",
                    templateId,
                    safeUserId(),
                    tenantId(),
                    scene
            );
        }
        return new TemplateFlowLinkInfo(
                scene,
                sceneName(scene),
                template.getId(),
                template.getTemplateName(),
                template.getStatus(),
                template.getCurrentVersionNo()
        );
    }

    private String templateSelectSql() {
        return "SELECT t.id, t.template_name, t.template_type, t.scope_type, t.project_id, t.meeting_id, t.scope_id, t.biz_scene, t.status, " +
                "t.current_version_no, tv.object_key AS current_object_key, " +
                "DATE_FORMAT(t.effective_from, '%Y-%m-%d %H:%i:%s') AS effective_from, " +
                "DATE_FORMAT(t.effective_to, '%Y-%m-%d %H:%i:%s') AS effective_to, " +
                "t.watermark_enabled, t.download_rate_limit_per_hour, " +
                "DATE_FORMAT(t.created_at, '%Y-%m-%d %H:%i:%s') AS created_at, " +
                "DATE_FORMAT(t.updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at " +
                "FROM template t " +
                "LEFT JOIN template_version tv ON tv.tenant_id=t.tenant_id AND tv.template_id=t.id AND tv.version_no=t.current_version_no ";
    }

    private void appendEffectiveStatusFilter(StringBuilder sql, String effectiveStatus) {
        if (effectiveStatus == null) {
            return;
        }
        if ("ACTIVE".equals(effectiveStatus)) {
            sql.append(" AND (t.effective_from IS NULL OR t.effective_from<=CURRENT_TIMESTAMP)");
            sql.append(" AND (t.effective_to IS NULL OR t.effective_to>=CURRENT_TIMESTAMP)");
            return;
        }
        if ("UPCOMING".equals(effectiveStatus)) {
            sql.append(" AND t.effective_from IS NOT NULL AND t.effective_from>CURRENT_TIMESTAMP");
            return;
        }
        sql.append(" AND t.effective_to IS NOT NULL AND t.effective_to<CURRENT_TIMESTAMP");
    }

    private String effectiveNowSql(String alias) {
        return "(" + alias + ".effective_from IS NULL OR " + alias + ".effective_from<=CURRENT_TIMESTAMP) " +
                "AND (" + alias + ".effective_to IS NULL OR " + alias + ".effective_to>=CURRENT_TIMESTAMP)";
    }

    private String normalizeScope(String scopeType) {
        String value = scopeType == null ? "ALL" : scopeType.trim().toUpperCase();
        if (!"ALL".equals(value) && !"PROJECT".equals(value) && !"MEETING".equals(value)) {
            throw new BusinessException(10003, "scopeType仅支持ALL/PROJECT/MEETING");
        }
        return value;
    }

    private String normalizeTemplateType(String templateType) {
        String value = templateType == null ? "OTHER" : templateType.trim().toUpperCase();
        if ("SIGN".equals(value)) {
            value = "SIGN_IN";
        } else if ("INVIT".equals(value) || "INVITATION_LETTER".equals(value)) {
            value = "INVITATION";
        }
        if (!SUPPORTED_TEMPLATE_TYPES.contains(value)) {
            throw new BusinessException(10003, "templateType仅支持AGENDA/SIGN_IN/INVITATION/OTHER");
        }
        return value;
    }

    private String normalizeBizScene(String sceneCode) {
        String value = sceneCode == null ? "MEETING_RECOMMEND" : sceneCode.trim().toUpperCase();
        if (!SUPPORTED_SCENES.contains(value)) {
            throw new BusinessException(10003, "bizScene仅支持MEETING_RECOMMEND/AUDIT_NOTIFY/SETTLEMENT");
        }
        return value;
    }

    private String normalizeOptionalScope(String scopeType) {
        String value = trimToNull(scopeType);
        return value == null ? null : normalizeScope(value);
    }

    private String normalizeOptionalTemplateType(String templateType) {
        String value = trimToNull(templateType);
        return value == null ? null : normalizeTemplateType(value);
    }

    private String normalizeOptionalBizScene(String bizScene) {
        String value = trimToNull(bizScene);
        return value == null ? null : normalizeBizScene(value);
    }

    private String normalizeOptionalTemplateStatus(String status) {
        String value = trimToNull(status);
        if (value == null) {
            return null;
        }
        String normalized = value.toUpperCase();
        if (!"DRAFT".equals(normalized) && !"PUBLISHED".equals(normalized)
                && !"DISABLED".equals(normalized) && !"ARCHIVED".equals(normalized)) {
            throw new BusinessException(10003, "status浠呮敮鎸丏RAFT/PUBLISHED/DISABLED/ARCHIVED");
        }
        return normalized;
    }

    private String normalizeOptionalEffectiveStatus(String effectiveStatus) {
        String value = trimToNull(effectiveStatus);
        if (value == null) {
            return null;
        }
        String normalized = value.toUpperCase();
        if (!"ACTIVE".equals(normalized) && !"UPCOMING".equals(normalized) && !"EXPIRED".equals(normalized)) {
            throw new BusinessException(10003, "effectiveStatus仅支持ACTIVE/UPCOMING/EXPIRED");
        }
        return normalized;
    }

    private String normalizeOptionalDownloadType(String downloadType) {
        String value = trimToNull(downloadType);
        if (value == null) {
            return null;
        }
        String normalized = value.toUpperCase();
        if (!"NORMAL".equals(normalized) && !"WATERMARK".equals(normalized)) {
            throw new BusinessException(10003, "downloadType仅支持NORMAL/WATERMARK");
        }
        return normalized;
    }

    private String normalizeContentType(String contentType) {
        if (contentType == null || contentType.trim().isEmpty()) {
            return "application/octet-stream";
        }
        return contentType.trim();
    }

    private String normalizeRequiredText(String value, String fieldName) {
        String normalized = trimToNull(value);
        if (normalized == null) {
            throw new BusinessException(10003, fieldName + "不能为空");
        }
        return normalized;
    }

    private String normalizeOptionalDateTime(String value, String fieldName) {
        String normalized = trimToNull(value);
        if (normalized == null) {
            return null;
        }
        try {
            return LocalDateTime.parse(normalized, DATE_TIME_FORMATTER).format(DATE_TIME_FORMATTER);
        } catch (DateTimeParseException ex) {
            throw new BusinessException(10003, fieldName + "格式应为yyyy-MM-dd HH:mm:ss");
        }
    }

    private Integer normalizeDownloadRateLimit(Integer downloadRateLimitPerHour) {
        if (downloadRateLimitPerHour == null) {
            return 100;
        }
        if (downloadRateLimitPerHour <= 0) {
            throw new BusinessException(10003, "downloadRateLimitPerHour必须大于0");
        }
        return downloadRateLimitPerHour;
    }

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

    private void assertTypeOptionExists(String typeCode) {
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM template_type_option WHERE tenant_id=? AND type_code=?",
                Integer.class,
                tenantId(),
                typeCode
        );
        if (count == null || count == 0) {
            throw new BusinessException(10003, "模板类型不存在");
        }
    }

    private void assertTypeOptionEnabled(String typeCode) {
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM template_type_option WHERE tenant_id=? AND type_code=? AND status='ENABLED'",
                Integer.class,
                tenantId(),
                typeCode
        );
        if (count == null || count == 0) {
            throw new BusinessException(10003, "模板类型已停用: " + typeCode);
        }
    }

    private void assertTemplateExists(Long templateId) {
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM template WHERE tenant_id=? AND id=? AND is_deleted=0",
                Integer.class,
                tenantId(),
                templateId
        );
        if (count == null || count == 0) {
            throw new BusinessException(10003, "模板不存在");
        }
    }

    private TemplateVersionInfo findVersion(Long templateId, Integer versionNo) {
        List<TemplateVersionInfo> list = jdbcTemplate.query(
                "SELECT * FROM template_version WHERE tenant_id=? AND template_id=? AND version_no=? LIMIT 1",
                VERSION_ROW_MAPPER,
                tenantId(),
                templateId,
                versionNo
        );
        if (list.isEmpty()) {
            throw new BusinessException(10003, "模板版本不存在");
        }
        return list.get(0);
    }

    private boolean safeEquals(String a, String b) {
        return (a == null && b == null) || (a != null && a.equals(b));
    }

    private void assertEffectiveRangeValid(String effectiveFrom, String effectiveTo) {
        if (effectiveFrom == null || effectiveTo == null) {
            return;
        }
        LocalDateTime start = LocalDateTime.parse(effectiveFrom, DATE_TIME_FORMATTER);
        LocalDateTime end = LocalDateTime.parse(effectiveTo, DATE_TIME_FORMATTER);
        if (start.isAfter(end)) {
            throw new BusinessException(10003, "生效开始时间不能晚于生效结束时间");
        }
    }

    private void assertPublishedTemplate(TemplateInfo template, String actionName) {
        if (!"PUBLISHED".equalsIgnoreCase(template.getStatus())) {
            throw new BusinessException(10003, actionName + "仅支持已发布模板");
        }
    }

    private void assertTemplateEditable(TemplateInfo template) {
        if ("ARCHIVED".equalsIgnoreCase(template.getStatus())) {
            throw new BusinessException(10003, "已归档模板不允许新增版本");
        }
    }

    private void assertTemplateCurrentVersionReady(TemplateInfo template) {
        if (template.getCurrentObjectKey() == null || template.getCurrentObjectKey().trim().isEmpty()) {
            throw new BusinessException(10003, "当前版本文件不存在，不能发布");
        }
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM template_version WHERE tenant_id=? AND template_id=? AND version_no=? AND object_key IS NOT NULL AND TRIM(object_key)<>''",
                Integer.class,
                tenantId(),
                template.getId(),
                template.getCurrentVersionNo()
        );
        if (count == null || count == 0) {
            throw new BusinessException(10003, "当前版本未找到可发布文件");
        }
    }

    private void assertTemplateSceneMatches(String expectedScene, TemplateInfo template) {
        if (!expectedScene.equalsIgnoreCase(template.getBizScene())) {
            throw new BusinessException(10003, "模板业务场景与绑定场景不一致");
        }
    }

    private void assertTemplateEffectiveNow(TemplateInfo template, String actionName) {
        LocalDateTime now = LocalDateTime.now();
        String effectiveFrom = trimToNull(template.getEffectiveFrom());
        if (effectiveFrom != null) {
            LocalDateTime start = LocalDateTime.parse(effectiveFrom, DATE_TIME_FORMATTER);
            if (start.isAfter(now)) {
                throw new BusinessException(10003, actionName + "失败，模板尚未生效");
            }
        }
        String effectiveTo = trimToNull(template.getEffectiveTo());
        if (effectiveTo != null) {
            LocalDateTime end = LocalDateTime.parse(effectiveTo, DATE_TIME_FORMATTER);
            if (end.isBefore(now)) {
                throw new BusinessException(10003, actionName + "失败，模板已过期");
            }
        }
    }

    private void assertTemplateDownloadAllowed(TemplateInfo template) {
        if ("DISABLED".equalsIgnoreCase(template.getStatus()) || "ARCHIVED".equalsIgnoreCase(template.getStatus())) {
            throw new BusinessException(10003, "模板已停用或归档，无法下载");
        }
        if (template.getCurrentObjectKey() == null || template.getCurrentObjectKey().trim().isEmpty()) {
            throw new BusinessException(10003, "模板当前版本文件不存在");
        }
    }

    private void assertDownloadRateLimit(TemplateInfo template, Long userId) {
        Integer limit = template.getDownloadRateLimitPerHour();
        if (limit == null || limit <= 0) {
            return;
        }
        Integer downloadedCount = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM template_download_log " +
                        "WHERE tenant_id=? AND template_id=? AND user_id=? AND downloaded_at>=DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 1 HOUR)",
                Integer.class,
                tenantId(),
                template.getId(),
                userId
        );
        int currentCount = downloadedCount == null ? 0 : downloadedCount;
        if (currentCount >= limit) {
            throw new BusinessException(10003, "当前小时下载次数已达上限");
        }
    }

    private TemplateInfo findById(Long templateId) {
        List<TemplateInfo> list = jdbcTemplate.query(
                "SELECT t.*, tv.object_key AS current_object_key " +
                        "FROM template t " +
                        "LEFT JOIN template_version tv ON tv.tenant_id=t.tenant_id AND tv.template_id=t.id AND tv.version_no=t.current_version_no " +
                        "WHERE t.tenant_id=? AND t.id=? AND t.is_deleted=0",
                TEMPLATE_ROW_MAPPER,
                tenantId(),
                templateId
        );
        if (list.isEmpty()) {
            throw new BusinessException(10003, "模板不存在");
        }
        return list.get(0);
    }

    private Map<String, String> scene(String code, String name) {
        Map<String, String> data = new LinkedHashMap<String, String>();
        data.put("sceneCode", code);
        data.put("sceneName", name);
        return data;
    }

    private String sceneName(String sceneCode) {
        if ("AUDIT_NOTIFY".equalsIgnoreCase(sceneCode)) {
            return "审核通知模板";
        }
        if ("SETTLEMENT".equalsIgnoreCase(sceneCode)) {
            return "结算模板";
        }
        return "会议推荐模板";
    }

    private Long safeUserId() {
        Long userId = AuthContext.userId();
        return userId == null ? 0L : userId;
    }

    private Long userId() {
        return AuthContext.userId();
    }

    private Long tenantId() {
        return AuthContext.requireTenantId();
    }
}
