from __future__ import annotations

import uuid
from collections.abc import Iterable, Mapping
from datetime import datetime
from typing import Any, Self

import strawberry
from aiotools import apartial
from strawberry import ID, UNSET, Info
from strawberry.dataloader import DataLoader
from strawberry.relay import Connection, Edge, Node, NodeID
from strawberry.scalars import JSON

from ai.backend.common.data.artifact.types import (
    VerificationStepResult,
    VerifierResult,
)
from ai.backend.common.data.storage.registries.types import ModelTarget as ModelTargetData
from ai.backend.manager.api.gql.artifact_registry_meta import ArtifactRegistryMeta
from ai.backend.manager.api.gql.base import (
    ByteSize,
    IntFilter,
    OrderDirection,
    StringFilter,
    UUIDFilter,
)
from ai.backend.manager.api.gql.types import GQLFilter, GQLOrderBy, StrawberryGQLContext
from ai.backend.manager.api.gql.utils import dedent_strip
from ai.backend.manager.data.artifact.types import (
    ArtifactAvailability,
    ArtifactData,
    ArtifactOrderField,
    ArtifactRemoteStatus,
    ArtifactRevisionData,
    ArtifactRevisionOrderField,
    ArtifactStatus,
    ArtifactType,
)
from ai.backend.manager.data.artifact.types import DelegateeTarget as DelegateeTargetData
from ai.backend.manager.defs import ARTIFACT_MAX_SCAN_LIMIT
from ai.backend.manager.repositories.artifact.options import (
    ArtifactConditions,
    ArtifactOrders,
    ArtifactRevisionConditions,
    ArtifactRevisionOrders,
)
from ai.backend.manager.repositories.base import (
    QueryCondition,
    QueryOrder,
    combine_conditions_or,
    negate_conditions,
)
from ai.backend.manager.services.artifact.actions.get import GetArtifactAction
from ai.backend.manager.services.artifact_revision.actions.get import GetArtifactRevisionAction


@strawberry.type(
    name="ArtifactVerifierMetadataEntry",
    description=dedent_strip("""
    Added in 26.1.0.

    A single key-value entry representing metadata from an artifact verifier.
    Contains additional information about the verification process.
    """),
)
class ArtifactVerifierMetadataEntryGQL:
    """Single metadata entry with key and value."""

    key: str = strawberry.field(description="The key identifier for this metadata entry.")
    value: str = strawberry.field(description="The value for this metadata entry.")


@strawberry.type(
    name="ArtifactVerifierMetadata",
    description=dedent_strip("""
    Added in 26.1.0.

    A collection of metadata from an artifact verifier.
    Contains key-value pairs providing additional information about the verification.
    """),
)
class ArtifactVerifierMetadataGQL:
    """Metadata containing multiple key-value entries."""

    entries: list[ArtifactVerifierMetadataEntryGQL] = strawberry.field(
        description="List of metadata entries. Each entry contains a key-value pair."
    )

    @classmethod
    def from_mapping(cls, data: Mapping[str, str]) -> ArtifactVerifierMetadataGQL:
        """Convert a Mapping to GraphQL type."""
        entries = [ArtifactVerifierMetadataEntryGQL(key=k, value=v) for k, v in data.items()]
        return cls(entries=entries)


@strawberry.type(
    name="ArtifactVerifierResult",
    description=dedent_strip("""
    Added in 25.17.0.

    Result from a single malware verifier containing scan results and metadata.

    Each verifier scans the artifact for potential security issues and reports
    findings including infected file count, scan time, and any errors encountered.
    """),
)
class ArtifactVerifierGQLResult:
    success: bool = strawberry.field(description="Whether the verification completed successfully")
    infected_count: int = strawberry.field(
        description="Number of infected or suspicious files detected"
    )
    scanned_at: datetime = strawberry.field(description="Timestamp when verification started")
    scan_time: float = strawberry.field(
        description="Time taken to complete verification in seconds"
    )
    scanned_count: int = strawberry.field(description="Total number of files scanned")
    metadata: ArtifactVerifierMetadataGQL = strawberry.field(
        description="Added in 26.1.0. Additional metadata from the verifier."
    )
    error: str | None = strawberry.field(
        description="Fatal error message if the verifier failed to complete"
    )

    @classmethod
    def from_dataclass(cls, data: VerifierResult) -> Self:
        return cls(
            success=data.success,
            infected_count=data.infected_count,
            scanned_at=data.scanned_at,
            scan_time=data.scan_time,
            scanned_count=data.scanned_count,
            metadata=ArtifactVerifierMetadataGQL.from_mapping(data.metadata),
            error=data.error,
        )


@strawberry.type(
    name="ArtifactVerifierResultEntry",
    description=dedent_strip("""
    Added in 25.17.0.

    Entry for a single verifier's result in the verification results.

    Associates a verifier name with its scan results.
    """),
)
class ArtifactVerifierGQLResultEntry:
    name: str = strawberry.field(description="Name of the verifier (e.g., 'clamav', 'custom')")
    result: ArtifactVerifierGQLResult = strawberry.field(
        description="Scan result from this verifier"
    )

    @classmethod
    def from_verifier_result(cls, name: str, result: VerifierResult) -> Self:
        return cls(
            name=name,
            result=ArtifactVerifierGQLResult.from_dataclass(result),
        )


@strawberry.type(
    name="ArtifactVerificationResult",
    description=dedent_strip("""
    Added in 25.17.0.

    Complete verification result containing results from all configured verifiers.

    Artifacts undergo malware scanning through multiple verifiers after being imported.
    This type aggregates results from all verifiers that were run on the artifact.
    """),
)
class ArtifactVerificationGQLResult:
    verifiers: list[ArtifactVerifierGQLResultEntry] = strawberry.field(
        description="Results from each verifier that scanned the artifact"
    )

    @classmethod
    def from_dataclass(cls, data: VerificationStepResult) -> Self:
        verifier_entries = [
            ArtifactVerifierGQLResultEntry.from_verifier_result(verifier_name, verifier_result)
            for verifier_name, verifier_result in data.verifiers.items()
        ]
        return cls(verifiers=verifier_entries)


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Filter options for artifacts based on various criteria such as type, name, registry,
    source, and availability status.

    Supports logical operations (AND, OR, NOT) for complex filtering scenarios.
    """)
)
class ArtifactFilter(GQLFilter):
    type: list[ArtifactType] | None = None
    name: StringFilter | None = None
    registry: StringFilter | None = None
    source: StringFilter | None = None
    availability: list[ArtifactAvailability] | None = None

    AND: list[ArtifactFilter] | None = None
    OR: list[ArtifactFilter] | None = None
    NOT: list[ArtifactFilter] | None = None

    def build_conditions(self) -> list[QueryCondition]:
        """Build query conditions from this filter.

        Returns a list containing QueryConditions that represent
        all filters with proper logical operators applied.
        """
        # Collect direct field conditions (these will be combined with AND)
        field_conditions: list[QueryCondition] = []

        # Apply type filter
        if self.type:
            field_conditions.append(ArtifactConditions.by_types(self.type))

        # Apply name filter
        if self.name:
            name_condition = self.name.build_query_condition(
                contains_factory=ArtifactConditions.by_name_contains,
                equals_factory=ArtifactConditions.by_name_equals,
                starts_with_factory=ArtifactConditions.by_name_starts_with,
                ends_with_factory=ArtifactConditions.by_name_ends_with,
            )
            if name_condition:
                field_conditions.append(name_condition)

        # Apply registry filter
        if self.registry:
            registry_condition = self.registry.build_query_condition(
                contains_factory=ArtifactConditions.by_registry_contains,
                equals_factory=ArtifactConditions.by_registry_equals,
                starts_with_factory=ArtifactConditions.by_registry_starts_with,
                ends_with_factory=ArtifactConditions.by_registry_ends_with,
            )
            if registry_condition:
                field_conditions.append(registry_condition)

        # Apply source filter
        if self.source:
            source_condition = self.source.build_query_condition(
                contains_factory=ArtifactConditions.by_source_contains,
                equals_factory=ArtifactConditions.by_source_equals,
                starts_with_factory=ArtifactConditions.by_source_starts_with,
                ends_with_factory=ArtifactConditions.by_source_ends_with,
            )
            if source_condition:
                field_conditions.append(source_condition)

        # Apply availability filter
        if self.availability:
            field_conditions.append(ArtifactConditions.by_availability(self.availability))

        # Handle AND logical operator - these are implicitly ANDed with field conditions
        if self.AND:
            for sub_filter in self.AND:
                field_conditions.extend(sub_filter.build_conditions())

        # Handle OR logical operator
        if self.OR:
            or_sub_conditions: list[QueryCondition] = []
            for sub_filter in self.OR:
                or_sub_conditions.extend(sub_filter.build_conditions())
            if or_sub_conditions:
                field_conditions.append(combine_conditions_or(or_sub_conditions))

        # Handle NOT logical operator
        if self.NOT:
            not_sub_conditions: list[QueryCondition] = []
            for sub_filter in self.NOT:
                not_sub_conditions.extend(sub_filter.build_conditions())
            if not_sub_conditions:
                field_conditions.append(negate_conditions(not_sub_conditions))

        return field_conditions


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Specifies the field and direction for ordering artifacts in queries.
    """)
)
class ArtifactOrderBy(GQLOrderBy):
    field: ArtifactOrderField
    direction: OrderDirection = OrderDirection.ASC

    def to_query_order(self) -> QueryOrder:
        """Convert to repository QueryOrder."""
        ascending = self.direction == OrderDirection.ASC
        match self.field:
            case ArtifactOrderField.NAME:
                return ArtifactOrders.name(ascending)
            case ArtifactOrderField.TYPE:
                return ArtifactOrders.type(ascending)
            case ArtifactOrderField.SCANNED_AT:
                return ArtifactOrders.scanned_at(ascending)
            case ArtifactOrderField.UPDATED_AT:
                return ArtifactOrders.updated_at(ascending)
            case ArtifactOrderField.SIZE:
                # SIZE ordering is not supported yet, fall back to updated_at
                return ArtifactOrders.updated_at(ascending)


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Filter for artifact revision status. Supports exact match or inclusion in a list of statuses.
    """)
)
class ArtifactRevisionStatusFilter:
    in_: list[ArtifactStatus] | None = strawberry.field(name="in", default=None)
    equals: ArtifactStatus | None = None


@strawberry.input(description="Added in 25.16.0")
class ArtifactRevisionRemoteStatusFilter:
    in_: list[ArtifactRemoteStatus] | None = strawberry.field(name="in", default=None)
    equals: ArtifactRemoteStatus | None = None


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Filter options for artifact revisions based on status, version, artifact ID, and file size.

    Supports logical operations (AND, OR, NOT) for complex filtering scenarios.
    """)
)
class ArtifactRevisionFilter(GQLFilter):
    status: ArtifactRevisionStatusFilter | None = None
    remote_status: ArtifactRevisionRemoteStatusFilter | None = strawberry.field(
        default=None, description="Added in 25.16.0"
    )
    version: StringFilter | None = None
    artifact_id: UUIDFilter | None = strawberry.field(default=None)
    size: IntFilter | None = None

    AND: list[ArtifactRevisionFilter] | None = None
    OR: list[ArtifactRevisionFilter] | None = None
    NOT: list[ArtifactRevisionFilter] | None = None

    def build_conditions(self) -> list[QueryCondition]:
        """Build query conditions from this filter.

        Returns a list containing QueryConditions that represent
        all filters with proper logical operators applied.
        """
        # Collect direct field conditions (these will be combined with AND)
        field_conditions: list[QueryCondition] = []

        # Apply status filter
        if self.status:
            statuses_to_filter: list[ArtifactStatus] = []
            if self.status.in_:
                statuses_to_filter = self.status.in_
            elif self.status.equals:
                statuses_to_filter = [self.status.equals]

            if statuses_to_filter:
                field_conditions.append(ArtifactRevisionConditions.by_statuses(statuses_to_filter))

        # Apply remote_status filter
        if self.remote_status:
            remote_statuses_to_filter: list[ArtifactRemoteStatus] = []
            if self.remote_status.in_:
                remote_statuses_to_filter = self.remote_status.in_
            elif self.remote_status.equals:
                remote_statuses_to_filter = [self.remote_status.equals]

            if remote_statuses_to_filter:
                field_conditions.append(
                    ArtifactRevisionConditions.by_remote_statuses(remote_statuses_to_filter)
                )

        # Apply version filter
        if self.version:
            version_condition = self.version.build_query_condition(
                contains_factory=ArtifactRevisionConditions.by_version_contains,
                equals_factory=ArtifactRevisionConditions.by_version_equals,
                starts_with_factory=ArtifactRevisionConditions.by_version_starts_with,
                ends_with_factory=ArtifactRevisionConditions.by_version_ends_with,
            )
            if version_condition:
                field_conditions.append(version_condition)

        # Apply artifact_id filter
        if self.artifact_id:
            if self.artifact_id.equals:
                field_conditions.append(
                    ArtifactRevisionConditions.by_artifact_id(self.artifact_id.equals)
                )
            elif self.artifact_id.in_:
                field_conditions.append(
                    ArtifactRevisionConditions.by_artifact_ids(self.artifact_id.in_)
                )

        # Apply size filter
        if self.size:
            if self.size.equals is not None:
                field_conditions.append(ArtifactRevisionConditions.by_size_equals(self.size.equals))
            elif self.size.not_equals is not None:
                field_conditions.append(
                    ArtifactRevisionConditions.by_size_not_equals(self.size.not_equals)
                )
            elif self.size.greater_than is not None:
                field_conditions.append(
                    ArtifactRevisionConditions.by_size_greater_than(self.size.greater_than)
                )
            elif self.size.greater_than_or_equal is not None:
                field_conditions.append(
                    ArtifactRevisionConditions.by_size_greater_than_or_equal(
                        self.size.greater_than_or_equal
                    )
                )
            elif self.size.less_than is not None:
                field_conditions.append(
                    ArtifactRevisionConditions.by_size_less_than(self.size.less_than)
                )
            elif self.size.less_than_or_equal is not None:
                field_conditions.append(
                    ArtifactRevisionConditions.by_size_less_than_or_equal(
                        self.size.less_than_or_equal
                    )
                )

        # Handle AND logical operator - these are implicitly ANDed with field conditions
        if self.AND:
            for sub_filter in self.AND:
                field_conditions.extend(sub_filter.build_conditions())

        # Handle OR logical operator
        if self.OR:
            or_sub_conditions: list[QueryCondition] = []
            for sub_filter in self.OR:
                or_sub_conditions.extend(sub_filter.build_conditions())
            if or_sub_conditions:
                field_conditions.append(combine_conditions_or(or_sub_conditions))

        # Handle NOT logical operator
        if self.NOT:
            not_sub_conditions: list[QueryCondition] = []
            for sub_filter in self.NOT:
                not_sub_conditions.extend(sub_filter.build_conditions())
            if not_sub_conditions:
                field_conditions.append(negate_conditions(not_sub_conditions))

        return field_conditions


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Specifies the field and direction for ordering artifact revisions in queries.
    """)
)
class ArtifactRevisionOrderBy(GQLOrderBy):
    field: ArtifactRevisionOrderField
    direction: OrderDirection = OrderDirection.ASC

    def to_query_order(self) -> QueryOrder:
        """Convert to repository QueryOrder."""
        ascending = self.direction == OrderDirection.ASC
        match self.field:
            case ArtifactRevisionOrderField.VERSION:
                return ArtifactRevisionOrders.version(ascending)
            case ArtifactRevisionOrderField.STATUS:
                return ArtifactRevisionOrders.status(ascending)
            case ArtifactRevisionOrderField.SIZE:
                return ArtifactRevisionOrders.size(ascending)
            case ArtifactRevisionOrderField.CREATED_AT:
                return ArtifactRevisionOrders.created_at(ascending)
            case ArtifactRevisionOrderField.UPDATED_AT:
                return ArtifactRevisionOrders.updated_at(ascending)


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Input for scanning artifacts from external registries (HuggingFace, Reservoir).

    Discovers available artifacts and registers their metadata in the system.
    Artifacts remain in SCANNED status until explicitly imported via import_artifacts.
    """)
)
class ScanArtifactsInput:
    registry_id: ID | None = None
    limit: int = strawberry.field(
        description=f"Maximum number of artifacts to scan (max: {ARTIFACT_MAX_SCAN_LIMIT})"
    )
    artifact_type: ArtifactType | None = None
    search: str | None = None


@strawberry.input(
    description=dedent_strip("""
    Added in 26.1.0.

    Options for importing artifact revisions.

    Controls import behavior such as forcing re-download regardless of digest freshness.
    """)
)
class ImportArtifactsOptionsGQL:
    force: bool = strawberry.field(
        default=False,
        description="Force re-download regardless of digest freshness check.",
    )


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Input for importing scanned artifact revisions from external registries.

    Downloads artifact files from the external source and transitions them through:
    SCANNED → PULLING → PULLED → AVAILABLE status progression.
    """)
)
class ImportArtifactsInput:
    artifact_revision_ids: list[ID]
    vfolder_id: ID | None = strawberry.field(
        default=None,
        description="Target vfolder ID to store the imported artifacts. Added in 26.1.0.",
    )
    options: ImportArtifactsOptionsGQL | None = strawberry.field(
        default=None,
        description="Options controlling import behavior such as forcing re-download. Added in 26.1.0.",
    )


@strawberry.input(description="Added in 25.15.0")
class DelegateeTarget:
    delegatee_reservoir_id: ID
    target_registry_id: ID

    def to_dataclass(self) -> DelegateeTargetData:
        return DelegateeTargetData(
            delegatee_reservoir_id=uuid.UUID(self.delegatee_reservoir_id),
            target_registry_id=uuid.UUID(self.target_registry_id),
        )


@strawberry.input(
    description=dedent_strip("""
    Added in 25.15.0.

    Input type for delegated scanning of artifacts from a delegatee reservoir registry's remote registry.
""")
)
class DelegateScanArtifactsInput:
    delegator_reservoir_id: ID | None = strawberry.field(
        default=None, description="ID of the reservoir registry to delegate the scan request to"
    )
    delegatee_target: DelegateeTarget | None = strawberry.field(
        default=None,
        description="Target delegatee reservoir registry and its remote registry to scan",
    )
    limit: int = strawberry.field(
        description=f"Maximum number of artifacts to scan (max: {ARTIFACT_MAX_SCAN_LIMIT})"
    )
    artifact_type: ArtifactType | None = strawberry.field(
        default=None, description="Filter artifacts by type (e.g., model, image, package)"
    )
    search: str | None = strawberry.field(
        default=None, description="Search term to filter artifacts by name or description"
    )


@strawberry.input(
    description=dedent_strip("""
    Added in 25.15.0.

    Input type for delegated import of artifact revisions from a reservoir registry's remote registry.
    Used to specify which artifact revisions should be imported from the remote registry source
    into the local reservoir registry storage.
""")
)
class DelegateImportArtifactsInput:
    artifact_revision_ids: list[ID] = strawberry.field(
        description="List of artifact revision IDs of delegatee artifact registry"
    )
    delegator_reservoir_id: ID | None = strawberry.field(
        default=None, description="ID of the reservoir registry to delegate the import request to"
    )
    artifact_type: ArtifactType | None = strawberry.field(default=None)
    delegatee_target: DelegateeTarget | None = strawberry.field(default=None)
    options: ImportArtifactsOptionsGQL | None = strawberry.field(
        default=None,
        description="Options controlling import behavior such as forcing re-download. Added in 26.1.0.",
    )


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Input for updating artifact metadata properties.

    Modifies artifact metadata such as readonly status and description.
    This operation does not affect the actual artifact files or revisions.
    """)
)
class UpdateArtifactInput:
    artifact_id: ID
    readonly: bool | None = UNSET
    description: str | None = UNSET


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Input for canceling an in-progress artifact import operation.

    Stops the download process and reverts the artifact revision status back to SCANNED.
    """)
)
class CancelArtifactInput:
    artifact_revision_id: ID


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Input for cleaning up stored artifact revision data.

    Removes downloaded files from storage and transitions the artifact revision
    back to SCANNED status, freeing up storage space.
    """)
)
class CleanupArtifactRevisionsInput:
    artifact_revision_ids: list[ID]


@strawberry.input(
    description=dedent_strip("""
    Added in 25.15.0.

    Input for soft-deleting artifacts from the system.

    Marks artifacts as deleted without permanently removing them.
    Deleted artifacts can be restored using restore_artifacts.
    """)
)
class DeleteArtifactsInput:
    artifact_ids: list[ID]


@strawberry.input(
    description=dedent_strip("""
    Added in 25.15.0.

    Input for restoring previously deleted artifacts.

    Reverses the soft-delete operation, making the artifacts available again.
    """)
)
class RestoreArtifactsInput:
    artifact_ids: list[ID]


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Input for approving an artifact revision.

    Admin-only operation to approve artifact revisions for general use.
    """)
)
class ApproveArtifactInput:
    artifact_revision_id: ID


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Input for rejecting an artifact revision.

    Admin-only operation to reject artifact revisions, preventing their use.
    """)
)
class RejectArtifactInput:
    artifact_revision_id: ID


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Input for subscribing to artifact status change notifications.

    Used with artifact_status_changed subscription to receive real-time updates
    when artifact revision statuses change.
    """)
)
class ArtifactStatusChangedInput:
    artifact_revision_ids: list[ID]


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Specifies a target model for scanning operations.

    Used to identify specific models in external registries for detailed scanning.
    If revision is not specified, defaults to 'main' revision.
    """)
)
class ModelTarget:
    model_id: str
    revision: str | None = None

    def to_dataclass(self) -> ModelTargetData:
        return ModelTargetData(model_id=self.model_id, revision=self.revision)


@strawberry.input(
    description=dedent_strip("""
    Added in 25.14.0.

    Input for batch scanning of specific models from external registries.

    Scans multiple specified models and retrieves detailed information including
    README content and file sizes. This operation performs immediate detailed scanning
    unlike the general scan_artifacts which only retrieves basic metadata.
    """)
)
class ScanArtifactModelsInput:
    models: list[ModelTarget]
    registry_id: uuid.UUID | None = None


# Object Types
@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Information about the source or registry of an artifact.

    Contains the name and URL of the registry where the artifact is stored or originates from.
    """)
)
class SourceInfo:
    name: str | None
    url: str | None


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    An artifact represents AI models, packages, images, or other resources that can be
    stored, managed, and used within Backend.AI.

    Artifacts are discovered through scanning external registries and,
    can have multiple revisions.

    Each artifact contains metadata and references to its source registry.

    Key concepts:
    - Type: MODEL, PACKAGE, or IMAGE
    - Availability: ALIVE (available), DELETED (soft-deleted)
    - Source: Original external registry where it was discovered
    """)
)
class Artifact(Node):
    id: NodeID[str]
    name: str
    type: ArtifactType
    description: str | None
    registry: SourceInfo
    source: SourceInfo
    readonly: bool
    extra: JSON | None
    scanned_at: datetime
    updated_at: datetime
    availability: ArtifactAvailability

    @classmethod
    def from_dataclass(cls, data: ArtifactData, registry_url: str, source_url: str) -> Self:
        return cls(
            id=ID(str(data.id)),
            name=data.name,
            type=ArtifactType(data.type),
            description=data.description,
            registry=SourceInfo(name=data.registry_type.value, url=registry_url),
            source=SourceInfo(name=data.source_registry_type.value, url=source_url),
            readonly=data.readonly,
            extra=data.extra,
            scanned_at=data.scanned_at,
            updated_at=data.updated_at,
            availability=data.availability,
        )

    @strawberry.field
    async def revisions(
        self,
        info: Info[StrawberryGQLContext],
        filter: ArtifactRevisionFilter | None = None,
        order_by: list[ArtifactRevisionOrderBy] | None = None,
        before: str | None = None,
        after: str | None = None,
        first: int | None = None,
        last: int | None = None,
        limit: int | None = None,
        offset: int | None = None,
    ) -> ArtifactRevisionConnection:
        from .fetcher import fetch_artifact_revisions

        base_conditions = [ArtifactRevisionConditions.by_artifact_id(uuid.UUID(self.id))]

        return await fetch_artifact_revisions(
            info,
            filter=filter,
            order_by=order_by,
            before=before,
            after=after,
            first=first,
            last=last,
            limit=limit,
            offset=offset,
            base_conditions=base_conditions,
        )


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    A specific version/revision of an artifact containing the actual file data.

    Artifact revisions progress through different statuses:
    - SCANNED: Discovered in external registry, metadata only
    - PULLING: Currently downloading from external source
    - PULLED: Downloaded to temporary storage
    - AVAILABLE: Ready for use by users

    Contains version information, file size, README content, and timestamps.
    Most HuggingFace models only have a 'main' revision.
    """)
)
class ArtifactRevision(Node):
    id: NodeID[str]
    status: ArtifactStatus
    remote_status: ArtifactRemoteStatus | None = strawberry.field(description="Added in 25.15.0")
    version: str
    readme: str | None
    size: ByteSize | None
    created_at: datetime | None
    updated_at: datetime | None
    digest: str | None = strawberry.field(
        description="Digest at the time of import. None for models that have not been imported. Added in 25.17.0"
    )
    verification_result: ArtifactVerificationGQLResult | None = strawberry.field(
        description="Verification result containing malware scan results from all verifiers. None if not yet verified. Added in 25.17.0"
    )

    @classmethod
    async def resolve_nodes(  # type: ignore[override]  # Strawberry Node uses AwaitableOrValue overloads incompatible with async def
        cls,
        *,
        info: Info[StrawberryGQLContext],
        node_ids: Iterable[str],
        required: bool = False,
    ) -> Iterable[Self | None]:
        results = await info.context.data_loaders.artifact_revision_loader.load_many([
            uuid.UUID(nid) for nid in node_ids
        ])
        return [cls.from_dataclass(data) if data is not None else None for data in results]

    @classmethod
    def from_dataclass(cls, data: ArtifactRevisionData) -> Self:
        return cls(
            id=ID(str(data.id)),
            status=ArtifactStatus(data.status),
            remote_status=ArtifactRemoteStatus(data.remote_status) if data.remote_status else None,
            readme=data.readme,
            version=data.version,
            size=ByteSize(data.size) if data.size is not None else None,
            created_at=data.created_at,
            updated_at=data.updated_at,
            digest=data.digest,
            verification_result=ArtifactVerificationGQLResult.from_dataclass(
                data.verification_result
            )
            if data.verification_result
            else None,
        )

    @strawberry.field
    async def artifact(self, info: Info[StrawberryGQLContext]) -> Artifact:
        revision_action_result = (
            await info.context.processors.artifact_revision.get.wait_for_complete(
                GetArtifactRevisionAction(artifact_revision_id=uuid.UUID(self.id))
            )
        )

        artifact_id = revision_action_result.revision.artifact_id

        artifact_action_result = await info.context.processors.artifact.get.wait_for_complete(
            GetArtifactAction(artifact_id=artifact_id)
        )

        registry_meta_loader = DataLoader(
            apartial(ArtifactRegistryMeta.load_by_id, info.context),
        )

        registry_data = await registry_meta_loader.load(artifact_action_result.result.registry_id)
        source_registry_data = await registry_meta_loader.load(
            artifact_action_result.result.source_registry_id
        )

        return Artifact.from_dataclass(
            artifact_action_result.result, registry_data.url, source_registry_data.url
        )


ArtifactEdge = Edge[Artifact]
ArtifactRevisionEdge = Edge[ArtifactRevision]


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Paginated connection for artifacts with total count information.

    Used for relay-style pagination with cursor-based navigation.
    """)
)
class ArtifactConnection(Connection[Artifact]):
    count: int

    def __init__(self, *args: Any, count: int, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self.count = count


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Paginated connection for artifact revisions with total count information.

    Used for relay-style pagination with cursor-based navigation.
    """)
)
class ArtifactRevisionConnection(Connection[ArtifactRevision]):
    count: int

    def __init__(self, *args: Any, count: int, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self.count = count


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Payload for artifact import progress subscription events.

    Provides real-time updates during the artifact import process,
    including progress percentage and current status.
    """)
)
class ArtifactImportProgressUpdatedPayload:
    artifact_id: ID
    progress: float
    status: ArtifactStatus


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Response payload for artifact scanning operations.

    Contains the list of artifacts discovered during scanning of external registries.
    These artifacts are registered with SCANNED status and can be imported for actual use.
    """)
)
class ScanArtifactsPayload:
    artifacts: list[Artifact]


@strawberry.type(
    description=dedent_strip("""
    Added in 25.15.0.

    Response payload for delegated artifact scanning operation.
    Contains the list of artifacts discovered during the scan of a reservoir registry's remote registry.
    These artifacts are now available for import or direct use.
""")
)
class DelegateScanArtifactsPayload:
    artifacts: list[Artifact] = strawberry.field(
        description="List of artifacts discovered during the delegated scan from the reservoir registry's remote registry"
    )


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Represents a background task for importing an artifact revision.

    Contains the task ID for monitoring progress and the associated artifact revision
    being imported from external registries.
    """)
)
class ArtifactRevisionImportTask:
    task_id: ID | None
    artifact_revision: ArtifactRevision


# Mutation Payloads
@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Response payload for artifact import operations.

    Contains the imported artifact revisions and their associated background tasks.
    Tasks can be monitored to track the import progress from SCANNED to AVAILABLE status.
    """)
)
class ImportArtifactsPayload:
    artifact_revisions: ArtifactRevisionConnection
    tasks: list[ArtifactRevisionImportTask]


@strawberry.type(
    description=dedent_strip("""
    Added in 25.15.0.

    Response payload for delegated artifact import operation.
    Contains the imported artifact revisions and associated background tasks.
    The tasks can be monitored to track the progress of the import operation.
""")
)
class DelegateImportArtifactsPayload:
    artifact_revisions: ArtifactRevisionConnection = strawberry.field(
        description="Connection of artifact revisions that were imported from the reservoir registry's remote registry"
    )
    tasks: list[ArtifactRevisionImportTask] = strawberry.field(
        description="List of background tasks created for importing the artifact revisions"
    )


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Response payload for artifact update operations.

    Returns the updated artifact with modified metadata properties.
    """)
)
class UpdateArtifactPayload:
    artifact: Artifact


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Response payload for artifact revision cleanup operations.

    Contains the cleaned artifact revisions that have had their stored data removed,
    transitioning them back to SCANNED status to free storage space.
    """)
)
class CleanupArtifactRevisionsPayload:
    artifact_revisions: ArtifactRevisionConnection


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Response payload for artifact revision approval operations.

    Contains the approved artifact revision. Admin-only operation.
    """)
)
class ApproveArtifactPayload:
    artifact_revision: ArtifactRevision


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Response payload for artifact revision rejection operations.

    Contains the rejected artifact revision. Admin-only operation.
    """)
)
class RejectArtifactPayload:
    artifact_revision: ArtifactRevision


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Response payload for canceling artifact import operations.

    Contains the artifact revision whose import was canceled,
    reverting its status back to SCANNED.
    """)
)
class CancelImportArtifactPayload:
    artifact_revision: ArtifactRevision


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Payload for artifact status change subscription events.

    Provides real-time notifications when artifact revision statuses change
    during import, cleanup, or other operations.
    """)
)
class ArtifactStatusChangedPayload:
    artifact_revision: ArtifactRevision


@strawberry.type(
    description=dedent_strip("""
    Added in 25.14.0.

    Response payload for batch model scanning operations.

    Contains the artifact revisions discovered during detailed scanning of specific models,
    including README content and file size information.
    """)
)
class ScanArtifactModelsPayload:
    artifact_revision: ArtifactRevisionConnection


@strawberry.type(
    description=dedent_strip("""
    Added in 25.15.0.

    Response payload for artifact deletion operations.

    Contains the artifacts that were soft-deleted. These can be restored later.
    """)
)
class DeleteArtifactsPayload:
    artifacts: list[Artifact]


@strawberry.type(
    description=dedent_strip("""
    Added in 25.15.0.

    Response payload for artifact restoration operations.

    Contains the artifacts that were restored from soft-deleted state.
    """)
)
class RestoreArtifactsPayload:
    artifacts: list[Artifact]
