"""
cortex/hippo/quality.py — Quality scoring for Hippo summaries.

Scores summaries on compression, coherence, and coverage.
Weights: compression=0.3, coherence=0.3, coverage=0.4

Issue: #805
"""

from __future__ import annotations

import math
import re
from collections import Counter
from dataclasses import dataclass, field


# Rough stopwords (English) — kept small intentionally
_STOPWORDS = frozenset(
    "a an the is are was were be been being have has had do does did "
    "will would shall should may might can could must need and but or "
    "nor not so yet for of in on at to by from with as into through "
    "about above below between under over after before during since "
    "until while if then than that this these those it its he she they "
    "we you i me him her us them my your his their our what which who "
    "whom how when where why all each every both few many much some "
    "any no more most other another such only own same also just even "
    "still already too very".split()
)


def _estimate_tokens(text: str) -> int:
    """Rough token estimate: ~4 chars per token."""
    return max(1, -(-len(text) // 4))  # ceiling division


def _tokenize(text: str) -> list[str]:
    """Extract lowercase words, removing stopwords and short tokens."""
    words = re.findall(r"[a-zA-Z0-9_/.:-]+", text.lower())
    return [w for w in words if len(w) >= 2 and w not in _STOPWORDS]


@dataclass
class QualityScoreResult:
    total: float  # 0.0 - 1.0
    compression: float  # 0.0 - 1.0
    coherence: float  # 0.0 - 1.0
    coverage: float  # 0.0 - 1.0
    reasons: list[str] = field(default_factory=list)


def score_quality(
    input_text: str,
    summary: str,
    is_condensed: bool = False,
) -> QualityScoreResult:
    """Score summary quality on a 0-1 scale.

    Weights: compression=0.3, coherence=0.3, coverage=0.4
    """
    comp_score, comp_reason = _score_compression(input_text, summary, is_condensed)
    coh_score, coh_reason = _score_coherence(summary, is_condensed)
    cov_score, cov_reason = _score_coverage(input_text, summary)
    total = 0.3 * comp_score + 0.3 * coh_score + 0.4 * cov_score
    return QualityScoreResult(
        total=round(total, 3),
        compression=round(comp_score, 3),
        coherence=round(coh_score, 3),
        coverage=round(cov_score, 3),
        reasons=[comp_reason, coh_reason, cov_reason],
    )


def _score_compression(
    input_text: str, summary: str, is_condensed: bool
) -> tuple[float, str]:
    """Score based on compression ratio.

    Leaf ideal: 0.05-0.15 of input tokens.
    Condensed ideal: 0.10-0.25 of input tokens.
    """
    input_tokens = _estimate_tokens(input_text)
    summary_tokens = _estimate_tokens(summary)

    if input_tokens == 0:
        return (0.0, "Empty input")

    ratio = summary_tokens / input_tokens

    if is_condensed:
        ideal_low, ideal_high = 0.10, 0.25
    else:
        ideal_low, ideal_high = 0.05, 0.15

    if ideal_low <= ratio <= ideal_high:
        score = 1.0
        reason = f"Compression ratio {ratio:.2f} in ideal range [{ideal_low}-{ideal_high}]"
    elif ratio < ideal_low:
        # Too compressed — linear falloff to 0 at ratio=0.01
        floor_ratio = 0.01
        if ratio <= floor_ratio:
            score = 0.0
        else:
            score = (ratio - floor_ratio) / (ideal_low - floor_ratio)
        reason = f"Over-compressed: ratio {ratio:.2f} < {ideal_low}"
    else:
        # Too verbose — linear falloff to 0 at ratio=0.8
        ceiling_ratio = 0.8
        if ratio >= ceiling_ratio:
            score = 0.0
        else:
            score = (ceiling_ratio - ratio) / (ceiling_ratio - ideal_high)
        reason = f"Under-compressed: ratio {ratio:.2f} > {ideal_high}"

    return (max(0.0, min(1.0, score)), reason)


def _score_coherence(summary: str, is_condensed: bool = False) -> tuple[float, str]:
    """Score structural coherence.

    Checks:
    - Has "Expand for details about:" footer
    - Reasonable sentence count
    - Doesn't start with meta-commentary
    - Has timestamp markers (for condensed)
    - Not a single wall of text
    """
    score = 0.0
    notes = []

    # Check for expand footer (+0.4)
    if "expand for details about:" in summary.lower():
        score += 0.4
        notes.append("has expand footer")
    else:
        notes.append("missing expand footer")

    # Sentence count check (+0.2)
    sentences = re.split(r"[.!?]+", summary)
    sentence_count = len([s for s in sentences if s.strip()])
    if 3 <= sentence_count <= 100:
        score += 0.2
        notes.append(f"sentence count {sentence_count} in range")
    else:
        notes.append(f"sentence count {sentence_count} out of range [3-100]")

    # Meta-commentary check (+0.2)
    stripped = summary.strip()
    if stripped.lower().startswith("this summary"):
        notes.append("starts with meta-commentary")
    else:
        score += 0.2
        notes.append("no meta-commentary")

    # Timestamp/structure check
    has_timestamps = bool(re.search(r"\d{4}-\d{2}-\d{2}", summary))
    if is_condensed:
        if has_timestamps:
            score += 0.1
            notes.append("has timestamps (condensed)")
        else:
            notes.append("missing timestamps (condensed)")
        # Line structure
        lines = [l for l in summary.split("\n") if l.strip()]
        if len(lines) >= 2:
            score += 0.1
            notes.append("multi-line structure")
    else:
        score += 0.2
        notes.append("leaf coherence baseline")

    return (min(1.0, score), "; ".join(notes))


def _score_coverage(input_text: str, summary: str) -> tuple[float, str]:
    """Score coverage via key term overlap.

    Algorithm:
    1. Tokenize both (lowercase, remove stopwords)
    2. Compute top-N key terms from input by frequency
    3. Score = fraction of top-N terms appearing in summary
    """
    input_tokens = _tokenize(input_text)
    summary_tokens_set = set(_tokenize(summary))

    if not input_tokens:
        return (1.0, "No key terms in input")

    # Get top-N by frequency (TF-like weighting)
    freq = Counter(input_tokens)
    n = min(20, max(3, len(input_tokens) // 10))
    top_terms = [term for term, _ in freq.most_common(n)]

    if not top_terms:
        return (1.0, "No key terms extracted")

    matches = sum(1 for t in top_terms if t in summary_tokens_set)
    score = matches / len(top_terms)
    return (
        round(score, 3),
        f"Coverage: {matches}/{len(top_terms)} key terms present",
    )
