"""Pointer-line parser for ``answer_mode="claim_lattice_pointer"``.

The CTI / Clause Lattice Intelligence runtime accepts weak natural-language
pointer clauses from the model and compiles them into structured claim
nodes. Hermes does NOT produce JSON, does NOT produce a formal graph —
Hermes only writes:

    Claim text here. [E12]
    Another claim. [E13,E14]

The runtime owns the structure. The model owns the prose. This stays
inside the model's prose-generation distribution (citation-style is
heavily represented in training; random hex tokens are not), so one-
shot SCHEMA_INVALID failures from JSON drift disappear.

Two-layer id discipline:

    pointer_id     E1, E2, E3, ...     short numeric tag the model writes
    evidence_id    E1f8e4c2a, ...      content-addressed sha256, run-stable

The model's prompt only ever shows pointer ids. The runtime maps each
pointer id back to its content-addressed evidence_id and stores the
content-addressed form in the cache, run-DAG, and audit chain so
provenance is run-stable.

Parser status taxonomy:

    PARSED                line had at least one ``[E\\d+...]`` tag and
                          surviving claim text after the strip
    NO_EVIDENCE_POINTER   line had non-empty prose but no pointer tag —
                          model produced a claim without citing
    EMPTY                 line was bullet/whitespace only (skipped)
"""

from __future__ import annotations

import re
from dataclasses import asdict, dataclass


# Match a bracketed pointer-id list. Examples:
#     [E12]
#     [E12,E13]
#     [E12, E13 ,E14]
#
# The outer brackets are required so the parser refuses fuzzy alternatives
# like (E12) or {E12}. The inner content is a comma-separated list of
# E\d+ tokens with optional whitespace. Pointer ids carry one capture
# group across the WHOLE bracket so the strip is clean.
_BRACKET_RE = re.compile(r"\[\s*(E\d+(?:\s*,\s*E\d+)*)\s*\]")

# Pointer id within a captured bracket payload.
_POINTER_RE = re.compile(r"E\d+")

# Bullet markers (subset of verify._BULLET_RE — kept narrower here so
# this module has no cross-import to verify.py): -, *, +, •, "1.", "2)".
_BULLET_RE = re.compile(r"^\s*(?:[-*+•]|\d+[.)])\s+")

# Trailing connector punctuation often left attached when the parser
# strips the trailing bracket. We keep terminal sentence punctuation
# (".", "!", "?", ":") because that's part of the claim text.
_TRAILING_STRIP = " ,;-—\t"


@dataclass(frozen=True)
class ParsedClaim:
    """One line of model output, parsed.

    - ``claim_text``     line with bullet marker stripped and pointer
                          tags removed; the natural-language claim
                          surviving for verification & rendering.
    - ``pointer_ids``    short ids the model wrote, in the order they
                          appeared. Decimal-suffixed E-prefixed
                          (``E1``, ``E12``). Verifier maps each back
                          to the content-addressed evidence_id.
    - ``parse_status``   PARSED / NO_EVIDENCE_POINTER. Empty/whitespace
                          lines are filtered by the parser and never
                          surface as claims.
    - ``raw_line``       the line as the model wrote it, pre-strip.
                          Useful for the audit trail and for the
                          render stage hash.
    """

    claim_text: str
    pointer_ids: list[str]
    parse_status: str
    raw_line: str

    def to_dict(self) -> dict:
        return asdict(self)


def parse_pointer_claims(text: str) -> list[ParsedClaim]:
    """Walk ``text`` line by line; emit one ``ParsedClaim`` per non-empty
    line.

    Skips empty / whitespace-only lines. Lines with at least one
    ``[E\\d+...]`` bracket get ``parse_status="PARSED"``; lines with prose
    but no bracket get ``parse_status="NO_EVIDENCE_POINTER"``. The
    verifier owns the policy on what to do with a NO_EVIDENCE_POINTER
    claim — typically it counts toward the denominator and downgrades
    the verdict to HYBRID/UNGROUNDED.

    Pointer-id ordering is preserved as the model wrote them; duplicates
    are NOT deduped here so the verifier can flag repeated cites.
    """
    out: list[ParsedClaim] = []
    for raw_line in text.splitlines():
        # Skip lines that are pure whitespace or only a bullet glyph.
        stripped_for_emptiness = _BULLET_RE.sub("", raw_line).strip()
        if not stripped_for_emptiness:
            continue

        # Pull every bracket-list match; collect all pointer ids inside.
        bracket_matches = list(_BRACKET_RE.finditer(raw_line))
        if not bracket_matches:
            out.append(ParsedClaim(
                claim_text=stripped_for_emptiness,
                pointer_ids=[],
                parse_status="NO_EVIDENCE_POINTER",
                raw_line=raw_line,
            ))
            continue

        pointer_ids: list[str] = []
        for m in bracket_matches:
            pointer_ids.extend(_POINTER_RE.findall(m.group(1)))

        # Claim text = line minus every bracket match, then bullet- and
        # trailing-punct-stripped. Preserves prose word order even when
        # the model interleaves tags mid-sentence.
        claim_text = _BRACKET_RE.sub("", raw_line)
        claim_text = _BULLET_RE.sub("", claim_text).strip()
        claim_text = claim_text.rstrip(_TRAILING_STRIP)

        out.append(ParsedClaim(
            claim_text=claim_text,
            pointer_ids=pointer_ids,
            parse_status="PARSED",
            raw_line=raw_line,
        ))
    return out
