# SPDX-License-Identifier: Apache-2.0
"""Anchors: lexical continuity constraints.

An Anchor defines what MUST or MUST NOT appear in text.
Required/forbidden patterns are checked via substring matching (case-insensitive).
Violations record what went wrong and how severe it is.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Any


# Severity levels — same semantics as governor.continuity.Severity
SEVERITY_WARN = "warn"
SEVERITY_CORRECT = "correct"
SEVERITY_REJECT = "reject"
VALID_SEVERITIES = frozenset({SEVERITY_WARN, SEVERITY_CORRECT, SEVERITY_REJECT})

# Constraint classes — same semantics as governor.continuity.ConstraintClass
CONSTRAINT_INVARIANT = "invariant"
CONSTRAINT_PREFERENCE = "preference"
VALID_CONSTRAINT_CLASSES = frozenset({CONSTRAINT_INVARIANT, CONSTRAINT_PREFERENCE})


@dataclass(frozen=True)
class Anchor:
    """A lexical constraint on generated text.

    required: substrings that MUST appear in text.
    forbidden: substrings that MUST NOT appear in text.
    """

    id: str
    description: str
    required: tuple[str, ...] = ()
    forbidden: tuple[str, ...] = ()
    severity: str = SEVERITY_CORRECT
    constraint_class: str = CONSTRAINT_PREFERENCE

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

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "description": self.description,
            "required": list(self.required),
            "forbidden": list(self.forbidden),
            "severity": self.severity,
            "constraint_class": self.constraint_class,
        }

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> Anchor:
        return cls(
            id=d["id"],
            description=d["description"],
            required=tuple(d.get("required", [])),
            forbidden=tuple(d.get("forbidden", [])),
            severity=d.get("severity", SEVERITY_CORRECT),
            constraint_class=d.get("constraint_class", CONSTRAINT_PREFERENCE),
        )


@dataclass(frozen=True)
class Violation:
    """A specific anchor violation found during checking."""

    anchor_id: str
    severity: str
    description: str
    evidence: tuple[str, ...] = ()

    def to_dict(self) -> dict[str, Any]:
        return {
            "anchor_id": self.anchor_id,
            "severity": self.severity,
            "description": self.description,
            "evidence": list(self.evidence),
        }

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> Violation:
        return cls(
            anchor_id=d["anchor_id"],
            severity=d["severity"],
            description=d["description"],
            evidence=tuple(d.get("evidence", [])),
        )


def check_text(text: str, anchors: list[Anchor]) -> list[Violation]:
    """Check text against anchors. Returns all violations found.

    Matching is case-insensitive substring search (re.escape + re.IGNORECASE).
    Same algorithm as governor.continuity.ContinuityChecker._check_anchor().
    """
    violations: list[Violation] = []

    for anchor in anchors:
        # Check forbidden patterns
        for pattern in anchor.forbidden:
            escaped = re.escape(pattern)
            if re.search(escaped, text, re.IGNORECASE):
                violations.append(Violation(
                    anchor_id=anchor.id,
                    severity=anchor.severity,
                    description=f"Forbidden pattern found: '{pattern}'",
                    evidence=(f"Pattern '{pattern}' matched in text",),
                ))

        # Check required patterns
        for pattern in anchor.required:
            escaped = re.escape(pattern)
            if not re.search(escaped, text, re.IGNORECASE):
                violations.append(Violation(
                    anchor_id=anchor.id,
                    severity=anchor.severity,
                    description=f"Required pattern missing: '{pattern}'",
                    evidence=(f"Pattern '{pattern}' not found in text",),
                ))

    return violations
