# ============================================================
# src/accountable_record/stage_010_identity/validation.py
# ============================================================
"""Stage 010 identity validation for Accountable Record.

WHY:
    Validates a TARGET repository's identity. By default this is pure internal
    coherence: the target's SE_MANIFEST.toml declares its identity, and the
    other root files are checked for agreement with that declaration. The
    validator is target-agnostic. It does NOT assume the target is named
    accountable-record, accountable-record-py, or anything else.

    Version is contract-only / publication metadata. When no version exists
    anywhere, version alignment is skipped (a no-op, not a warning, not an
    error). When CITATION.cff carries a version, it is canonical: any manifest
    or pyproject version is compared against it and the diagnostic names
    CITATION.cff as authoritative.

    ExpectedIdentity is an optional EXTERNAL assertion (the rare
    distrust-the-manifest guard), checked only when supplied by the caller.
    It is not part of the default target path.
"""

from dataclasses import dataclass, field
from pathlib import Path

from accountable_record.pipeline.diagnostics import Diagnostics
from accountable_record.stage_010_identity.base import diagnostics as dx
from accountable_record.stage_010_identity.base.constants import (
    CITATION_FILENAME,
    PYPROJECT_FILENAME,
    STAGE_NAME,
)
from accountable_record.stage_010_identity.citation import (
    CitationIdentity,
    read_citation_identity,
)
from accountable_record.stage_010_identity.manifest import (
    ManifestIdentity,
    read_manifest_identity,
)
from accountable_record.stage_010_identity.repository import (
    IdentityArtifact,
    find_repository_root,
)
from accountable_record.stage_010_identity.versioning import (
    PyprojectIdentity,
    VersionAlignment,
    read_pyproject_identity,
)


@dataclass(frozen=True)
class IdentityValidationResult:
    """Validation result for stage 010."""

    stage: str = STAGE_NAME
    diagnostics: Diagnostics = field(default_factory=Diagnostics)
    identity: IdentityArtifact | None = None
    pyproject: PyprojectIdentity | None = None
    manifest: ManifestIdentity | None = None
    citation: CitationIdentity | None = None
    versions: VersionAlignment | 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_identity_stage(
    target_root: Path | str | None = None,
    *,
    expected: object | None = None,
) -> IdentityValidationResult:
    """Validate the identity of the target repository."""
    diagnostics = Diagnostics()

    repo_root = _resolve_root(target_root, diagnostics)
    if repo_root is None:
        return IdentityValidationResult(diagnostics=diagnostics)

    pyproject = _read_pyproject(repo_root, diagnostics)
    manifest = _read_manifest(repo_root, diagnostics)
    citation = _read_citation(repo_root, diagnostics)

    versions: VersionAlignment | None = None
    identity: IdentityArtifact | None = None

    if pyproject and manifest and citation:
        versions = _check_version_alignment(pyproject, manifest, citation, diagnostics)

        identity = IdentityArtifact(
            root=repo_root,
            package_name=pyproject.package_name,
            project_name=pyproject.project_name,
            repository_name=manifest.repository_name,
            organization=manifest.organization,
            version=pyproject.version,
            status=manifest.status,
            summary=manifest.summary,
        )

        _check_name_coherence(identity, diagnostics)
        if expected is not None:
            _check_expected_identity(identity, expected, diagnostics)

    return IdentityValidationResult(
        diagnostics=diagnostics,
        identity=identity,
        pyproject=pyproject,
        manifest=manifest,
        citation=citation,
        versions=versions,
    )


def require_valid_identity_stage(
    target_root: Path | str | None = None,
) -> IdentityArtifact:
    """Return target repository identity or raise ValueError with diagnostics."""
    result = validate_identity_stage(target_root)
    if result.has_errors or result.identity is None:
        messages = "; ".join(item.message for item in result.diagnostics.items)
        msg = f"{STAGE_NAME} failed: {messages}"
        raise ValueError(msg)
    return result.identity


def _resolve_root(
    target_root: Path | str | None,
    diagnostics: Diagnostics,
) -> Path | None:
    """Resolve the target repository root."""
    if target_root is None:
        try:
            return find_repository_root(None)
        except Exception as exc:  # noqa: BLE001
            diagnostics.add(dx.from_spec(dx.ROOT_NOT_FOUND, detail=str(exc)))
            return None

    root = Path(target_root).resolve()
    if not root.is_dir():
        diagnostics.add(
            dx.from_spec(
                dx.ROOT_NOT_FOUND,
                detail=f"Target repository root is not a directory: {root}",
            )
        )
        return None

    return root


def _check_version_alignment(
    pyproject: PyprojectIdentity,
    manifest: ManifestIdentity,
    citation: CitationIdentity,
    diagnostics: Diagnostics,
) -> VersionAlignment:
    """Check conditional, CITATION-anchored version alignment."""
    py_v = pyproject.version
    mf_v = manifest.version
    cf_v = citation.version

    alignment = VersionAlignment(
        pyproject_version=py_v,
        manifest_version=mf_v,
        citation_version=cf_v,
    )

    present = [version for version in (py_v, mf_v, cf_v) if version]
    if not present:
        return alignment

    if not cf_v:
        diagnostics.add(
            dx.from_spec(
                dx.VERSION_AUTHORITY_MISSING,
                location=CITATION_FILENAME,
            )
        )
        return alignment

    for value in (py_v, mf_v):
        if value and value != cf_v:
            diagnostics.add(
                dx.from_spec(
                    dx.VERSION_MISMATCH,
                    pyproject_version=py_v,
                    manifest_version=mf_v,
                    citation_version=cf_v,
                    location=CITATION_FILENAME,
                )
            )

    return alignment


def _check_name_coherence(
    identity: IdentityArtifact,
    diagnostics: Diagnostics,
) -> None:
    """Check pyproject project name agrees with manifest repository name."""
    if identity.project_name != identity.repository_name:
        diagnostics.add(
            dx.from_spec(
                dx.NAME_MISMATCH,
                project_name=identity.project_name,
                repository_name=identity.repository_name,
            )
        )


def _check_expected_identity(
    identity: IdentityArtifact,
    expected: object,
    diagnostics: Diagnostics,
) -> None:
    """Check optional external identity assertions."""
    checks = (
        (
            "project_name",
            identity.project_name,
            dx.UNEXPECTED_PROJECT_NAME,
        ),
        (
            "package_name",
            identity.package_name,
            dx.UNEXPECTED_PACKAGE_NAME,
        ),
        (
            "repository_name",
            identity.repository_name,
            dx.UNEXPECTED_REPOSITORY_NAME,
        ),
        (
            "organization",
            identity.organization,
            dx.UNEXPECTED_ORGANIZATION,
        ),
    )

    for attr, actual, spec in checks:
        expected_value = getattr(expected, attr, None)
        if expected_value is not None and actual != expected_value:
            diagnostics.add(
                dx.from_spec(
                    spec,
                    expected=expected_value,
                    actual=actual,
                )
            )


def _read_pyproject(
    repo_root: Path,
    diagnostics: Diagnostics,
) -> PyprojectIdentity | None:
    """Read pyproject identity metadata."""
    try:
        return read_pyproject_identity(repo_root)
    except Exception as exc:  # noqa: BLE001
        diagnostics.add(
            dx.from_spec(
                dx.PYPROJECT_INVALID,
                detail=str(exc),
                location=str(repo_root / PYPROJECT_FILENAME),
            )
        )
        return None


def _read_manifest(
    repo_root: Path,
    diagnostics: Diagnostics,
) -> ManifestIdentity | None:
    """Read manifest identity metadata."""
    try:
        return read_manifest_identity(repo_root)
    except FileNotFoundError as exc:
        diagnostics.add(dx.from_spec(dx.MANIFEST_MISSING, detail=str(exc)))
        return None
    except Exception as exc:  # noqa: BLE001
        diagnostics.add(dx.from_spec(dx.MANIFEST_INVALID, detail=str(exc)))
        return None


def _read_citation(
    repo_root: Path,
    diagnostics: Diagnostics,
) -> CitationIdentity | None:
    """Read citation identity metadata."""
    try:
        return read_citation_identity(repo_root)
    except FileNotFoundError as exc:
        diagnostics.add(dx.from_spec(dx.CITATION_MISSING, detail=str(exc)))
        return None
    except Exception as exc:  # noqa: BLE001
        diagnostics.add(
            dx.from_spec(
                dx.CITATION_INVALID,
                detail=str(exc),
                location=str(repo_root / CITATION_FILENAME),
            )
        )
        return None
