package com.jeesharp.business.modules.contentsecurity.application;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.jeesharp.business.modules.contentsecurity.api.spi.ContentSecurityProvider;
import com.jeesharp.business.modules.contentsecurity.config.ContentSecurityProperties;
import com.jeesharp.business.modules.contentsecurity.convert.ContentSecurityLogConvert;
import com.jeesharp.business.modules.contentsecurity.dto.ContentCheckRequest;
import com.jeesharp.business.modules.contentsecurity.dto.ContentCheckResult;
import com.jeesharp.business.modules.contentsecurity.dto.ContentMemberSnapshot;
import com.jeesharp.business.modules.contentsecurity.dto.ContentSecurityBizRef;
import com.jeesharp.business.modules.contentsecurity.dto.WechatMediaCheckCallbackDTO;
import com.jeesharp.business.modules.contentsecurity.entity.ContentSecurityLog;
import com.jeesharp.business.modules.contentsecurity.entity.ContentSecurityTask;
import com.jeesharp.business.modules.contentsecurity.enums.CheckDecisionEnum;
import com.jeesharp.business.modules.contentsecurity.enums.ContentSecurityProviderCodeEnum;
import com.jeesharp.business.modules.contentsecurity.enums.ContentSecurityTaskStatusEnum;
import com.jeesharp.business.modules.contentsecurity.enums.ContentTypeEnum;
import com.jeesharp.business.modules.contentsecurity.event.ContentSecurityMediaCompletedEvent;
import com.jeesharp.business.modules.contentsecurity.application.support.ContentSecurityOpenIdResolver;
import com.jeesharp.business.modules.contentsecurity.application.support.ContentSecurityProviderRouter;
import com.jeesharp.business.modules.contentsecurity.service.ContentSecurityLogService;
import com.jeesharp.business.modules.contentsecurity.service.ContentSecurityTaskService;
import com.jeesharp.business.modules.contentsecurity.vo.ContentSecurityLogListVO;
import com.jeesharp.business.modules.contentsecurity.vo.ContentSecurityLogQueryVO;
import com.jeesharp.business.modules.member.service.MemberService;
import com.jeesharp.framework.exception.BizErrorException;
import com.jeesharp.framework.pojo.Page;
import com.jeesharp.framework.pojo.query.PageRespVO;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 内容安全应用编排：Provider 路由、openid 补全、流水与异步任务、微信回调。
 *
 * @author jeesharp
 * @since 2026-06-27
 */
@Slf4j
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class ContentSecurityApplicationService {

    private static final String EVENT_WXA_MEDIA_CHECK = "wxa_media_check";

    private final ContentSecurityProperties properties;
    private final ContentSecurityProviderRouter providerRouter;
    private final ContentSecurityOpenIdResolver openIdResolver;
    private final ContentSecurityLogService contentSecurityLogService;
    private final ContentSecurityTaskService contentSecurityTaskService;
    private final ApplicationEventPublisher applicationEventPublisher;
    private final MemberService memberService;

    public ContentCheckResult checkText(ContentCheckRequest request) {
        return executeCheck(request, ContentTypeEnum.TEXT);
    }

    public ContentCheckResult checkMedia(ContentCheckRequest request) {
        return executeCheck(request, ContentTypeEnum.IMAGE);
    }

    public ContentCheckResult checkMediaBatch(List<ContentCheckRequest> requests) {
        if (requests == null || requests.isEmpty()) {
            return skippedPass();
        }
        CheckDecisionEnum worst = CheckDecisionEnum.PASS;
        ContentCheckResult last = null;
        List<String> pendingTraceIds = new ArrayList<>();
        List<String> auditLogIds = new ArrayList<>();
        for (ContentCheckRequest req : requests) {
            ContentCheckResult one = checkMedia(req);
            last = one;
            worst = maxSeverity(worst, one.getDecision());
            if (CollUtil.isNotEmpty(one.getPendingTraceIds())) {
                pendingTraceIds.addAll(one.getPendingTraceIds());
            }
            if (StrUtil.isNotBlank(one.getAuditLogId())) {
                auditLogIds.add(one.getAuditLogId());
            }
            if (worst == CheckDecisionEnum.REJECT) {
                return one;
            }
        }
        if (last == null) {
            return skippedPass();
        }
        return ContentCheckResult.builder()
                .decision(worst)
                .providerCode(last.getProviderCode())
                .rejectReason(last.getRejectReason())
                .async(worst == CheckDecisionEnum.PENDING)
                .pendingTraceIds(pendingTraceIds.isEmpty() ? null : pendingTraceIds)
                .auditLogIds(auditLogIds.isEmpty() ? null : auditLogIds)
                .build();
    }

    @Transactional(readOnly = false)
    public void bindPendingTasks(String bizModule, String bizId, List<String> traceIds, List<String> auditLogIds) {
        contentSecurityTaskService.bindBizIdByTraceIds(bizModule, bizId, traceIds);
        contentSecurityLogService.updateBizIdByTraceIds(bizModule, bizId, traceIds);
        contentSecurityLogService.updateBizIdByIds(bizModule, bizId, auditLogIds);
    }

    @Transactional(readOnly = false)
    public void handleWechatMediaCallback(String rawJson) {
        if (StrUtil.isBlank(rawJson)) {
            return;
        }
        JSONObject json = JSONUtil.parseObj(rawJson);
        WechatMediaCheckCallbackDTO callback = parseWechatMediaCallback(json);
        if (!EVENT_WXA_MEDIA_CHECK.equalsIgnoreCase(callback.getEvent())) {
            log.debug("忽略非 wxa_media_check 推送 event={}", callback.getEvent());
            return;
        }
        String traceId = callback.getTraceId();
        if (StrUtil.isBlank(traceId)) {
            log.warn("wxa_media_check 缺少 trace_id body={}", rawJson);
            return;
        }
        WechatMediaCheckCallbackDTO.WechatMediaCheckResultDTO resultDto = callback.getResult();
        String suggest = resultDto != null ? resultDto.getSuggest() : null;
        Integer label = resultDto != null ? resultDto.getLabel() : null;
        CheckDecisionEnum decision = CheckDecisionEnum.fromWechatSuggest(suggest);
        ContentSecurityTask task = contentSecurityTaskService.getByTraceId(traceId);
        if (task == null) {
            log.warn("未找到 trace_id 对应任务 traceId={}", traceId);
            return;
        }
        Integer taskStatus = toTaskStatus(decision);
        contentSecurityTaskService.updateTaskStatus(traceId, taskStatus, suggest);
        appendCallbackLog(task, decision, traceId, label != null ? String.valueOf(label) : suggest);
        if (StrUtil.isNotBlank(task.getBizModule()) && StrUtil.isNotBlank(task.getBizId())) {
            applicationEventPublisher.publishEvent(new ContentSecurityMediaCompletedEvent(
                    this, task.getBizModule(), task.getBizId(), traceId, decision,
                    label != null ? String.valueOf(label) : suggest));
        }
    }

    public PageRespVO<ContentSecurityLogListVO> findLogPage(Page<ContentSecurityLog> page, ContentSecurityLogQueryVO query) {
        ContentSecurityLogQueryVO prepared = prepareLogQuery(query);
        if (prepared == null) {
            return emptyLogPage(page);
        }
        Page<ContentSecurityLog> result = contentSecurityLogService.findPage(page, prepared);
        List<ContentSecurityLogListVO> rows = ContentSecurityLogConvert.INSTANCE.toListVOList(result.getRecords());
        PageRespVO<ContentSecurityLogListVO> vo = new PageRespVO<>();
        vo.setRecords(rows);
        vo.setTotal(result.getTotal());
        vo.setPageNo(result.getPageNo());
        vo.setPageSize(result.getPageSize());
        vo.setPages(result.getPages());
        return vo;
    }

    /**
     * 检测流水详情（含检测文本与图片 URL）。
     */
    public ContentSecurityLogListVO getLog(String id) {
        if (StrUtil.isBlank(id)) {
            throw new BizErrorException("记录ID不能为空");
        }
        ContentSecurityLog entity = contentSecurityLogService.get(id);
        if (entity == null) {
            throw new BizErrorException("检测记录不存在");
        }
        return ContentSecurityLogConvert.INSTANCE.toListVO(entity);
    }

    private ContentSecurityLogQueryVO prepareLogQuery(ContentSecurityLogQueryVO query) {
        ContentSecurityLogQueryVO q = query != null ? query : new ContentSecurityLogQueryVO();
        if (StrUtil.isBlank(q.getMemberKeyword())) {
            return q;
        }
        List<String> memberIds = memberService.findIdsByKeyword(q.getMemberKeyword().trim());
        q.setMemberKeyword(null);
        if (CollUtil.isEmpty(memberIds)) {
            return null;
        }
        q.setMemberIdIn(memberIds);
        return q;
    }

    private PageRespVO<ContentSecurityLogListVO> emptyLogPage(Page<ContentSecurityLog> page) {
        PageRespVO<ContentSecurityLogListVO> empty = new PageRespVO<>();
        empty.setRecords(Collections.emptyList());
        empty.setTotal(0L);
        empty.setPageNo(page.getPageNo());
        empty.setPageSize(page.getPageSize());
        empty.setPages(0L);
        return empty;
    }

    private ContentCheckResult executeCheck(ContentCheckRequest request, ContentTypeEnum type) {
        if (!properties.isEnabled()) {
            return skippedPass();
        }
        long start = System.currentTimeMillis();
        ContentCheckRequest enriched = enrichRequest(request);
        ContentCheckResult result;
        try {
            ContentSecurityProvider provider = providerRouter.resolve(type);
            result = type == ContentTypeEnum.IMAGE ? provider.checkMedia(enriched) : provider.checkText(enriched);
        } catch (Exception ex) {
            log.error("内容安全 Provider 调用失败 type={} memberId={}", type, request.getMemberId(), ex);
            CheckDecisionEnum fallback = properties.resolveProviderErrorDecision();
            result = ContentCheckResult.builder()
                    .decision(fallback)
                    .providerCode(properties.getDefaultProvider())
                    .rejectReason(fallback == CheckDecisionEnum.REJECT ? "内容安全检测暂不可用，请稍后重试" : null)
                    .resultLabel("provider_error")
                    .async(false)
                    .build();
        }
        String logId = appendAuditLog(enriched, type, result, (int) (System.currentTimeMillis() - start), null);
        if (result.getDecision() == CheckDecisionEnum.PENDING && StrUtil.isNotBlank(result.getTraceId())) {
            createPendingTask(enriched, result, logId);
        }
        return ContentCheckResult.builder()
                .decision(result.getDecision())
                .providerCode(result.getProviderCode())
                .traceId(result.getTraceId())
                .rejectReason(result.getRejectReason())
                .resultLabel(result.getResultLabel())
                .async(result.isAsync())
                .pendingTraceIds(result.getPendingTraceIds())
                .auditLogId(logId)
                .build();
    }

    private void createPendingTask(ContentCheckRequest request, ContentCheckResult result, String logId) {
        ContentSecurityBizRef ref = request.getBizRef();
        ContentSecurityTask task = new ContentSecurityTask();
        task.setLogId(logId);
        task.setTraceId(result.getTraceId());
        task.setMediaUrl(request.getMediaUrl());
        task.setTaskStatus(ContentSecurityTaskStatusEnum.PENDING.getValue());
        task.setMemberId(request.getMemberId());
        task.setTenantId(request.getTenantId());
        task.setProviderCode(result.getProviderCode());
        if (ref != null) {
            task.setBizModule(ref.getBizModule());
            task.setBizId(ref.getBizId());
        }
        contentSecurityTaskService.createPendingTask(task);
    }

    private void appendCallbackLog(ContentSecurityTask task, CheckDecisionEnum decision, String traceId, String label) {
        ContentSecurityLog row = new ContentSecurityLog();
        row.setMemberId(task.getMemberId());
        ContentSecurityLog source = null;
        if (StrUtil.isNotBlank(task.getLogId())) {
            source = contentSecurityLogService.get(task.getLogId());
            copyMemberSnapshot(row, source);
        }
        row.setContentType(ContentTypeEnum.IMAGE.getCode());
        row.setProviderCode(task.getProviderCode());
        row.setDecision(decision.getCode());
        row.setBizModule(task.getBizModule());
        row.setBizId(task.getBizId());
        row.setMediaUrl(StrUtil.isNotBlank(task.getMediaUrl()) ? task.getMediaUrl() : source != null ? source.getMediaUrl() : null);
        if (source != null && StrUtil.isNotBlank(source.getContentText())) {
            row.setContentText(source.getContentText());
        }
        row.setTraceId(traceId);
        row.setResultLabel(label);
        row.setTenantId(task.getTenantId());
        contentSecurityLogService.appendLog(row);
    }

    private ContentCheckRequest enrichRequest(ContentCheckRequest request) {
        ContentCheckRequest withOpenid = request;
        if (StrUtil.isBlank(request.getOpenid())) {
            String openid = openIdResolver.resolveOpenid(request.getMemberId());
            withOpenid = ContentCheckRequest.builder()
                    .contentType(request.getContentType())
                    .scene(request.getScene())
                    .memberId(request.getMemberId())
                    .tenantId(request.getTenantId())
                    .text(request.getText())
                    .mediaUrl(request.getMediaUrl())
                    .openid(openid)
                    .bizRef(request.getBizRef())
                    .memberSnapshot(request.getMemberSnapshot())
                    .build();
        }
        return withOpenid;
    }

    private String appendAuditLog(ContentCheckRequest request, ContentTypeEnum type, ContentCheckResult result,
            int durationMs, String errorMessage) {
        ContentSecurityBizRef ref = request.getBizRef();
        ContentSecurityLog row = new ContentSecurityLog();
        row.setMemberId(request.getMemberId());
        applyMemberSnapshot(row, request.getMemberSnapshot());
        row.setContentType(type.getCode());
        row.setScene(request.getScene() != null ? request.getScene().getCode() : null);
        row.setProviderCode(result.getProviderCode());
        row.setDecision(result.getDecision().getCode());
        if (ref != null) {
            row.setBizModule(ref.getBizModule());
            row.setBizType(ref.getBizType());
            row.setBizId(ref.getBizId());
        }
        row.setTenantId(request.getTenantId());
        String digestSource = type == ContentTypeEnum.IMAGE ? request.getMediaUrl() : request.getText();
        if (StrUtil.isNotBlank(digestSource)) {
            row.setRequestDigest(DigestUtil.sha256Hex(digestSource));
        }
        if (StrUtil.isNotBlank(request.getText())) {
            row.setContentText(request.getText());
        }
        if (StrUtil.isNotBlank(request.getMediaUrl())) {
            row.setMediaUrl(request.getMediaUrl());
        }
        row.setTraceId(result.getTraceId());
        row.setResultLabel(result.getResultLabel());
        row.setErrorMessage(errorMessage);
        row.setDurationMs(durationMs);
        contentSecurityLogService.appendLog(row);
        return row.getId();
    }

    private static void applyMemberSnapshot(ContentSecurityLog row, ContentMemberSnapshot snapshot) {
        if (row == null || snapshot == null) {
            return;
        }
        row.setMemberNo(snapshot.getMemberNo());
        row.setMemberNickname(snapshot.getMemberNickname());
        row.setMemberMobile(snapshot.getMemberMobile());
        row.setMemberIdCard(snapshot.getMemberIdCard());
    }

    private static void copyMemberSnapshot(ContentSecurityLog target, ContentSecurityLog source) {
        if (target == null || source == null) {
            return;
        }
        target.setMemberNo(source.getMemberNo());
        target.setMemberNickname(source.getMemberNickname());
        target.setMemberMobile(source.getMemberMobile());
        target.setMemberIdCard(source.getMemberIdCard());
    }

    private static ContentCheckResult skippedPass() {
        return ContentCheckResult.builder()
                .decision(CheckDecisionEnum.PASS)
                .providerCode(ContentSecurityProviderCodeEnum.NONE.getCode())
                .async(false)
                .build();
    }

    private static CheckDecisionEnum maxSeverity(CheckDecisionEnum a, CheckDecisionEnum b) {
        if (a == CheckDecisionEnum.REJECT || b == CheckDecisionEnum.REJECT) {
            return CheckDecisionEnum.REJECT;
        }
        if (a == CheckDecisionEnum.PENDING || b == CheckDecisionEnum.PENDING) {
            return CheckDecisionEnum.PENDING;
        }
        if (a == CheckDecisionEnum.REVIEW || b == CheckDecisionEnum.REVIEW) {
            return CheckDecisionEnum.REVIEW;
        }
        return CheckDecisionEnum.PASS;
    }

    private static WechatMediaCheckCallbackDTO parseWechatMediaCallback(JSONObject json) {
        WechatMediaCheckCallbackDTO callback = new WechatMediaCheckCallbackDTO();
        callback.setMsgType(json.getStr("MsgType"));
        callback.setEvent(json.getStr("Event"));
        String traceId = json.getStr("trace_id");
        if (StrUtil.isBlank(traceId)) {
            traceId = json.getStr("traceId");
        }
        callback.setTraceId(traceId);
        JSONObject resultObj = json.getJSONObject("result");
        if (resultObj != null) {
            WechatMediaCheckCallbackDTO.WechatMediaCheckResultDTO result =
                    new WechatMediaCheckCallbackDTO.WechatMediaCheckResultDTO();
            result.setSuggest(resultObj.getStr("suggest"));
            result.setLabel(resultObj.getInt("label"));
            callback.setResult(result);
        }
        return callback;
    }

    private static Integer toTaskStatus(CheckDecisionEnum decision) {
        if (decision == CheckDecisionEnum.REJECT) {
            return ContentSecurityTaskStatusEnum.REJECT.getValue();
        }
        if (decision == CheckDecisionEnum.REVIEW) {
            return ContentSecurityTaskStatusEnum.ERROR.getValue();
        }
        return ContentSecurityTaskStatusEnum.PASS.getValue();
    }
}
