package com.easyagents.agent.runtime.knowledge.citation;

import com.easyagents.agent.runtime.message.AgentKnowledgeReference;

import java.util.*;

/**
 * 基于文本证据的启发式知识库引用匹配器。
 *
 * <p>该实现用于模型没有显式输出引用 ID 的场景。它只能说明答案文本与某些检索片段
 * 存在较强文本支撑关系，不能证明 LLM 真实使用了该片段。因此匹配策略保持保守：
 * 宁可少返回引用，也不为了“看起来有引用”而强行猜测。</p>
 */
public class HeuristicKnowledgeCitationMatcher implements AgentKnowledgeCitationMatcher {

    private static final int MIN_NORMALIZED_ANSWER_LENGTH = 4;
    private static final int MIN_CONTENT_LENGTH = 6;
    private static final int MAX_REFERENCES = 5;
    private static final double MIN_SUPPORT_SCORE = 0.42D;

    /**
     * 根据最终答案和候选知识片段匹配引用。
     *
     * @param answerText 最终答案文本
     * @param candidates 本轮检索候选引用
     * @return 高置信引用列表
     */
    @Override
    public List<AgentKnowledgeReference> match(String answerText, Collection<AgentKnowledgeReference> candidates) {
        if (candidates == null || candidates.isEmpty()) {
            return List.of();
        }
        String normalizedAnswer = normalize(answerText);
        if (normalizedAnswer.length() < MIN_NORMALIZED_ANSWER_LENGTH) {
            return List.of();
        }
        List<String> normalizedSegments = normalizeSegments(answerText);
        List<ScoredKnowledgeReference> scoredReferences = new ArrayList<>();
        for (AgentKnowledgeReference candidate : candidates) {
            String normalizedContent = normalize(candidate == null ? null : candidate.getChunkContent());
            double supportScore = supportScore(normalizedAnswer, normalizedContent);
            // 长篇汇总回答中，每条证据通常只支撑一个段落或条目。继续只用整篇答案作分母，
            // 会随着主题增多把有效引用的重合度稀释到阈值以下。
            for (String normalizedSegment : normalizedSegments) {
                supportScore = Math.max(supportScore, supportScore(normalizedSegment, normalizedContent));
            }
            if (supportScore >= MIN_SUPPORT_SCORE) {
                scoredReferences.add(new ScoredKnowledgeReference(candidate, supportScore));
            }
        }
        scoredReferences.sort(Comparator.comparingDouble(ScoredKnowledgeReference::score).reversed());
        return scoredReferences.stream()
                .limit(MAX_REFERENCES)
                .map(ScoredKnowledgeReference::reference)
                .toList();
    }

    /**
     * 将答案切分为可独立核验的段落或句子并完成归一化。
     *
     * @param answerText 最终答案文本
     * @return 非空的归一化答案片段
     */
    private List<String> normalizeSegments(String answerText) {
        if (answerText == null || answerText.isBlank()) {
            return List.of();
        }
        return Arrays.stream(answerText.split("[\\r\\n。！？!?；;]+"))
                .map(this::normalize)
                .filter(segment -> segment.length() >= MIN_NORMALIZED_ANSWER_LENGTH)
                .toList();
    }

    /**
     * 计算答案与候选片段之间的文本支撑分。
     *
     * @param normalizedAnswer 归一化后的答案
     * @param normalizedContent 归一化后的候选片段
     * @return 支撑分
     */
    private double supportScore(String normalizedAnswer, String normalizedContent) {
        if (normalizedAnswer.isBlank()
                || normalizedContent.isBlank()
                || normalizedContent.length() < MIN_CONTENT_LENGTH) {
            return 0D;
        }
        if (normalizedContent.contains(normalizedAnswer)) {
            return 1D;
        }
        int longestCommon = longestCommonSubstringLength(normalizedAnswer, normalizedContent);
        double gramOverlap = gramOverlapRatio(charGrams(normalizedAnswer, 2), charGrams(normalizedContent, 2));
        double numericCoverage = numericCoverage(normalizedAnswer, normalizedContent);

        // 长连续文本片段比零散关键词更能说明答案来自该 chunk。
        if (longestCommon >= 8 && gramOverlap >= 0.12D) {
            double numericBoost = numericCoverage >= 0.8D ? 0.12D : numericCoverage * 0.08D;
            return Math.max(0.55D, gramOverlap + Math.min(0.35D, longestCommon / 60D) + numericBoost);
        }
        if (gramOverlap >= 0.28D && longestCommon >= 5) {
            return gramOverlap + Math.min(0.25D, longestCommon / 80D);
        }
        return gramOverlap * 0.7D + Math.min(0.2D, longestCommon / 80D) + numericCoverage * 0.08D;
    }

    /**
     * 归一化用于引用匹配的文本。
     *
     * @param text 原始文本
     * @return 低噪声文本
     */
    private String normalize(String text) {
        if (text == null) {
            return "";
        }
        return text.toLowerCase()
                .replace('至', '到')
                .replaceAll("[^\\p{IsHan}a-z0-9]", "");
    }

    /**
     * 生成字符 ngram。
     *
     * @param text 文本
     * @param size ngram 长度
     * @return ngram 集合
     */
    private Set<String> charGrams(String text, int size) {
        Set<String> grams = new HashSet<>();
        if (text == null || text.length() < size) {
            return grams;
        }
        for (int i = 0; i <= text.length() - size; i++) {
            grams.add(text.substring(i, i + size));
        }
        return grams;
    }

    /**
     * 计算答案 ngram 被候选片段覆盖的比例。
     *
     * @param answerGrams 答案 ngram
     * @param contentGrams 候选片段 ngram
     * @return 覆盖比例
     */
    private double gramOverlapRatio(Set<String> answerGrams, Set<String> contentGrams) {
        if (answerGrams == null || answerGrams.isEmpty() || contentGrams == null || contentGrams.isEmpty()) {
            return 0D;
        }
        int matched = 0;
        for (String gram : answerGrams) {
            if (contentGrams.contains(gram)) {
                matched++;
            }
        }
        return (double) matched / (double) answerGrams.size();
    }

    /**
     * 计算答案中的数字片段被候选片段覆盖的比例。
     *
     * <p>数字只能作为弱加权因素，不能单独造成高置信引用，避免日期、章节号等噪声误判。</p>
     *
     * @param normalizedAnswer 归一化后的答案
     * @param normalizedContent 归一化后的候选片段
     * @return 数字覆盖比例
     */
    private double numericCoverage(String normalizedAnswer, String normalizedContent) {
        List<String> numbers = extractNumericTokens(normalizedAnswer);
        if (numbers.isEmpty()) {
            return 0D;
        }
        int matched = 0;
        for (String number : numbers) {
            if (normalizedContent.contains(number)) {
                matched++;
            }
        }
        return (double) matched / (double) numbers.size();
    }

    /**
     * 提取数字片段。
     *
     * @param text 文本
     * @return 数字片段
     */
    private List<String> extractNumericTokens(String text) {
        List<String> numbers = new ArrayList<>();
        if (text == null || text.isBlank()) {
            return numbers;
        }
        java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("\\d+").matcher(text);
        while (matcher.find()) {
            numbers.add(matcher.group());
        }
        return numbers;
    }

    /**
     * 计算最长公共子串长度。
     *
     * @param first 第一段文本
     * @param second 第二段文本
     * @return 最长公共子串长度
     */
    private int longestCommonSubstringLength(String first, String second) {
        if (first == null || second == null || first.isEmpty() || second.isEmpty()) {
            return 0;
        }
        int[] previous = new int[second.length() + 1];
        int max = 0;
        for (int i = 1; i <= first.length(); i++) {
            int[] current = new int[second.length() + 1];
            for (int j = 1; j <= second.length(); j++) {
                if (first.charAt(i - 1) == second.charAt(j - 1)) {
                    current[j] = previous[j - 1] + 1;
                    max = Math.max(max, current[j]);
                }
            }
            previous = current;
        }
        return max;
    }

    private record ScoredKnowledgeReference(AgentKnowledgeReference reference, double score) {
    }
}
