"""Heuristic evaluators — pure Python stdlib, no LLM calls required."""
from __future__ import annotations

import re
from collections import Counter
from dataclasses import dataclass, field
from typing import List


@dataclass
class EvaluatorResult:
    score: float
    indicators: List[str] = field(default_factory=list)
    notes: str = ""


@dataclass
class EvaluationResult:
    confidence_score: float
    hallucination_score: float
    coherence_score: float
    instruction_following_score: float
    hallucination_indicators: List[str] = field(default_factory=list)
    confidence_indicators: List[str] = field(default_factory=list)
    coherence_indicators: List[str] = field(default_factory=list)
    instruction_indicators: List[str] = field(default_factory=list)

    @property
    def all_indicators(self) -> List[str]:
        return (
            self.confidence_indicators
            + self.hallucination_indicators
            + self.coherence_indicators
            + self.instruction_indicators
        )


# ── Confidence Evaluator ──────────────────────────────────────────────────────

_HEDGING = [
    "i'm not sure", "i don't know", "i cannot verify", "i may be wrong",
    "approximately", "roughly", "it's possible", "it seems", "i believe",
    "as far as i know", "to my knowledge", "i think", "it appears",
]
_OVERCONFIDENT = [
    "definitely", "absolutely", "certainly", "100%", "guaranteed",
    "always", "never", "without a doubt", "i am certain", "it is a fact",
    "proven that", "definitively",
]
_FILLER = [
    "as an ai language model", "as an ai", "i cannot provide",
    "i don't have access to real-time", "my knowledge cutoff",
]
_STOP_WORDS = {
    "the", "a", "an", "is", "are", "was", "were", "be", "been",
    "have", "has", "do", "does", "did", "will", "would", "could",
    "should", "may", "might", "can", "i", "you", "we", "it", "in",
    "on", "at", "to", "for", "of", "and", "or", "but", "not",
}


def _evaluate_confidence(prompt: str, response: str) -> EvaluatorResult:
    score = 65.0
    indicators: list[str] = []

    r = response.lower()
    prompt_words = prompt.split()
    response_words = response.split()

    ratio = len(response_words) / max(len(prompt_words), 1)
    if ratio < 0.5 and len(prompt_words) > 20:
        score -= 12
        indicators.append("Response is very short relative to a complex prompt")
    elif ratio > 30:
        score -= 5
        indicators.append("Response may be excessively verbose")
    elif 1.0 <= ratio <= 15.0:
        score += 8

    sentences = [s.strip() for s in re.split(r"[.!?]", response) if s.strip()]
    if len(sentences) >= 3:
        score += 7
    elif len(sentences) == 1 and len(response_words) > 40:
        score -= 5
        indicators.append("Long response lacks sentence breaks — may be run-on")

    hedging_count = sum(1 for h in _HEDGING if h in r)
    if 1 <= hedging_count <= 3:
        score += 6
    elif hedging_count > 5:
        score -= 5
        indicators.append("Excessive hedging — response may be unreliable")

    overconfident_count = sum(1 for o in _OVERCONFIDENT if o in r)
    if overconfident_count > 0:
        score -= overconfident_count * 4
        indicators.append(f"Overconfident language detected ({overconfident_count} instance(s))")

    filler_count = sum(1 for f in _FILLER if f in r)
    if filler_count > 0:
        score -= filler_count * 3
        indicators.append("Response contains generic AI disclaimer language")

    prompt_keywords = {w.lower().strip("?,.:!") for w in prompt_words
                       if len(w) > 4 and w.lower() not in _STOP_WORDS}
    response_set = {w.lower().strip("?,.:!") for w in response_words}
    if prompt_keywords:
        coverage = len(prompt_keywords & response_set) / len(prompt_keywords)
        score += coverage * 14
        if coverage < 0.2:
            indicators.append("Response appears to miss key topics from the prompt")

    if any(marker in response for marker in ["1.", "2.", "- ", "* ", "•", "##", "**"]):
        score += 5

    return EvaluatorResult(score=round(max(0.0, min(100.0, score)), 1), indicators=indicators)


# ── Hallucination Evaluator ───────────────────────────────────────────────────

_FAKE_CITATION_PATTERNS = [
    (r"\b[A-Z][a-z]+ et al\.,?\s+\d{4}\b", "Possible fabricated citation (Author et al., year)"),
    (r"\bJournal of [A-Z][a-z]+(?: [A-Z][a-z]+){1,3}\b", "Possible fabricated journal name"),
    (r"\b(?:Vol\.|Volume)\s+\d+,\s+(?:No\.|Issue)\s+\d+\b", "Possible fabricated journal volume/issue"),
    (r"\bpp\.\s+\d+[-\u2013]\d+\b", "Possible fabricated page range"),
    (r'"[^"]{10,60}"\s*\(\d{4}\)', "Possible fabricated quoted source with year"),
]
_VAGUE_STAT_PATTERNS = [
    (r"\b\d{1,3}\.?\d?\s*%\s+of\s+(?:companies|people|users|employees|organizations|businesses|Americans|adults)\b",
     "Suspiciously specific statistic without a cited source"),
    (r"\bstudies?\s+show(?:s)?\s+that\b", "Unattributed 'studies show' claim"),
    (r"\bresearch\s+(?:suggests?|shows?|indicates?|finds?)\s+that\b", "Unattributed research claim"),
    (r"\baccording\s+to\s+(?:a\s+)?(?:recent\s+)?(?:study|report|survey|analysis)\b",
     "Unattributed 'according to a study' claim"),
    (r"\b(?:experts?|scientists?|researchers?)\s+(?:say|believe|suggest|agree)\b",
     "Vague expert attribution without source"),
]
_TEMPORAL_FABRICATION = [
    (r"\bin\s+(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+20\d{2}\b",
     "Specific date claim — verify accuracy"),
    (r"\bas\s+of\s+20\d{2}\b", "Temporal claim that may be outdated or fabricated"),
]
_EPISTEMIC_ACKNOWLEDGMENTS = [
    "i don't have", "i cannot access", "i'm not sure", "i don't know",
    "i may be wrong", "please verify", "you should check", "as of my knowledge cutoff",
    "i cannot confirm", "i would recommend verifying",
]


def _evaluate_hallucination(prompt: str, response: str) -> EvaluatorResult:
    score = 0.0
    indicators: list[str] = []
    r_lower = response.lower()

    for pattern, label in _FAKE_CITATION_PATTERNS:
        matches = re.findall(pattern, response)
        if matches:
            score += 15
            indicators.append(f"{label}: '{matches[0]}'")

    for pattern, label in _VAGUE_STAT_PATTERNS:
        if re.findall(pattern, r_lower):
            score += 10
            indicators.append(label)

    for pattern, label in _TEMPORAL_FABRICATION:
        matches = re.findall(pattern, response)
        if matches:
            score += 8
            indicators.append(f"{label}: '{matches[0]}'")

    long_quoted = re.findall(r'"([A-Z][^"]{15,60})"', response)
    for q in long_quoted:
        if not any(c in q for c in ["?", "!"]):
            score += 5
            indicators.append(f'Unverified quoted entity: "{q[:50]}"')
            break

    acknowledgment_count = sum(1 for phrase in _EPISTEMIC_ACKNOWLEDGMENTS if phrase in r_lower)
    score -= acknowledgment_count * 8

    factual_domains = ["history", "law", "medicine", "finance", "science",
                       "statistics", "regulation", "legal", "clinical"]
    if sum(1 for d in factual_domains if d in r_lower) >= 2:
        score += 5

    return EvaluatorResult(score=round(max(0.0, min(100.0, score)), 1), indicators=indicators)


# ── Coherence Evaluator ───────────────────────────────────────────────────────

_TRANSITION_WORDS = [
    "first", "second", "third", "finally", "however", "therefore",
    "additionally", "furthermore", "moreover", "consequently", "in conclusion",
    "in summary", "as a result", "for example", "for instance", "specifically",
    "in contrast", "on the other hand", "meanwhile", "subsequently",
]
_COHERENCE_STOP_WORDS = {
    "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
    "have", "has", "had", "do", "does", "did", "will", "would", "could",
    "should", "may", "might", "can", "i", "you", "we", "it", "in", "on",
    "at", "to", "for", "of", "and", "or", "but", "not", "this", "that",
    "with", "from", "by", "as", "an", "if", "so", "about", "also",
}


def _evaluate_coherence(prompt: str, response: str) -> EvaluatorResult:
    score = 60.0
    indicators: list[str] = []

    if not response.strip():
        return EvaluatorResult(score=0.0, indicators=["Empty response"])

    r_lower = response.lower()
    words = [w.strip(".,!?;:\"'()[]") for w in response.split()]
    words_filtered = [w for w in words if w.lower() not in _COHERENCE_STOP_WORDS and len(w) > 2]
    sentences = [s.strip() for s in re.split(r"[.!?]", response) if s.strip()]

    if len(sentences) >= 2:
        score += 10
    avg_sentence_len = sum(len(s.split()) for s in sentences) / max(len(sentences), 1)
    if 8 <= avg_sentence_len <= 30:
        score += 8
    elif avg_sentence_len > 60:
        score -= 10
        indicators.append("Sentences are very long — may indicate run-on text")

    transitions = sum(1 for t in _TRANSITION_WORDS if t in r_lower)
    if transitions >= 2:
        score += 8
    elif transitions == 0 and len(sentences) > 4:
        score -= 5
        indicators.append("No transition words in a multi-sentence response")

    if words_filtered and len(words_filtered) > 10:
        word_freq = Counter(words_filtered)
        most_common_word, most_common_count = word_freq.most_common(1)[0]
        if most_common_count / len(words_filtered) > 0.15:
            score -= 12
            indicators.append(f"High word repetition: '{most_common_word}' appears {most_common_count}x")

    if re.search(r"^\s*[-•*]\s+", response, re.MULTILINE):
        score += 6
    if re.search(r"^\s*\d+[.)]\s+", response, re.MULTILINE):
        score += 6

    if prompt.strip().endswith("?"):
        question_words = {"what", "how", "why", "when", "where", "who", "which"}
        prompt_q = prompt.lower().split()[0] if prompt.split() else ""
        if prompt_q in question_words and len(response.split()) > 5:
            score += 5

    if len(words) < 5:
        score -= 20
        indicators.append("Response is extremely short")

    return EvaluatorResult(score=round(max(0.0, min(100.0, score)), 1), indicators=indicators)


# ── Instruction Following Evaluator ──────────────────────────────────────────

_FORMAT_INSTRUCTIONS = [
    (r"\bbullet\s+point", lambda r: bool(re.search(r"[-•*]\s+", r))),
    (r"\bnumbered\s+list", lambda r: bool(re.search(r"\d+[.)]\s+", r))),
    (r"\bin\s+(?:a\s+)?table", lambda r: "|" in r),
    (r"\bjson\b", lambda r: r.strip().startswith("{") or r.strip().startswith("[")),
    (r"\bmarkdown\b", lambda r: "#" in r or "**" in r or "```" in r),
    (r"\bcode\s+block\b", lambda r: "```" in r),
    (r"\bin\s+one\s+(?:sentence|paragraph)", lambda r: len(re.split(r"[.!?]", r.strip())) <= 3),
]
_NEGATIVE_INSTRUCTIONS = [
    (r"\bdo\s+not\s+(?:include|mention|use)\b", "Possible violation of negative instruction"),
    (r"\bwithout\s+(?:using|mentioning|including)\b", "Possible violation of exclusion instruction"),
    (r"\bavoid\b", "Check if avoidance instruction was followed"),
]
_ROLE_INSTRUCTIONS = [
    r"\bact\s+as\b",
    r"\byou\s+are\s+a\b",
    r"\bpretend\s+(?:you\s+are|to\s+be)\b",
    r"\bbehave\s+as\b",
]


def _evaluate_instruction_following(prompt: str, response: str) -> EvaluatorResult:
    score = 70.0
    indicators: list[str] = []
    p_lower = prompt.lower()
    r_lower = response.lower()

    for pattern, checker in _FORMAT_INSTRUCTIONS:
        if re.search(pattern, p_lower) and checker is not None:
            if checker(response):
                score += 10
            else:
                score -= 15
                indicators.append(f"Format instruction may not be followed: '{pattern}'")

    word_count_match = re.search(r"in\s+(\d+)\s+words?", p_lower)
    if word_count_match:
        requested = int(word_count_match.group(1))
        actual = len(response.split())
        if abs(actual - requested) / max(requested, 1) <= 0.25:
            score += 10
        else:
            direction = "too long" if actual > requested else "too short"
            score -= 10
            indicators.append(f"Word count {direction}: requested {requested}, got {actual}")

    for pattern, label in _NEGATIVE_INSTRUCTIONS:
        if re.search(pattern, p_lower):
            match = re.search(pattern + r"\s+(\w+)", p_lower)
            if match:
                forbidden = match.group(1)
                if forbidden in r_lower and len(forbidden) > 3:
                    score -= 10
                    indicators.append(f"Possible violation: '{forbidden}' found despite exclusion instruction")

    for pattern in _ROLE_INSTRUCTIONS:
        if re.search(pattern, p_lower):
            if response.lower().startswith("as an ai"):
                score -= 10
                indicators.append("Response breaks character by opening with 'As an AI'")
            break

    if p_lower.strip().startswith(("what is", "what are", "who is", "when did", "where is")):
        first_sentence = re.split(r"[.!?]", response)[0].lower()
        if re.search(r"(you'?ve? asked|you want to know|great question)", first_sentence):
            score -= 5
            indicators.append("Response opens with an unnecessary preamble")

    return EvaluatorResult(score=round(max(0.0, min(100.0, score)), 1), indicators=indicators)


# ── Public evaluate() function ────────────────────────────────────────────────

def evaluate(prompt: str, response: str) -> EvaluationResult:
    """Run all 4 heuristic evaluators and return combined result."""
    confidence = _evaluate_confidence(prompt, response)
    hallucination = _evaluate_hallucination(prompt, response)
    coherence = _evaluate_coherence(prompt, response)
    instruction = _evaluate_instruction_following(prompt, response)

    return EvaluationResult(
        confidence_score=confidence.score,
        hallucination_score=hallucination.score,
        coherence_score=coherence.score,
        instruction_following_score=instruction.score,
        hallucination_indicators=hallucination.indicators,
        confidence_indicators=confidence.indicators,
        coherence_indicators=coherence.indicators,
        instruction_indicators=instruction.indicators,
    )
