# SPDX-License-Identifier: Apache-2.0
"""Claims: assertive statement detection.

Extracts assertive claims from text using regex pattern matching.
This is a strict subset of governor.claim_signals — assertive and
hedged statements only. Date/entity/quantity extraction belongs
to the governor runtime, not the kernel.

Compatibility contract: any text that nlai classifies as ASSERTIVE
must also be classified as assertive by governor's SignalExtractor.
The reverse is NOT required (governor may extract more).
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Any

from .evidence import Evidence


# Claim status
STATUS_UNSUPPORTED = "unsupported"
STATUS_SUPPORTED = "supported"
STATUS_CONTESTED = "contested"

# Claim strength
STRENGTH_ASSERTIVE = "assertive"
STRENGTH_HEDGED = "hedged"
VALID_STRENGTHS = frozenset({STRENGTH_ASSERTIVE, STRENGTH_HEDGED})


# ---------------------------------------------------------------------------
# Pre-compiled patterns (module-level constants, identical to governor)
# ---------------------------------------------------------------------------

# Assertive language — same pattern as governor.claim_signals.ASSERTIVE_PATTERN
ASSERTIVE_PATTERN = re.compile(
    r"\b(definitely|certainly|clearly|undoubtedly|obviously|"
    r"was acquired|acquired in|founded in|established in|"
    r"is located|is headquartered|died in|born in)\b",
    re.IGNORECASE,
)

# Hedged language — weak claims that are still claims
HEDGED_PATTERN = re.compile(
    r"\b(I think|I believe|probably|might be|could be|"
    r"perhaps|possibly|it seems|likely|unlikely|"
    r"may have|might have|appears to)\b",
    re.IGNORECASE,
)


@dataclass(frozen=True)
class Claim:
    """An extracted claim from text.

    text: the matched phrase or surrounding sentence.
    strength: "assertive" or "hedged".
    status: starts as "unsupported" — evidence promotes to "supported".
            "supported" = evidence record(s) attached, NOT independently validated.
            "contested" = conflicting claims detected; sticky until human resolution.
    span: (start, end) character offsets in original text.
    evidence: attached evidence records (opaque — kernel records, doesn't validate).
    conflicts_with: texts of contradicting claims.
    """

    text: str
    strength: str
    status: str = STATUS_UNSUPPORTED
    span: tuple[int, int] = (0, 0)
    evidence: tuple[Evidence, ...] = ()
    conflicts_with: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        if self.strength not in VALID_STRENGTHS:
            raise ValueError(
                f"Invalid strength {self.strength!r}; "
                f"must be one of {sorted(VALID_STRENGTHS)}"
            )

    def to_dict(self) -> dict[str, Any]:
        # Canonical sorted order for stable hashes
        sorted_evidence = sorted(
            [e.to_dict() for e in self.evidence],
            key=lambda e: (e["kind"], e["reference"], e["source"]),
        )
        sorted_conflicts = sorted(self.conflicts_with)
        return {
            "text": self.text,
            "strength": self.strength,
            "status": self.status,
            "span": list(self.span),
            "evidence": sorted_evidence,
            "conflicts_with": sorted_conflicts,
        }

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> Claim:
        evidence_dicts = d.get("evidence", [])
        evidence = tuple(Evidence.from_dict(e) for e in evidence_dicts)
        conflicts = tuple(d.get("conflicts_with", []))
        return cls(
            text=d["text"],
            strength=d["strength"],
            status=d.get("status", STATUS_UNSUPPORTED),
            span=tuple(d.get("span", [0, 0])),
            evidence=evidence,
            conflicts_with=conflicts,
        )


def attach_evidence(claim: Claim, evidence: Evidence) -> Claim:
    """Attach evidence record, promote UNSUPPORTED → SUPPORTED.

    CONTESTED stays CONTESTED — evidence doesn't resolve contests.
    Returns new Claim (frozen). Idempotent: duplicate evidence ignored.
    """
    # Dedupe by value equality on frozen dataclass
    if evidence in claim.evidence:
        return claim

    new_evidence = claim.evidence + (evidence,)

    # Status transition: UNSUPPORTED → SUPPORTED, CONTESTED stays CONTESTED
    new_status = claim.status
    if claim.status == STATUS_UNSUPPORTED:
        new_status = STATUS_SUPPORTED

    return Claim(
        text=claim.text,
        strength=claim.strength,
        status=new_status,
        span=claim.span,
        evidence=new_evidence,
        conflicts_with=claim.conflicts_with,
    )


def contest_claim(claim: Claim, conflicting_text: str) -> Claim:
    """Mark claim as CONTESTED, record the conflicting text.

    Returns new Claim (frozen). Idempotent: duplicate conflict ignored.
    conflicting_text must be non-empty.
    """
    if not conflicting_text:
        raise ValueError("conflicting_text must be non-empty")

    # Dedupe by string equality
    if conflicting_text in claim.conflicts_with:
        return claim

    new_conflicts = claim.conflicts_with + (conflicting_text,)

    return Claim(
        text=claim.text,
        strength=claim.strength,
        status=STATUS_CONTESTED,
        span=claim.span,
        evidence=claim.evidence,
        conflicts_with=new_conflicts,
    )


def extract_claims(text: str) -> list[Claim]:
    """Extract assertive and hedged claims from text.

    Returns a list of Claim objects with their spans. Assertive claims
    are stronger signals; hedged claims are weaker but still tracked.
    """
    claims: list[Claim] = []
    seen_spans: set[tuple[int, int]] = set()

    # Assertive claims first (higher priority)
    for m in ASSERTIVE_PATTERN.finditer(text):
        span = m.span()
        if span not in seen_spans:
            seen_spans.add(span)
            claims.append(Claim(
                text=m.group(0),
                strength=STRENGTH_ASSERTIVE,
                span=span,
            ))

    # Hedged claims
    for m in HEDGED_PATTERN.finditer(text):
        span = m.span()
        if span not in seen_spans:
            seen_spans.add(span)
            claims.append(Claim(
                text=m.group(0),
                strength=STRENGTH_HEDGED,
                span=span,
            ))

    return claims


def assertiveness_score(text: str) -> float:
    """Score text assertiveness from 0.0 (no claims) to 1.0 (highly assertive).

    Same scoring semantics as governor.claim_signals.assertiveness_score.
    """
    claims = extract_claims(text)
    if not claims:
        return 0.0

    assertive_count = sum(1 for c in claims if c.strength == STRENGTH_ASSERTIVE)
    total = len(claims)

    # Assertive claims contribute full weight, hedged claims half
    hedged_count = total - assertive_count
    weighted = assertive_count + (hedged_count * 0.5)

    # Normalize: 5+ weighted claims = score 1.0
    return min(weighted / 5.0, 1.0)
