"""
Incoherence detection and quality evaluation for personalization steering.

Split from coherence.py to meet 300-line limit.
"""

from __future__ import annotations

import re
from collections import Counter
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from transformers import PreTrainedModel, PreTrainedTokenizer
    import torch

from wisent.core.constants import (
    COHERENCE_UNIQUE_TOKEN_RATIO_MIN, COHERENCE_REPETITION_RATIO_MAX,
    COHERENCE_SPECIAL_CHAR_RATIO_MAX, COHERENCE_SPECIAL_CHAR_PENALTY,
    COHERENCE_SHORT_RESPONSE_PENALTY, COHERENCE_NONSENSE_WORD_RATIO_MAX,
    COHERENCE_TRIGRAM_REPEAT_THRESHOLD, COHERENCE_CONTENT_WORD_REPEAT_THRESHOLD,
    COHERENCE_FUNCTION_WORD_THRESHOLD, MIN_SENTENCE_LENGTH, SCORE_SCALE_100,
    MIN_RESPONSE_TOKENS, MIN_TOKENS_TRIGRAM, MIN_CONTENT_WORD_LENGTH,
    NONSENSE_MIN_TOKENS, MIN_TOKEN_LENGTH_NONSENSE,
    COHERENCE_DEGENERATION_PENALTY,
)
from wisent.core.evaluators.personalization.coherence import (
    _is_gibberish,
    _get_tokenizer,
    _is_nonsense_word,
)


def _is_incoherent(text: str) -> bool:
    """
    Detect if text is semantically incoherent or unhelpful.

    Catches issues that pass gibberish detection but are still low quality:
    - Too short to be helpful (< 20 chars for a response)
    - Repeated phrases/sentences
    - Circular/meaningless statements
    - Refusals or non-answers disguised as responses
    """
    if not text:
        return True

    text = text.strip()

    # Check 1: Too short to be a helpful response
    if len(text) < MIN_SENTENCE_LENGTH:
        return True

    # Check 2: Single word or very few words (unhelpful)
    tokens = text.split()
    if len(tokens) < MIN_RESPONSE_TOKENS:
        return True

    # Check 3: Consecutive duplicate words (e.g., "policymakers policymakers")
    tokens_lower = [t.lower().strip('.,!?"\'-') for t in tokens]
    for i in range(len(tokens_lower) - 1):
        if tokens_lower[i] == tokens_lower[i + 1] and len(tokens_lower[i]) > 2:
            return True

    # Check 4: Repeated sentences - split by sentence endings
    sentences = re.split(r'[.!?]+', text)
    sentences = [s.strip().lower() for s in sentences if s.strip()]
    if len(sentences) >= 2:
        unique_sentences = set(sentences)
        # If more than half the sentences are duplicates, it's repetitive
        if len(unique_sentences) < len(sentences) * 0.5:
            return True

    # Check 5: Repeated phrases (3+ word sequences appearing multiple times)
    if len(tokens) >= MIN_TOKENS_TRIGRAM:
        trigrams = [' '.join(tokens[i:i+3]) for i in range(len(tokens) - 2)]
        trigram_counts = Counter(trigrams)
        most_common_count = trigram_counts.most_common(1)[0][1] if trigrams else 0
        # If any trigram appears 3+ times, it's repetitive
        if most_common_count >= COHERENCE_TRIGRAM_REPEAT_THRESHOLD:
            return True

    # Check 6: Circular statements that don't add information
    # e.g., "Football, football, and the beautiful game are intertwined, intertwined, intertwined"
    unique_tokens = set(t.lower().strip('.,!?"\'-') for t in tokens)
    # If unique words are less than 40% of total words, very repetitive
    if len(tokens) >= 5 and len(unique_tokens) / len(tokens) < COHERENCE_UNIQUE_TOKEN_RATIO_MIN:
        return True

    # Check 7: Non-answer patterns
    non_answer_patterns = [
        r'^"?no"?\.?$',  # Just "No" or "No."
        r'^therefore\s+there\s+(are|is)\s+no',  # "Therefore there are no..."
        r'^you\s+didn\'?t\s+tell\s+me',  # "You didn't tell me..."
        r'^i\'?ve?\s+got\s+a\s+few\s+of\s+you',  # Nonsensical "I've got a few of you"
    ]
    text_lower = text.lower()
    for pattern in non_answer_patterns:
        if re.match(pattern, text_lower):
            return True

    # Check 8: Excessive repetition of the same word (3+ times for content words)
    content_words = [t.lower().strip('.,!?"\'-') for t in tokens if len(t) >= MIN_CONTENT_WORD_LENGTH]
    if content_words:
        word_counts = Counter(content_words)
        for word, count in word_counts.items():
            # Skip common filler words
            if word in {'that', 'this', 'have', 'been', 'with', 'from', 'they', 'would', 'could', 'should'}:
                continue
            # If any content word appears more than 3 times in a short response, flag it
            if count >= COHERENCE_CONTENT_WORD_REPEAT_THRESHOLD and count / len(content_words) > COHERENCE_FUNCTION_WORD_THRESHOLD:
                return True

    # Check 9: Nonsense words (using tokenizer fragmentation)
    tokenizer = _get_tokenizer()
    if tokenizer and len(tokens) >= NONSENSE_MIN_TOKENS:
        nonsense_count = 0
        for token in tokens_lower:
            if len(token) >= MIN_TOKEN_LENGTH_NONSENSE and _is_nonsense_word(token, tokenizer):
                nonsense_count += 1
        # If more than 15% of words are nonsense, flag it
        if nonsense_count / len(tokens) > COHERENCE_NONSENSE_WORD_RATIO_MAX:
            return True

    return False


def evaluate_quality(
    response: "str | list[str]",
    model: "PreTrainedModel | None" = None,
    tokenizer: "PreTrainedTokenizer | None" = None,
    device: "torch.device | None" = None,
) -> float:
    """
    Evaluate response quality using heuristic checks on a scale of 1-100.

    Checks for common quality issues:
    - Gibberish/nonsensical text (immediate zero)
    - Empty or too short responses
    - Repetitive tokens (single token appearing too frequently)
    - Repeated phrases (same bigram appearing multiple times)
    - Nonsensical patterns (excessive special characters)
    - Character repetition (same character repeated many times)

    Args:
        response: The response to evaluate (string or list of strings)
        model: The model (not used, kept for API compatibility)
        tokenizer: The tokenizer (not used, kept for API compatibility)
        device: Device (not used, kept for API compatibility)

    Returns:
        Quality score between 1 and 100
        - 100 = Perfect quality (no issues detected)
        - 1 = Very poor quality (multiple severe issues)
        - 0 = Gibberish detected
    """
    # Handle list inputs - compute average quality
    if isinstance(response, list):
        if not response:
            return 50.0  # Default if empty
        scores = [evaluate_quality(r) for r in response]
        return sum(scores) / len(scores)

    # Check for gibberish first - immediate zero if detected
    if _is_gibberish(response):
        return 0.0

    # Check for incoherent/unhelpful responses - immediate zero if detected
    if _is_incoherent(response):
        return 0.0

    score = 1.0  # Start with perfect score (0-1 scale)

    # Check 1: Empty or too short
    if len(response.strip()) < 10:
        score *= COHERENCE_SHORT_RESPONSE_PENALTY
        # Scale to 1-100 and return early
        return max(1.0, score * SCORE_SCALE_100 + 1.0)

    tokens = response.lower().split()

    # Check 2: Repetitive tokens
    if len(tokens) > 0:
        token_counts = Counter(tokens)
        most_common_count = token_counts.most_common(1)[0][1]
        repetition_ratio = most_common_count / len(tokens)

        # Penalize if any token appears more than 30% of the time
        if repetition_ratio > COHERENCE_REPETITION_RATIO_MAX:
            score *= 1.0 - (repetition_ratio - COHERENCE_REPETITION_RATIO_MAX)

    # Check 3: Repeated n-grams (phrases)
    bigrams = [f"{tokens[i]} {tokens[i+1]}" for i in range(len(tokens) - 1)]
    if bigrams:
        bigram_counts = Counter(bigrams)
        most_common_bigram_count = bigram_counts.most_common(1)[0][1]
        if most_common_bigram_count > 2:  # Same phrase repeated 3+ times
            score *= COHERENCE_DEGENERATION_PENALTY

    # Check 4: Nonsensical patterns (too many special chars)
    special_char_ratio = len(re.findall(r"[^a-zA-Z0-9\s.,!?']", response)) / max(
        len(response), 1
    )
    if special_char_ratio > COHERENCE_SPECIAL_CHAR_RATIO_MAX:
        score *= COHERENCE_SPECIAL_CHAR_PENALTY

    # Check 5: Single repeated character
    if re.search(r"(.)\1{10,}", response):  # Same char 10+ times in a row
        score *= 0.3

    # Ensure score is non-negative
    score = max(0.0, score)

    # Scale from 0-1 to 1-100
    quality_score = score * SCORE_SCALE_100 + 1.0

    return quality_score
