# ============================================================
# src/accountable_record/pipeline/diagnostics.py
# ============================================================
"""Shared diagnostic vocabulary.

WHY:
    Diagnostics are the most-depended-on public surface in the whole system.
    Every stage raises them, every jurisdiction adds its own, humans read them,
    and downstream code matches on them. They must therefore be:

      - stably named by concern, never by pipeline position, and
      - self-identifying as to who owns them via a lineage prefix.

    A diagnostic code has three parts: <OWNER>.<CONCERN>.<NAME>.

      OWNER    lineage marker. AR for base diagnostics every record can raise.
               A general domain uses its own prefix; a jurisdiction uses its
               own. The prefix is the diagnostic analog of single inheritance:
               AR.* is spine, inherited by every record.

      CONCERN  the durable two-letter concern code. This names what kind of
               concern the diagnostic is about, independent of which stage
               number currently handles it.

      NAME     the specific condition, SCREAMING_SNAKE_CASE.

OBS:
    This module is Layer 0 vocabulary. It depends on nothing internal. That
    lets a substrate consumer borrow it without dragging in the pipeline and
    lets it port cleanly to -rs.

OBS:
    DiagnosticSpec intentionally avoids Python-only callable generics. The
    shape ports directly to Rust as a struct. Python stages may use a message
    callable for convenience, but the durable public object remains:
    code + severity + message builder + description.

REQ:
    - No internal dependencies. Pure stdlib + typing.
    - Concern codes are a closed StrEnum on the AR side.
    - Owners are open: jurisdictions supply their own owner prefix as a plain
      string when they mint diagnostics.
"""

from collections.abc import Callable
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Self

__all__ = [
    "AR_OWNER",
    "Concern",
    "Diagnostic",
    "DiagnosticCode",
    "DiagnosticSpec",
    "Diagnostics",
    "Severity",
    "ar_code",
    "from_spec",
    "from_spec_at",
]

AR_OWNER: str = "AR"


class Concern(StrEnum):
    """Durable two-letter concern codes."""

    IDENTITY = "ID"
    CONTRACT = "CT"
    SOURCES = "SR"
    ELEMENTS = "EL"
    CATALOG = "CA"
    EXPORTS = "EX"
    RESOLUTION = "RS"
    GENERATED = "GN"
    DOCS = "DC"
    COMMANDS = "CM"


class Severity(StrEnum):
    """Diagnostic severity."""

    ERROR = "error"
    WARNING = "warning"


@dataclass(frozen=True)
class DiagnosticCode:
    """A fully-qualified diagnostic code: OWNER.CONCERN.NAME."""

    owner: str
    concern: Concern
    name: str

    @property
    def rendered(self) -> str:
        """Return the canonical OWNER.CONCERN.NAME string form."""
        return f"{self.owner}.{self.concern.value}.{self.name}"

    @property
    def is_base(self) -> bool:
        """True if this is an AR-spine diagnostic inherited by all records."""
        return self.owner == AR_OWNER

    def __str__(self) -> str:
        """Stringify as the rendered form."""
        return self.rendered


@dataclass(frozen=True)
class Diagnostic:
    """One emitted diagnostic."""

    code: DiagnosticCode
    severity: Severity
    message: str
    location: str | None = None

    @property
    def is_error(self) -> bool:
        """True when this diagnostic should fail the stage."""
        return self.severity is Severity.ERROR


@dataclass
class Diagnostics:
    """A mutable collection of diagnostics gathered during a stage run."""

    items: list[Diagnostic] = field(default_factory=lambda: [])

    def add(self, diagnostic: Diagnostic) -> None:
        """Add a diagnostic to the collection."""
        self.items.append(diagnostic)

    def extend(self, other: Self) -> None:
        """Extend this collection with another Diagnostics instance."""
        self.items.extend(other.items)

    def copy(self) -> Self:
        """Return a shallow copy of this diagnostics collection."""
        return type(self)(items=list(self.items))

    def merged(self, other: Self) -> Self:
        """Return a new diagnostics collection containing both collections."""
        merged = self.copy()
        merged.extend(other)
        return merged

    @property
    def errors(self) -> list[Diagnostic]:
        """Return diagnostics with severity ERROR."""
        return [diagnostic for diagnostic in self.items if diagnostic.is_error]

    @property
    def has_errors(self) -> bool:
        """Return true if any diagnostics have severity ERROR."""
        return any(diagnostic.is_error for diagnostic in self.items)

    @property
    def exit_code(self) -> int:
        """Return 0 if no errors, else 1."""
        return 1 if self.has_errors else 0


@dataclass(frozen=True)
class DiagnosticSpec:
    """Definition of one diagnostic emitted by a stage.

    OBS:
        This is passive metadata plus a Python message builder. It is not a
        checker. A Rust port can represent the same object as code, severity,
        description, and a stage-local message-rendering function.
    """

    code: DiagnosticCode
    severity: Severity
    message: Callable[..., str]
    description: str


def ar_code(concern: Concern, name: str) -> DiagnosticCode:
    """Mint a base AR-owned diagnostic code."""
    return DiagnosticCode(owner=AR_OWNER, concern=concern, name=name)


def from_spec(
    spec: DiagnosticSpec,
    *args: object,
    **kwargs: object,
) -> Diagnostic:
    """Build a Diagnostic from a diagnostic spec."""
    return Diagnostic(
        code=spec.code,
        severity=spec.severity,
        message=spec.message(*args, **kwargs),
    )


def from_spec_at(
    spec: DiagnosticSpec,
    location: str | None,
    *args: object,
    **kwargs: object,
) -> Diagnostic:
    """Build a Diagnostic from a diagnostic spec with a location."""
    return Diagnostic(
        code=spec.code,
        severity=spec.severity,
        message=spec.message(*args, **kwargs),
        location=location,
    )
