# ============================================================
# src/accountable_record/stage_020_contract/validation.py
# ============================================================
"""Stage 020 contract-context validation for Accountable Record.

WHY:
    Stage 020 runs after identity. It establishes the repository's contract
    context from the schema-valid [contract] table.

OBS:
    Contract repositories define contract authority fields. Implementation
    repositories declare the contract they consume. Stage 020 branches by the
    repository profile supplied by pipeline context, not by illegal manifest
    sections and not by old [contract].name / [contract].org fields.

REQ:
    Returns a frozen result carrying diagnostics plus the parsed declaration
    and resolved context, matching the stage validation pattern used elsewhere.
"""

from dataclasses import dataclass, field
from pathlib import Path

from accountable_record.pipeline.context import PipelineState
from accountable_record.pipeline.diagnostics import Diagnostics
from accountable_record.stage_020_contract.base import diagnostics as dx
from accountable_record.stage_020_contract.base.constants import (
    EXPECTED_CONTRACT_NAME,
    MANIFEST_FILENAME,
    STAGE_NAME,
)
from accountable_record.stage_020_contract.load import read_contract_declaration
from accountable_record.stage_020_contract.model import (
    ContractContext,
    ContractDeclaration,
)


@dataclass(frozen=True)
class ContractValidationResult:
    """Validation result for stage 020."""

    stage: str = STAGE_NAME
    diagnostics: Diagnostics = field(default_factory=Diagnostics)
    declaration: ContractDeclaration | None = None
    contract: ContractContext | None = None

    @property
    def has_errors(self) -> bool:
        """Return true if any error-severity diagnostics are present."""
        return self.diagnostics.has_errors

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


def validate_contract_stage(state: PipelineState) -> ContractValidationResult:
    """Validate the contract context of the target repository."""
    diagnostics = Diagnostics()

    profile = state.context.repository_profile
    repo_root = state.context.target_root
    repo_class = profile.repo_class
    identity_version = _identity_version_from_state(state)

    declaration = _read_declaration(
        repo_root=repo_root,
        repo_class=repo_class,
        diagnostics=diagnostics,
    )
    if declaration is None:
        return ContractValidationResult(diagnostics=diagnostics)

    version_aligned = _check_version_alignment(
        declaration,
        identity_version,
        diagnostics,
        version_location=MANIFEST_FILENAME,
    )
    targets_expected = _check_contract_name(
        declaration,
        expected_contract_name=EXPECTED_CONTRACT_NAME,
        diagnostics=diagnostics,
    )

    contract = ContractContext(
        declaration=declaration,
        identity_version=identity_version,
        version_aligned=version_aligned,
        targets_expected_contract=targets_expected,
    )

    return ContractValidationResult(
        diagnostics=diagnostics,
        declaration=declaration,
        contract=contract,
    )


def _identity_version_from_state(state: PipelineState) -> str | None:
    """Return the identity version from pipeline state, if available."""
    identity = state.identity
    version = getattr(identity, "version", None)
    if isinstance(version, str) and version.strip():
        return version
    return None


def _read_declaration(
    *,
    repo_root: Path,
    repo_class: str,
    diagnostics: Diagnostics,
) -> ContractDeclaration | None:
    """Read the contract declaration, recording diagnostics on failure."""
    try:
        return read_contract_declaration(repo_root, repo_class=repo_class)
    except FileNotFoundError:
        diagnostics.add(dx.from_spec(dx.CONTRACT_MISSING))
        return None
    except ValueError as exc:
        text = str(exc)
        if "requires [contract] table" in text or "requires a [contract] table" in text:
            diagnostics.add(dx.from_spec(dx.CONTRACT_MISSING))
        else:
            diagnostics.add(dx.from_spec(dx.CONTRACT_INVALID, detail=text))
        return None


def _check_version_alignment(
    declaration: ContractDeclaration,
    identity_version: str | None,
    diagnostics: Diagnostics,
    *,
    version_location: str,
) -> bool:
    """Check contract version against the identity version.

    Returns:
        True when aligned or when there is nothing to compare; False when a
        real disagreement was found and recorded.
    """
    contract_version = declaration.contract_version
    if not contract_version or not identity_version:
        return True

    if contract_version != identity_version:
        diagnostics.add(
            dx.from_spec(
                dx.CONTRACT_VERSION_MISMATCH,
                contract_version=contract_version,
                identity_version=identity_version,
                location=version_location,
            )
        )
        return False

    return True


def _check_contract_name(
    declaration: ContractDeclaration,
    expected_contract_name: str | None,
    diagnostics: Diagnostics,
) -> bool:
    """Check the declared/consumed contract name against an expectation.

    Returns:
        True when the declaration targets the expected contract, or when no
        expectation was supplied; False when it disagrees.
    """
    if expected_contract_name is None:
        return True

    declared_name = _declared_contract_name(declaration)
    if declared_name == expected_contract_name:
        return True

    diagnostics.add(
        dx.from_spec(
            dx.UNEXPECTED_CONTRACT_NAME,
            contract_name=declared_name or "<unknown>",
        )
    )
    return False


def _declared_contract_name(declaration: ContractDeclaration) -> str | None:
    """Return the contract name implied by the schema-valid declaration."""
    if declaration.consumes_contract_from:
        return _repo_name_from_reference(declaration.consumes_contract_from)

    if declaration.contract_authority:
        return _repo_name_from_reference(declaration.contract_authority)

    return None


def _repo_name_from_reference(reference: str) -> str:
    """Extract the repository name from org/repo@ref-style references."""
    without_ref = reference.split("@", maxsplit=1)[0]
    return without_ref.rstrip("/").rsplit("/", maxsplit=1)[-1]
