package com.writeoff.module.notification.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.writeoff.common.api.PageResult;
import com.writeoff.common.exception.BusinessException;
import com.writeoff.common.exception.ErrorCodes;
import com.writeoff.module.notification.dto.DispatchNotificationRequest;
import com.writeoff.module.notification.dto.AliyunSmsReceiptRequest;
import com.writeoff.module.notification.dto.NotificationReceiptRequest;
import com.writeoff.module.notification.model.NotificationTaskInfo;
import com.writeoff.module.notification.provider.NotificationChannelProvider;
import com.writeoff.module.notification.provider.NotificationSendResult;
import com.writeoff.module.scheduler.service.AsyncJobService;
import com.writeoff.security.AuthContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Locale;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Service
public class NotificationDispatchService {
    private static final Logger log = LoggerFactory.getLogger(NotificationDispatchService.class);
    private final JdbcTemplate jdbcTemplate;
    private final AsyncJobService asyncJobService;
    private final NotificationDeliveryProtectionService deliveryProtectionService;
    private final Map<String, NotificationChannelProvider> providerMap;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final String webhookSecret;
    private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{([^}]+)}");

    private static final RowMapper<NotificationTaskInfo> TASK_ROW_MAPPER = (rs, n) -> new NotificationTaskInfo(
            rs.getLong("id"),
            rs.getLong("policy_id"),
            rs.getString("event_code"),
            rs.getString("channel"),
            rs.getString("receiver_type"),
            rs.getString("receiver_ref"),
            rs.getString("receiver_resolve_source"),
            rs.getString("status"),
            rs.getInt("retry_count"),
            rs.getString("provider_message_id"),
            rs.getString("receipt_code"),
            rs.getString("receipt_message"),
            rs.getString("error_message"),
            rs.getString("created_at"),
            rs.getString("sent_at"),
            rs.getString("receipt_at")
    );

    public NotificationDispatchService(JdbcTemplate jdbcTemplate,
                                       AsyncJobService asyncJobService,
                                       NotificationDeliveryProtectionService deliveryProtectionService,
                                       List<NotificationChannelProvider> providers,
                                       @Value("${app.notification.webhook-secret:change-me}") String webhookSecret) {
        this.jdbcTemplate = jdbcTemplate;
        this.asyncJobService = asyncJobService;
        this.deliveryProtectionService = deliveryProtectionService;
        this.webhookSecret = webhookSecret == null ? "change-me" : webhookSecret;
        this.providerMap = new HashMap<String, NotificationChannelProvider>();
        if (providers != null) {
            for (NotificationChannelProvider provider : providers) {
                providerMap.put(provider.channel().toUpperCase(), provider);
            }
        }
    }

    public PageResult<NotificationTaskInfo> listTasks(Integer pageNo, Integer pageSize) {
        int resolvedPageNo = normalizePageNo(pageNo);
        int resolvedPageSize = normalizePageSize(pageSize);
        int offset = (resolvedPageNo - 1) * resolvedPageSize;
        Long tenantId = tenantId();

        String whereSql = " FROM notification_task WHERE tenant_id=? AND is_deleted=0";
        List<Object> args = new ArrayList<Object>();
        args.add(tenantId);

        Long total = jdbcTemplate.queryForObject("SELECT COUNT(1)" + whereSql, Long.class, args.toArray());
        String dataSql =
                "SELECT id, policy_id, event_code, channel, receiver_type, receiver_ref, " +
                        "JSON_UNQUOTE(JSON_EXTRACT(payload_json, '$.receiverResolveSource')) AS receiver_resolve_source, " +
                        "status, retry_count, provider_message_id, receipt_code, receipt_message, error_message, " +
                        "DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS created_at, DATE_FORMAT(sent_at, '%Y-%m-%d %H:%i:%s') AS sent_at, DATE_FORMAT(receipt_at, '%Y-%m-%d %H:%i:%s') AS receipt_at " +
                        whereSql +
                        " ORDER BY id DESC LIMIT ? OFFSET ?";
        List<Object> dataArgs = new ArrayList<Object>(args);
        dataArgs.add(resolvedPageSize);
        dataArgs.add(offset);
        List<NotificationTaskInfo> list = jdbcTemplate.query(dataSql, TASK_ROW_MAPPER, dataArgs.toArray());
        return new PageResult<NotificationTaskInfo>(list, total == null ? 0L : total, resolvedPageNo, resolvedPageSize);
    }

    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> dispatch(DispatchNotificationRequest request) {
        Long filterPolicyId = request.getPolicyId();
        List<Map<String, Object>> policies;
        if (filterPolicyId != null && filterPolicyId > 0) {
            policies = jdbcTemplate.queryForList(
                    "SELECT p.id, p.channel, p.receiver_type, p.template_id, p.variables_json, p.policy_name, " +
                            "tt.template_name, tt.subject_template, tt.title_template, tt.content_template " +
                            "FROM notification_policy p " +
                            "JOIN notification_text_template tt ON tt.tenant_id=p.tenant_id AND tt.id=p.template_id AND tt.is_deleted=0 AND tt.status='ENABLED' " +
                            "WHERE p.tenant_id=? AND p.id=? AND p.event_code=? AND p.status='ENABLED' AND p.is_deleted=0",
                    tenantId(),
                    filterPolicyId,
                    request.getEventCode()
            );
        } else {
            policies = jdbcTemplate.queryForList(
                    "SELECT p.id, p.channel, p.receiver_type, p.template_id, p.variables_json, p.policy_name, " +
                            "tt.template_name, tt.subject_template, tt.title_template, tt.content_template " +
                            "FROM notification_policy p " +
                            "JOIN notification_text_template tt ON tt.tenant_id=p.tenant_id AND tt.id=p.template_id AND tt.is_deleted=0 AND tt.status='ENABLED' " +
                            "WHERE p.tenant_id=? AND p.event_code=? AND p.status='ENABLED' AND p.is_deleted=0",
                    tenantId(),
                    request.getEventCode()
            );
        }
        if (policies.isEmpty()) {
            if (filterPolicyId != null && filterPolicyId > 0) {
                throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "未找到可用通知策略（策略不存在、未启用或事件不匹配）");
            }
            throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "未找到可用通知策略");
        }
        Map<String, Object> requestVars = parseVariables(request.getVariablesJson());
        int created = 0;
        for (Map<String, Object> p : policies) {
            Long policyId = ((Number) p.get("id")).longValue();
            String channel = String.valueOf(p.get("channel"));
            String receiverType = String.valueOf(p.get("receiver_type"));
            ReceiverResolution receiverResolution = resolveReceiver(channel, receiverType, request, requestVars);
            String payloadJson = buildPolicyPayload(request, p, requestVars, receiverResolution.resolveSource, receiverResolution.receiverRef);
            jdbcTemplate.update(
                    "INSERT INTO notification_task (tenant_id, policy_id, event_code, channel, receiver_type, receiver_ref, payload_json, status, retry_count, max_retry, idempotency_key, created_by, updated_by) " +
                            "VALUES (?, ?, ?, ?, ?, ?, ?, 'PENDING', 0, 3, ?, ?, ?)",
                    tenantId(),
                    policyId,
                    request.getEventCode(),
                    channel,
                    receiverType,
                    receiverResolution.receiverRef,
                    payloadJson,
                    request.getIdempotencyKey() + "-" + policyId,
                    safeUserId(),
                    safeUserId()
            );
            Long taskId = jdbcTemplate.queryForObject("SELECT IFNULL(MAX(id),0) FROM notification_task WHERE tenant_id=?", Long.class, tenantId());
            Map<String, Object> payload = new LinkedHashMap<String, Object>();
            payload.put("taskId", taskId == null ? 0L : taskId);
            payload.put("eventCode", request.getEventCode());
            try {
                asyncJobService.enqueue("NOTIFICATION_DISPATCH", objectMapper.writeValueAsString(payload), "notify-task-" + taskId);
            } catch (Exception ex) {
                throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "通知任务入队失败");
            }
            created++;
        }
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("eventCode", request.getEventCode());
        data.put("taskCount", created);
        return data;
    }

    private ReceiverResolution resolveReceiver(String channel, String receiverType, DispatchNotificationRequest request, Map<String, Object> requestVars) {
        String ch = normalizeUpper(channel);
        String bizType = request == null ? "" : String.valueOf(request.getBizType() == null ? "" : request.getBizType());
        String bizId = request == null ? "" : String.valueOf(request.getBizId() == null ? "" : request.getBizId());
        String input = bizId.trim();
        if (!input.isEmpty() && isExplicitReceiverRef(ch, input)) {
            return new ReceiverResolution(normalizeExplicitReceiverRef(ch, input), "EXPLICIT_INPUT");
        }
        ReceiverTarget receiverTarget = resolveTargetUser(receiverType, bizType, input, requestVars);
        String receiverRef = resolveReceiverByUser(channel, receiverTarget.userId);
        return new ReceiverResolution(receiverRef, receiverTarget.source);
    }

    private ReceiverTarget resolveTargetUser(String receiverType, String bizType, String bizId, Map<String, Object> requestVars) {
        String type = normalizeUpper(receiverType);
        Long directUserId = resolveUserId(bizType, bizId, requestVars);
        if ("TARGET_USER".equals(type) && directUserId != null && directUserId > 0) {
            return new ReceiverTarget(directUserId, "RECEIVER_TYPE_TARGET_USER");
        }
        Long meetingId = resolveMeetingId(bizType, bizId, requestVars);
        if ("SUBMITTER".equals(type)) {
            Long submitterUserId = findMeetingSubmitterUserId(meetingId);
            if (submitterUserId != null && submitterUserId > 0) {
                return new ReceiverTarget(submitterUserId, "RECEIVER_TYPE_SUBMITTER");
            }
        } else if ("AUDITOR".equals(type)) {
            Long auditorUserId = findMeetingAuditorUserId(meetingId);
            if (auditorUserId != null && auditorUserId > 0) {
                return new ReceiverTarget(auditorUserId, "RECEIVER_TYPE_AUDITOR");
            }
        } else if ("FINANCE_ROLE".equals(type)) {
            Long financeUserId = findMeetingFinanceApproverUserId(meetingId);
            if (financeUserId == null || financeUserId <= 0) {
                financeUserId = findFirstFinanceRoleUserId();
            }
            if (financeUserId != null && financeUserId > 0) {
                return new ReceiverTarget(financeUserId, "RECEIVER_TYPE_FINANCE_ROLE");
            }
        }
        return new ReceiverTarget(safeUserId(), "FALLBACK_CURRENT_USER");
    }

    private Long resolveUserId(String bizType, String bizId, Map<String, Object> requestVars) {
        if (requestVars != null) {
            Long userIdFromVars = toLong(requestVars.get("targetUserId"));
            if (userIdFromVars != null && userIdFromVars > 0) {
                return userIdFromVars;
            }
            userIdFromVars = toLong(requestVars.get("userId"));
            if (userIdFromVars != null && userIdFromVars > 0) {
                return userIdFromVars;
            }
        }
        String normalizedBizType = normalizeUpper(bizType);
        if (!normalizedBizType.isEmpty() && !"USER".equals(normalizedBizType)) {
            return null;
        }
        String value = bizId == null ? "" : bizId.trim();
        if (value.isEmpty()) {
            return null;
        }
        if (value.startsWith("user-")) {
            return toLong(value.substring("user-".length()));
        }
        return toLong(value);
    }

    private String resolveReceiverByUser(String channel, Long userId) {
        String ch = normalizeUpper(channel);
        if ("EMAIL".equals(ch)) {
            String email = userEmail(userId);
            if (email.isEmpty()) {
                throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "目标用户未配置邮箱，无法发送邮件通知");
            }
            return email;
        }
        if ("SMS".equals(ch)) {
            String phone = userPhone(userId);
            if (phone.isEmpty()) {
                throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "目标用户未配置手机号，无法发送短信通知");
            }
            return phone;
        }
        return "user-" + (userId == null ? 0L : userId);
    }

    private String userEmail(Long userId) {
        if (userId == null || userId <= 0) {
            return "";
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT email FROM sys_user WHERE tenant_id=? AND id=? AND is_deleted=0 LIMIT 1",
                tenantId(),
                userId
        );
        if (rows.isEmpty()) {
            return "";
        }
        Object val = rows.get(0).get("email");
        return val == null ? "" : String.valueOf(val).trim();
    }

    private String userPhone(Long userId) {
        if (userId == null || userId <= 0) {
            return "";
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT phone FROM sys_user WHERE tenant_id=? AND id=? AND is_deleted=0 LIMIT 1",
                tenantId(),
                userId
        );
        if (rows.isEmpty()) {
            return "";
        }
        Object val = rows.get(0).get("phone");
        return val == null ? "" : String.valueOf(val).trim();
    }

    private Long resolveMeetingId(String bizType, String bizId, Map<String, Object> requestVars) {
        if (requestVars != null) {
            Object meetingId = requestVars.get("meetingId");
            Long parsedFromVars = toLong(meetingId);
            if (parsedFromVars != null && parsedFromVars > 0) {
                return parsedFromVars;
            }
        }
        String type = normalizeUpper(bizType);
        if (!type.isEmpty() && !"MEETING".equals(type)) {
            return null;
        }
        String val = bizId == null ? "" : bizId.trim();
        if (val.isEmpty()) {
            return null;
        }
        if (val.startsWith("meeting-")) {
            return toLong(val.substring("meeting-".length()));
        }
        return toLong(val);
    }

    private Long findMeetingSubmitterUserId(Long meetingId) {
        if (meetingId == null || meetingId <= 0) {
            return null;
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT created_by FROM meeting WHERE tenant_id=? AND id=? AND is_deleted=0 LIMIT 1",
                tenantId(),
                meetingId
        );
        if (rows.isEmpty()) {
            return null;
        }
        return toLong(rows.get(0).get("created_by"));
    }

    private Long findMeetingAuditorUserId(Long meetingId) {
        if (meetingId == null || meetingId <= 0) {
            return null;
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT current_auditor_user_id FROM meeting WHERE tenant_id=? AND id=? AND is_deleted=0 LIMIT 1",
                tenantId(),
                meetingId
        );
        if (rows.isEmpty()) {
            return null;
        }
        return toLong(rows.get(0).get("current_auditor_user_id"));
    }

    private Long findMeetingFinanceApproverUserId(Long meetingId) {
        if (meetingId == null || meetingId <= 0) {
            return null;
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT p.finance_approver_user_id AS finance_user_id " +
                        "FROM meeting m JOIN project p ON m.tenant_id=p.tenant_id AND m.project_id=p.id " +
                        "WHERE m.tenant_id=? AND m.id=? AND m.is_deleted=0 AND p.is_deleted=0 LIMIT 1",
                tenantId(),
                meetingId
        );
        if (rows.isEmpty()) {
            return null;
        }
        return toLong(rows.get(0).get("finance_user_id"));
    }

    private Long findFirstFinanceRoleUserId() {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT u.id AS user_id " +
                        "FROM sys_user u " +
                        "JOIN user_role ur ON u.tenant_id=ur.tenant_id AND u.id=ur.user_id AND ur.is_deleted=0 " +
                        "JOIN role r ON ur.tenant_id=r.tenant_id AND ur.role_id=r.id AND r.is_deleted=0 " +
                        "WHERE u.tenant_id=? AND u.is_deleted=0 AND u.status='ENABLED' " +
                        "AND r.role_code IN ('FINANCE', 'FINANCE_ROLE', 'FINANCE_APPROVER') " +
                        "ORDER BY u.id ASC LIMIT 1",
                tenantId()
        );
        if (rows.isEmpty()) {
            return null;
        }
        return toLong(rows.get(0).get("user_id"));
    }

    private String normalizeUpper(String val) {
        if (val == null) {
            return "";
        }
        return val.trim().toUpperCase(Locale.ROOT);
    }

    private boolean isExplicitReceiverRef(String channel, String value) {
        if (value == null || value.trim().isEmpty()) {
            return false;
        }
        String val = value.trim();
        if ("IN_APP".equals(channel)) {
            return "ALL".equalsIgnoreCase(val) || val.startsWith("user-") || isDigits(val);
        }
        if ("EMAIL".equals(channel)) {
            return val.contains("@");
        }
        if ("SMS".equals(channel)) {
            return isPhoneLike(val);
        }
        return true;
    }

    private String normalizeExplicitReceiverRef(String channel, String value) {
        String val = value == null ? "" : value.trim();
        if ("IN_APP".equals(channel) && isDigits(val)) {
            return "user-" + val;
        }
        return val;
    }

    private boolean isDigits(String val) {
        if (val == null || val.isEmpty()) {
            return false;
        }
        for (int i = 0; i < val.length(); i++) {
            if (!Character.isDigit(val.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    private boolean isPhoneLike(String val) {
        if (val == null) {
            return false;
        }
        String normalized = val.trim();
        if (normalized.startsWith("+")) {
            normalized = normalized.substring(1);
        }
        if (normalized.length() < 6 || normalized.length() > 20) {
            return false;
        }
        for (int i = 0; i < normalized.length(); i++) {
            if (!Character.isDigit(normalized.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    private Long toLong(Object val) {
        if (val == null) {
            return null;
        }
        try {
            if (val instanceof Number) {
                return ((Number) val).longValue();
            }
            String text = String.valueOf(val).trim();
            if (text.isEmpty()) {
                return null;
            }
            return Long.valueOf(text);
        } catch (Exception ex) {
            return null;
        }
    }

    private String buildPolicyPayload(DispatchNotificationRequest request, Map<String, Object> policy, Map<String, Object> requestVars, String receiverResolveSource, String receiverResolvedRef) {
        Map<String, Object> policyVars = parseVariables(policy.get("variables_json") == null ? null : String.valueOf(policy.get("variables_json")));
        Map<String, Object> merged = new LinkedHashMap<String, Object>();
        merged.putAll(policyVars);
        merged.putAll(requestVars);
        merged.put("eventCode", request.getEventCode());
        merged.put("bizType", request.getBizType());
        merged.put("bizId", request.getBizId());
        merged.put("receiverResolveSource", receiverResolveSource == null ? "UNKNOWN" : receiverResolveSource);
        merged.put("receiverResolvedRef", receiverResolvedRef == null ? "" : receiverResolvedRef);
        merged.put("policyId", policy.get("id"));
        merged.put("policyName", policy.get("policy_name"));
        merged.put("templateId", policy.get("template_id"));
        merged.put("templateName", policy.get("template_name"));

        String policyName = policy.get("policy_name") == null ? "系统通知" : String.valueOf(policy.get("policy_name"));
        String eventCode = request.getEventCode() == null ? "" : request.getEventCode().trim();
        String defaultTitle = policyName;
        if (!eventCode.isEmpty()) {
            defaultTitle = policyName + "（" + eventCode + "）";
        }
        String defaultContent = "事件[" + eventCode + "]触发通知";

        String templateSubject = policy.get("subject_template") == null ? null : String.valueOf(policy.get("subject_template"));
        String templateTitle = policy.get("title_template") == null ? null : String.valueOf(policy.get("title_template"));
        String templateContent = policy.get("content_template") == null ? null : String.valueOf(policy.get("content_template"));

        String overrideSubject = pickText(merged, "subject", "title");
        String overrideTitle = pickText(merged, "title", "subject");
        String overrideContent = pickText(merged, "content", "message");

        String finalSubject = resolvePlaceholders(
                chooseText(overrideSubject, templateSubject, defaultTitle),
                merged
        );
        String finalTitle = resolvePlaceholders(
                chooseText(overrideTitle, templateTitle, defaultTitle),
                merged
        );
        String finalContent = resolvePlaceholders(
                chooseText(overrideContent, templateContent, defaultContent),
                merged
        );

        Map<String, Object> payload = new LinkedHashMap<String, Object>();
        payload.putAll(merged);
        payload.put("subject", finalSubject);
        payload.put("title", finalTitle);
        payload.put("content", finalContent);
        payload.put("message", finalContent);
        try {
            return objectMapper.writeValueAsString(payload);
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "通知变量格式非法");
        }
    }

    private Map<String, Object> parseVariables(String variablesJson) {
        if (variablesJson == null || variablesJson.trim().isEmpty()) {
            return new LinkedHashMap<String, Object>();
        }
        String raw = variablesJson.trim();
        if (!raw.startsWith("{")) {
            Map<String, Object> simple = new LinkedHashMap<String, Object>();
            simple.put("content", raw);
            return simple;
        }
        try {
            Map<String, Object> map = objectMapper.readValue(raw, new TypeReference<Map<String, Object>>() {});
            if (map == null) {
                return new LinkedHashMap<String, Object>();
            }
            return new LinkedHashMap<String, Object>(map);
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "variablesJson必须为JSON对象");
        }
    }

    private String pickText(Map<String, Object> map, String preferredKey, String fallbackKey) {
        if (map == null) {
            return null;
        }
        Object preferred = map.get(preferredKey);
        if (preferred != null && String.valueOf(preferred).trim().length() > 0) {
            return String.valueOf(preferred);
        }
        Object fallback = map.get(fallbackKey);
        if (fallback != null && String.valueOf(fallback).trim().length() > 0) {
            return String.valueOf(fallback);
        }
        return null;
    }

    private String chooseText(String first, String second, String fallback) {
        if (first != null && first.trim().length() > 0) {
            return first;
        }
        if (second != null && second.trim().length() > 0) {
            return second;
        }
        return fallback;
    }

    private String resolvePlaceholders(String template, Map<String, Object> vars) {
        if (template == null) {
            return "";
        }
        Map<String, Object> safeVars = vars == null ? Collections.<String, Object>emptyMap() : vars;
        Matcher matcher = PLACEHOLDER_PATTERN.matcher(template);
        StringBuffer buffer = new StringBuffer();
        while (matcher.find()) {
            String key = matcher.group(1) == null ? "" : matcher.group(1).trim();
            Object val = safeVars.get(key);
            String replacement = val == null ? "" : String.valueOf(val);
            matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement));
        }
        matcher.appendTail(buffer);
        return buffer.toString();
    }

    @Transactional(rollbackFor = Exception.class)
    public void processTask(String payload) {
        Long taskId = parseTaskId(payload);
        Map<String, Object> task = findTask(taskId);
        String status = String.valueOf(task.get("status"));
        if ("SENT".equalsIgnoreCase(status) || "FAILED".equalsIgnoreCase(status) || "DELIVERED".equalsIgnoreCase(status)) {
            return;
        }
        int retryCount = ((Number) task.get("retry_count")).intValue();
        int maxRetry = ((Number) task.get("max_retry")).intValue();
        String channel = String.valueOf(task.get("channel"));
        String receiverRef = String.valueOf(task.get("receiver_ref"));
        try {
            if (receiverRef == null || receiverRef.trim().isEmpty()) {
                throw new IllegalStateException("接收人为空");
            }
            NotificationDeliveryProtectionService.GuardDecision guardDecision = deliveryProtectionService.checkBeforeSend(channel, receiverRef);
            if (!guardDecision.isAllowed()) {
                jdbcTemplate.update(
                        "UPDATE notification_task SET status='FAILED', retry_count=max_retry, error_message=?, receipt_code=?, updated_at=CURRENT_TIMESTAMP, updated_by=? WHERE tenant_id=? AND id=?",
                        guardDecision.getMessage(),
                        guardDecision.getCode(),
                        safeUserId(),
                        tenantId(),
                        taskId
                );
                log.warn("notification task blocked by delivery protection, taskId={}, channel={}, receiver={}, code={}", taskId, channel, receiverRef, guardDecision.getCode());
                return;
            }
            NotificationChannelProvider provider = providerMap.get(channel == null ? "" : channel.toUpperCase());
            if (provider == null) {
                throw new IllegalStateException("渠道适配器不存在");
            }
            Map<String, Object> sendContext = new LinkedHashMap<String, Object>();
            sendContext.put("taskId", taskId);
            sendContext.put("outId", "task-" + taskId);
            NotificationSendResult sendResult = provider.send(
                    receiverRef,
                    task.get("payload_json") == null ? null : String.valueOf(task.get("payload_json")),
                    sendContext
            );
            if (!sendResult.isAccepted()) {
                throw new IllegalStateException("渠道未受理: " + sendResult.getProviderCode());
            }
            jdbcTemplate.update(
                    "UPDATE notification_task SET status='SENT', provider_message_id=?, receipt_code=?, receipt_message=?, error_message=NULL, sent_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP, updated_by=? WHERE tenant_id=? AND id=?",
                    sendResult.getProviderMessageId(),
                    sendResult.getProviderCode(),
                    sendResult.getProviderMessage(),
                    safeUserId(),
                    tenantId(),
                    taskId
            );
            try {
                deliveryProtectionService.recordSuccess(channel, receiverRef);
            } catch (Exception guardEx) {
                log.warn("notification delivery protection success bookkeeping failed, taskId={}, err={}", taskId, guardEx.getMessage());
            }
        } catch (Exception ex) {
            try {
                deliveryProtectionService.recordFailure(channel, ex.getMessage());
            } catch (Exception guardEx) {
                log.warn("notification delivery protection failure bookkeeping failed, taskId={}, err={}", taskId, guardEx.getMessage());
            }
            int nextRetry = retryCount + 1;
            String nextStatus = nextRetry >= maxRetry ? "FAILED" : "PENDING";
            jdbcTemplate.update(
                    "UPDATE notification_task SET status=?, retry_count=?, error_message=?, updated_at=CURRENT_TIMESTAMP, updated_by=? WHERE tenant_id=? AND id=?",
                    nextStatus,
                    nextRetry,
                    ex.getMessage(),
                    safeUserId(),
                    tenantId(),
                    taskId
            );
            if ("FAILED".equals(nextStatus)) {
                log.warn("notification task exhausted retries, taskId={}, channel={}, receiver={}, err={}", taskId, channel, receiverRef, ex.getMessage());
                return;
            }
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "通知发送失败");
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> ingestReceipt(NotificationReceiptRequest request) {
        Long tenantId = tenantId();
        return ingestReceiptInternal(request, tenantId, true);
    }

    public Map<String, Object> ingestReceiptWebhook(NotificationReceiptRequest request, String timestamp, String signature) {
        if (!verifySignature(request, timestamp, signature)) {
            throw new BusinessException(ErrorCodes.NO_DATA_PERMISSION, "回执签名校验失败");
        }
        return ingestReceiptInternal(request, null, false);
    }

    public Map<String, Object> ingestAliyunSmsReceipt(AliyunSmsReceiptRequest request) {
        NotificationReceiptRequest receiptRequest = new NotificationReceiptRequest();
        Long taskId = parseTaskIdFromOutId(request == null ? null : request.getOutId());
        if (taskId == null || taskId <= 0) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "阿里云短信回执缺少可识别的任务标识");
        }
        receiptRequest.setTaskId(taskId);
        receiptRequest.setProviderMessageId(request == null ? null : request.getBizId());
        String errCode = request == null ? "" : String.valueOf(request.getErrCode() == null ? "" : request.getErrCode()).trim();
        boolean delivered = request != null && Boolean.TRUE.equals(request.getSuccess()) && (errCode.isEmpty() || "DELIVERED".equalsIgnoreCase(errCode) || "OK".equalsIgnoreCase(errCode));
        receiptRequest.setDelivered(delivered);
        receiptRequest.setReceiptCode(errCode.isEmpty() ? (delivered ? "DELIVERED" : "FAILED") : errCode);
        StringBuilder message = new StringBuilder();
        if (request != null && request.getPhoneNumber() != null && request.getPhoneNumber().trim().length() > 0) {
            message.append("手机号=").append(request.getPhoneNumber().trim());
        }
        if (request != null && request.getReceiveDate() != null && request.getReceiveDate().trim().length() > 0) {
            if (message.length() > 0) {
                message.append("；");
            }
            message.append("回执时间=").append(request.getReceiveDate().trim());
        }
        if (request != null && request.getErrMsg() != null && request.getErrMsg().trim().length() > 0) {
            if (message.length() > 0) {
                message.append("；");
            }
            message.append(request.getErrMsg().trim());
        }
        receiptRequest.setReceiptMessage(message.toString());
        return ingestReceiptInternal(receiptRequest, null, false);
    }

    public Map<String, Object> ingestAliyunSmsReceipts(List<AliyunSmsReceiptRequest> requests) {
        if (requests == null || requests.isEmpty()) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "阿里云短信回执内容不能为空");
        }
        int deliveredCount = 0;
        int failedCount = 0;
        List<Map<String, Object>> items = new ArrayList<Map<String, Object>>();
        for (AliyunSmsReceiptRequest request : requests) {
            Map<String, Object> item = ingestAliyunSmsReceipt(request);
            items.add(item);
            String status = item.get("status") == null ? "" : String.valueOf(item.get("status"));
            if ("DELIVERED".equalsIgnoreCase(status)) {
                deliveredCount++;
            } else if ("FAILED".equalsIgnoreCase(status)) {
                failedCount++;
            }
        }
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("count", items.size());
        data.put("deliveredCount", deliveredCount);
        data.put("failedCount", failedCount);
        data.put("items", items);
        return data;
    }

    @Transactional(rollbackFor = Exception.class)
    private Map<String, Object> ingestReceiptInternal(NotificationReceiptRequest request, Long tenantIdHint, boolean requireTenantAuth) {
        Map<String, Object> task = requireTenantAuth ? findTask(request.getTaskId()) : findTaskAnyTenant(request.getTaskId());
        Long taskTenantId = tenantIdHint != null ? tenantIdHint : toLong(task.get("tenant_id"));
        if (taskTenantId == null || taskTenantId <= 0) {
            throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "通知任务不存在");
        }
        String providerMessageId = task.get("provider_message_id") == null ? "" : String.valueOf(task.get("provider_message_id"));
        if (!providerMessageId.equals(request.getProviderMessageId())) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "供应商消息ID不匹配");
        }
        boolean delivered = request.getDelivered() != null && request.getDelivered();
        String nextStatus = delivered ? "DELIVERED" : "FAILED";
        jdbcTemplate.update(
                "UPDATE notification_task SET status=?, receipt_code=?, receipt_message=?, receipt_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP, updated_by=? WHERE tenant_id=? AND id=?",
                nextStatus,
                request.getReceiptCode(),
                request.getReceiptMessage(),
                safeUserId(),
                taskTenantId,
                request.getTaskId()
        );
        jdbcTemplate.update(
                "INSERT INTO notification_receipt_log (tenant_id, task_id, provider_message_id, receipt_code, receipt_message, receipt_status, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)",
                taskTenantId,
                request.getTaskId(),
                request.getProviderMessageId(),
                request.getReceiptCode(),
                request.getReceiptMessage(),
                nextStatus,
                safeUserId()
        );
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("taskId", request.getTaskId());
        data.put("status", nextStatus);
        return data;
    }

    private Long parseTaskId(String payload) {
        try {
            Map<String, Object> map = objectMapper.readValue(payload, new TypeReference<Map<String, Object>>() {});
            Object val = map.get("taskId");
            if (val == null) {
                throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "通知任务ID缺失");
            }
            return Long.valueOf(String.valueOf(val));
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "通知任务参数非法");
        }
    }

    private Map<String, Object> findTask(Long taskId) {
        List<Map<String, Object>> list = jdbcTemplate.queryForList(
                "SELECT id, tenant_id, status, retry_count, max_retry, channel, receiver_ref, provider_message_id, payload_json FROM notification_task WHERE tenant_id=? AND id=? AND is_deleted=0",
                tenantId(),
                taskId
        );
        if (list.isEmpty()) {
            throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "通知任务不存在");
        }
        return list.get(0);
    }

    private Map<String, Object> findTaskAnyTenant(Long taskId) {
        List<Map<String, Object>> list = jdbcTemplate.queryForList(
                "SELECT id, tenant_id, status, retry_count, max_retry, channel, receiver_ref, provider_message_id, payload_json FROM notification_task WHERE id=? AND is_deleted=0",
                taskId
        );
        if (list.isEmpty()) {
            throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "通知任务不存在");
        }
        return list.get(0);
    }

    private Long parseTaskIdFromOutId(String outId) {
        if (outId == null || outId.trim().isEmpty()) {
            return null;
        }
        String raw = outId.trim();
        if (raw.startsWith("task-")) {
            raw = raw.substring("task-".length());
        }
        return toLong(raw);
    }

    private boolean verifySignature(NotificationReceiptRequest request, String timestamp, String signature) {
        if (timestamp == null || signature == null || signature.trim().isEmpty()) {
            return false;
        }
        String plain = String.valueOf(request.getTaskId()) + "|" + request.getProviderMessageId() + "|" + request.getReceiptCode() + "|" + timestamp;
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(webhookSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            byte[] bytes = mac.doFinal(plain.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : bytes) {
                String hex = Integer.toHexString(b & 0xff);
                if (hex.length() == 1) {
                    sb.append('0');
                }
                sb.append(hex);
            }
            return sb.toString().equalsIgnoreCase(signature.trim());
        } catch (Exception ex) {
            return false;
        }
    }

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

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

    private int normalizePageNo(Integer pageNo) {
        if (pageNo == null || pageNo < 1) {
            return 1;
        }
        return pageNo;
    }

    private int normalizePageSize(Integer pageSize) {
        if (pageSize == null || pageSize < 1) {
            return 20;
        }
        return Math.min(pageSize, 200);
    }

    private static final class ReceiverTarget {
        private final Long userId;
        private final String source;

        private ReceiverTarget(Long userId, String source) {
            this.userId = userId;
            this.source = source;
        }
    }

    private static final class ReceiverResolution {
        private final String receiverRef;
        private final String resolveSource;

        private ReceiverResolution(String receiverRef, String resolveSource) {
            this.receiverRef = receiverRef;
            this.resolveSource = resolveSource;
        }
    }
}
