"""
text_scorer.py - 基于语言模型的文本完整性评分模块

使用中文 GPT-2 模型（ONNX 格式）计算文本的自然度评分，
用于判断：
1. 两个文本片段是否应该拼接（跨页内容延续检测）
2. 文本中的换行符是否需要去除
"""

import logging
import os
from typing import Optional

import numpy as np
import onnxruntime as ort
from tokenizers import Tokenizer

logger = logging.getLogger(__name__)

# 模型路径
MODEL_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
                         "models", "gpt2-chinese-cluecorpussmall-onnx")
MODEL_PATH = os.path.join(MODEL_DIR, "model.onnx")
TOKENIZER_PATH = os.path.join(MODEL_DIR, "tokenizer.json")

# 特殊 token ID（BERT WordPiece）
CLS_ID = 101
SEP_ID = 102
PAD_ID = 0

# 最大序列长度
MAX_LEN = 64


class TextScorer:
    """使用中文 GPT-2 计算文本自然度评分的单例封装。"""

    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        if self._initialized:
            return
        self._initialized = True
        self._session = None
        self._tokenizer = None

    def _ensure_loaded(self):
        if self._session is not None:
            return
        if not os.path.exists(MODEL_PATH):
            raise FileNotFoundError(
                f"模型文件不存在: {MODEL_PATH}\n"
                f"请确认 models/gpt2-chinese-cluecorpussmall-onnx/ 已下载"
            )
        self._session = ort.InferenceSession(MODEL_PATH)
        self._tokenizer = Tokenizer.from_file(TOKENIZER_PATH)
        logger.info(f"文本评分模型已加载: {MODEL_PATH}")

    def score(self, text: str) -> float:
        """计算文本的自然度评分（0~1），越高越自然完整。

        基于困惑度（Perplexity）转换：score = 1 / (1 + PPL/10)
        """
        self._ensure_loaded()
        if not text or not text.strip():
            return 1.0

        tokens = self._tokenizer.encode(text.strip())
        ids = tokens.ids

        if len(ids) > MAX_LEN - 2:
            ids = ids[:MAX_LEN - 2]

        input_ids = [CLS_ID] + ids + [SEP_ID]
        seq_len = len(input_ids)
        padding = [PAD_ID] * (MAX_LEN - seq_len)
        input_ids_padded = input_ids + padding
        attn_mask = [1] * seq_len + [0] * (MAX_LEN - seq_len)

        inputs = {
            'input_ids': np.array([input_ids_padded], dtype=np.int64),
            'attention_mask': np.array([attn_mask], dtype=np.int64),
        }
        logits = self._session.run(None, inputs)[0]

        # 计算每个位置的交叉熵损失（预测下一个 token）
        losses = []
        for i in range(seq_len - 1):
            pred = logits[0, i, :]
            true_id = input_ids[i + 1]
            # softmax + cross-entropy
            pred_max = np.max(pred)
            exp_pred = np.exp(pred - pred_max)
            probs = exp_pred / np.sum(exp_pred)
            loss = -np.log(max(probs[true_id], 1e-10))
            losses.append(loss)

        avg_loss = np.mean(losses) if losses else 0
        ppl = np.exp(avg_loss)
        return float(1.0 / (1.0 + ppl / 10.0))

    def is_continuation(self, text1: str, text2: str,
                        threshold: float = 1.5) -> bool:
        """判断 text2 是否是 text1 的内容延续。

        比较 text1、text2 和拼接后文本的评分。
        如果拼接后评分显著高于各自评分，说明是延续关系。

        Args:
            text1: 前半段文本
            text2: 后半段文本
            threshold: 拼接提升倍率阈值（默认 1.5 倍）

        Returns:
            bool: 是否构成内容延续
        """
        if not text1 or not text2:
            return False
        if len(text1.strip()) < 2 or len(text2.strip()) < 2:
            return False

        s1 = self.score(text1)
        s2 = self.score(text2)
        sc = self.score(text1.strip() + text2.strip())

        # 拼接后评分 > 各自最大评分的 threshold 倍
        best_individual = max(s1, s2)
        if best_individual <= 0:
            return False
        improvement = sc / best_individual

        # 同时检查：拼接后不低于单独评分，且明显提升
        return sc > best_individual and improvement >= threshold

    def clean_newlines(self, text: str,
                       threshold: float = 1.1) -> str:
        """去除文本中对自然度有负面影响的换行符。

        比较原文和去除 \\n 后的评分，若去除后评分提升超过 threshold，
        则返回去除换行符的版本。

        Args:
            text: 原始文本
            threshold: 评分提升阈值（默认 1.1 倍）

        Returns:
            str: 清理后的文本
        """
        if not text or '\n' not in text:
            return text

        cleaned = text.replace('\n', '')
        if cleaned == text:
            return text

        s_orig = self.score(text)
        s_clean = self.score(cleaned)

        if s_clean > s_orig * threshold:
            logger.debug(f"去除换行符: '{text[:30]}' -> '{cleaned[:30]}'"
                         f" (评分 {s_orig:.4f} -> {s_clean:.4f})")
            return cleaned

        return text


# 全局单例
_scorer = None


def get_scorer() -> TextScorer:
    """获取全局 TextScorer 单例。"""
    global _scorer
    if _scorer is None:
        _scorer = TextScorer()
    return _scorer
