# ============================================================
# src/accountable_record/pipeline/context.py
# ============================================================
"""Pipeline run context and accumulated state.

WHY:
    PipelineRunContext is fixed run input.
    PipelineState is accumulated pipeline output.

    PipelineState is generic so pipeline/ does not import stage packages.
    Stage modules specialize artifact slots locally without reversing the
    dependency direction.
"""

from dataclasses import dataclass, field, replace
from pathlib import Path
from typing import Self

from accountable_record.pipeline.diagnostics import Diagnostics
from accountable_record.pipeline.model import IdentityArtifact
from accountable_record.pipeline.repository_profile import (
    RepositoryProfile,
    repository_profile_from_target,
)

__all__ = [
    "AnyPipelineState",
    "ExpectedIdentity",
    "PipelineRunContext",
    "PipelineState",
]


@dataclass(frozen=True)
class ExpectedIdentity:
    """Optional external assertion about what a target repository must be."""

    project_name: str | None = None
    package_name: str | None = None
    repository_name: str | None = None
    organization: str | None = None


@dataclass(frozen=True)
class PipelineRunContext:
    """Fixed input for one pipeline run, shared by every stage."""

    target_root: Path
    repository_profile: RepositoryProfile
    strict: bool = True
    expected: ExpectedIdentity | None = None

    @classmethod
    def from_target(
        cls,
        target: Path | str | None = None,
        *,
        strict: bool = True,
        expected: ExpectedIdentity | None = None,
    ) -> Self:
        """Build a context from a target path. Default: current directory."""
        root = Path.cwd() if target is None else Path(target).resolve()
        return cls(
            target_root=root,
            repository_profile=repository_profile_from_target(root),
            strict=strict,
            expected=expected,
        )


@dataclass(frozen=True)
class PipelineState[
    ContractArtifactT = object,
    SourcesArtifactT = object,
    ElementsArtifactT = object,
    CatalogArtifactT = object,
    ExportsArtifactT = object,
    ResolutionArtifactT = object,
    GeneratedArtifactT = object,
    DocsArtifactT = object,
]:
    """Accumulated state produced by pipeline stages.

    Type parameters:
        ContractArtifactT: Artifact folded into state by s020.
        SourcesArtifactT: Artifact folded into state by s030.
        ElementsArtifactT: Artifact folded into state by s040.
        CatalogArtifactT: Artifact folded into state by s050.
        ExportsArtifactT: Artifact folded into state by s060.
        ResolutionArtifactT: Artifact folded into state by s070.
        GeneratedArtifactT: Artifact folded into state by s080.
        DocsArtifactT: Artifact folded into state by s090.
    """

    context: PipelineRunContext
    diagnostics: Diagnostics = field(default_factory=Diagnostics)
    failed_stage: str | None = None

    identity: IdentityArtifact | None = None
    contract: ContractArtifactT | None = None
    sources: SourcesArtifactT | None = None
    elements: ElementsArtifactT | None = None
    catalog: CatalogArtifactT | None = None
    exports: ExportsArtifactT | None = None
    resolution: ResolutionArtifactT | None = None
    generated: GeneratedArtifactT | None = None
    docs: DocsArtifactT | None = None

    @classmethod
    def initial(cls, context: PipelineRunContext) -> Self:
        """Build the starting state for a run."""
        return cls(context=context)

    @property
    def has_failed(self) -> bool:
        """True once any stage has recorded a failure."""
        return self.failed_stage is not None

    @property
    def exit_code(self) -> int:
        """Process exit code derived from accumulated diagnostics."""
        return self.diagnostics.exit_code

    def with_failure(
        self,
        stage_code: str,
        diagnostics: Diagnostics,
    ) -> Self:
        """Return a new failed state with accumulated diagnostics."""
        return replace(
            self,
            diagnostics=self.diagnostics.merged(diagnostics),
            failed_stage=stage_code,
        )

    def with_diagnostics(self, diagnostics: Diagnostics) -> Self:
        """Return a new state with accumulated diagnostics and no artifact update."""
        return replace(
            self,
            diagnostics=self.diagnostics.merged(diagnostics),
        )

    def with_identity(
        self,
        identity: IdentityArtifact,
        diagnostics: Diagnostics,
    ) -> Self:
        """Return state with identity output and accumulated diagnostics."""
        return replace(
            self,
            diagnostics=self.diagnostics.merged(diagnostics),
            identity=identity,
        )

    def with_contract(
        self,
        contract: ContractArtifactT,
        diagnostics: Diagnostics,
    ) -> Self:
        """Return state with contract output and accumulated diagnostics."""
        return replace(
            self,
            diagnostics=self.diagnostics.merged(diagnostics),
            contract=contract,
        )

    def with_sources(
        self,
        sources: SourcesArtifactT,
        diagnostics: Diagnostics,
    ) -> Self:
        """Return state with sources output and accumulated diagnostics."""
        return replace(
            self,
            diagnostics=self.diagnostics.merged(diagnostics),
            sources=sources,
        )

    def with_elements(
        self,
        elements: ElementsArtifactT,
        diagnostics: Diagnostics,
    ) -> Self:
        """Return state with elements output and accumulated diagnostics."""
        return replace(
            self,
            diagnostics=self.diagnostics.merged(diagnostics),
            elements=elements,
        )

    def with_catalog(
        self,
        catalog: CatalogArtifactT,
        diagnostics: Diagnostics,
    ) -> Self:
        """Return state with catalog output and accumulated diagnostics."""
        return replace(
            self,
            diagnostics=self.diagnostics.merged(diagnostics),
            catalog=catalog,
        )

    def with_exports(
        self,
        exports: ExportsArtifactT,
        diagnostics: Diagnostics,
    ) -> Self:
        """Return state with exports output and accumulated diagnostics."""
        return replace(
            self,
            diagnostics=self.diagnostics.merged(diagnostics),
            exports=exports,
        )

    def with_resolution(
        self,
        resolution: ResolutionArtifactT,
        diagnostics: Diagnostics,
    ) -> Self:
        """Return state with resolution output and accumulated diagnostics."""
        return replace(
            self,
            diagnostics=self.diagnostics.merged(diagnostics),
            resolution=resolution,
        )

    def with_generated(
        self,
        generated: GeneratedArtifactT,
        diagnostics: Diagnostics,
    ) -> Self:
        """Return state with generated-artifact output and accumulated diagnostics."""
        return replace(
            self,
            diagnostics=self.diagnostics.merged(diagnostics),
            generated=generated,
        )

    def with_docs(
        self,
        docs: DocsArtifactT,
        diagnostics: Diagnostics,
    ) -> Self:
        """Return state with docs output and accumulated diagnostics."""
        return replace(
            self,
            diagnostics=self.diagnostics.merged(diagnostics),
            docs=docs,
        )


type AnyPipelineState = PipelineState[
    object,
    object,
    object,
    object,
    object,
    object,
    object,
    object,
]
