"""Registry types and dataclasses."""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Tuple

if TYPE_CHECKING:
    pass

# ─────────────────────────────────────────────────────────────────────────────
# Constants
# ─────────────────────────────────────────────────────────────────────────────

VERSION_PENDING = "pending"  # Placeholder for version not yet assigned
ERROR_UNKNOWN = "UnknownError"  # Fallback error type when error info unavailable


# ─────────────────────────────────────────────────────────────────────────────
# Enums
# ─────────────────────────────────────────────────────────────────────────────


class OnConflict(str, Enum):
    """Behavior when a version conflict occurs during push/save operations.

    - SKIP: Don't modify existing version. Single ops raise, batch ops return skipped result.
    - OVERWRITE: Replace existing version with new data.
    """

    SKIP = "skip"
    OVERWRITE = "overwrite"


class VerifyLevel(str, Enum):
    """Verification level for load operations.

    Controls what checks are performed when loading artifacts:

    - NONE: No verification. Trust cache/download completely. Fastest option.
    - INTEGRITY: Verify downloaded files match declared hash in metadata.
        Catches corruption but not remote storage corruption.
    - FULL: Integrity check + staleness check (cache layer only).
        Ensures cached data matches current remote state.

    The staleness check (FULL vs INTEGRITY) only applies when caching is
    enabled (remote backend with ``use_cache=True``). For local backends,
    FULL behaves like INTEGRITY.
    """

    NONE = "none"
    INTEGRITY = "integrity"
    FULL = "full"


class CleanupState(str, Enum):
    """Cleanup status for best-effort artifact cleanup in remote MVCC flows."""

    OK = "ok"
    ORPHANED = "orphaned"
    UNKNOWN = "unknown"
    NOT_APPLICABLE = "n/a"

    @property
    def has_orphan(self) -> bool:
        """True when cleanup state is ORPHANED and follow-up is needed."""
        return self == CleanupState.ORPHANED

    @property
    def has_unknown(self) -> bool:
        """True when cleanup state is UNKNOWN and follow-up is needed."""
        return self == CleanupState.UNKNOWN


# ─────────────────────────────────────────────────────────────────────────────
# Backend Operation Results
# ─────────────────────────────────────────────────────────────────────────────


@dataclass
class OpResult:
    """Result of a single backend operation (push, pull, delete, fetch_metadata, delete_metadata).

    Factory methods:
        OpResult.success(name, version, metadata=None)
        OpResult.failed(name, version, exception)
        OpResult.skipped(name, version)
        OpResult.overwritten(name, version)
    """

    name: str
    version: str
    ok: bool
    status: str = "ok"  # ok, skipped, overwritten, error
    metadata: dict | None = None
    error: str | None = None
    message: str | None = None
    path: str | None = None
    cleanup: CleanupState | None = None
    exception: Exception | None = None  # Original exception for re-raising in single-item ops

    @property
    def key(self) -> Tuple[str, str]:
        """Return (name, version) tuple for dict compatibility."""
        return (self.name, self.version)

    @property
    def is_error(self) -> bool:
        return self.status == "error"

    @property
    def is_skipped(self) -> bool:
        return self.status == "skipped"

    @property
    def is_overwritten(self) -> bool:
        return self.status == "overwritten"

    @classmethod
    def success(
        cls,
        name: str,
        version: str,
        *,
        metadata: dict | None = None,
        path: str | None = None,
        cleanup: CleanupState | None = None,
        status: str = "ok",
    ) -> "OpResult":
        """Create a successful result."""
        return cls(
            name=name,
            version=version,
            ok=True,
            status=status,
            metadata=metadata,
            path=path,
            cleanup=cleanup,
        )

    @classmethod
    def failed(
        cls,
        name: str,
        version: str,
        exception: Exception | None = None,
        *,
        error_type: str | None = None,
        message: str | None = None,
        cleanup: CleanupState | None = None,
    ) -> "OpResult":
        """Create a failed result from an exception or explicit error_type/message.

        The original exception is preserved for re-raising in single-item operations
        at the Registry API surface.
        """
        if exception is not None:
            error_type = type(exception).__name__
            message = str(exception)
        return cls(
            name=name,
            version=version,
            ok=False,
            status="error",
            error=error_type,
            message=message,
            cleanup=cleanup,
            exception=exception,
        )

    @classmethod
    def skipped(cls, name: str, version: str, *, cleanup: CleanupState | None = None) -> "OpResult":
        """Create a skipped result (e.g., version already exists with on_conflict='skip')."""
        return cls(name=name, version=version, ok=True, status="skipped", cleanup=cleanup)

    @classmethod
    def overwritten(cls, name: str, version: str, *, cleanup: CleanupState | None = None) -> "OpResult":
        """Create an overwritten result."""
        return cls(name=name, version=version, ok=True, status="overwritten", cleanup=cleanup)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to legacy dict format for backwards compatibility."""
        if self.is_error:
            result: Dict[str, Any] = {"status": "error", "error": self.error, "message": self.message}
        else:
            result = {"status": self.status}
            if self.metadata is not None:
                result["metadata"] = self.metadata
            if self.path is not None:
                result["path"] = self.path
        if self.cleanup is not None:
            result["cleanup"] = self.cleanup.value
        return result


@dataclass
class OpResults:
    """Results of batch backend operations.

    Provides dict-like access by (name, version) key while offering
    convenient properties for filtering and error handling.

    Usage:
        results = backend.push(names, versions, paths, metadata)

        # Dict-like access
        if results[(name, version)].ok:
            print(results[(name, version)].metadata)

        # Iteration
        for result in results:
            if result.ok:
                process(result)

        # Error handling
        results.raise_on_errors()  # Raises if any errors

        # Filtering
        for result in results.successful:
            ...
        for result in results.failed:
            ...
    """

    _results: Dict[Tuple[str, str], OpResult] = field(default_factory=dict)

    def add(self, result: OpResult) -> None:
        """Add a result to the collection."""
        self._results[result.key] = result

    def __getitem__(self, key: Tuple[str, str]) -> OpResult:
        return self._results[key]

    def __contains__(self, key: Tuple[str, str]) -> bool:
        return key in self._results

    def __iter__(self) -> Iterator[OpResult]:
        return iter(self._results.values())

    def __len__(self) -> int:
        return len(self._results)

    def __bool__(self) -> bool:
        return len(self._results) > 0

    def get(self, key: Tuple[str, str], default: OpResult | None = None) -> OpResult | None:
        return self._results.get(key, default)

    def first(self) -> OpResult | None:
        """Return the first (or only) result. Useful for single-item operations."""
        if self._results:
            return next(iter(self._results.values()))
        return None

    def keys(self) -> Iterator[Tuple[str, str]]:
        return iter(self._results.keys())

    def values(self) -> Iterator[OpResult]:
        return iter(self._results.values())

    def items(self) -> Iterator[Tuple[Tuple[str, str], OpResult]]:
        return iter(self._results.items())

    @property
    def successful(self) -> List[OpResult]:
        """Results where ok=True (includes skipped/overwritten)."""
        return [r for r in self._results.values() if r.ok]

    @property
    def failed(self) -> List[OpResult]:
        """Results where ok=False."""
        return [r for r in self._results.values() if not r.ok]

    @property
    def errors(self) -> List[OpResult]:
        """Results with status='error'."""
        return [r for r in self._results.values() if r.is_error]

    @property
    def all_ok(self) -> bool:
        """True if all operations succeeded."""
        return all(r.ok for r in self._results.values())

    @property
    def any_failed(self) -> bool:
        """True if any operation failed."""
        return any(not r.ok for r in self._results.values())

    def raise_on_errors(self, message_prefix: str = "Operation failed") -> None:
        """Raise RuntimeError if any results have errors."""
        errors = self.errors
        if errors:
            error_msgs = [f"{r.name}@{r.version}: {r.error} - {r.message}" for r in errors]
            raise RuntimeError(f"{message_prefix}: {'; '.join(error_msgs)}")

    def to_dict(self) -> Dict[Tuple[str, str], Dict[str, Any]]:
        """Convert to legacy dict format for backwards compatibility."""
        return {key: result.to_dict() for key, result in self._results.items()}


# ─────────────────────────────────────────────────────────────────────────────
# Registry-Level Results (for save/load batch operations)
# ─────────────────────────────────────────────────────────────────────────────


@dataclass
class BatchResult:
    """Result of a batch operation.

    Attributes:
        results: List of results in order. None for failed/skipped items.
        errors: Dict mapping (name, version) to error info for failed items.
        succeeded: List of (name, version) tuples that succeeded (ok or overwritten).
        skipped: List of (name, version) tuples that were skipped (on_conflict="skip").
        failed: List of (name, version) tuples that failed (actual errors).
        cleanup_needed: Dict mapping (name, version) to cleanup state for
            items that require follow-up cleanup.
    """

    results: List[Any] = field(default_factory=list)
    errors: Dict[Tuple[str, str], Dict[str, str]] = field(default_factory=dict)
    succeeded: List[Tuple[str, str]] = field(default_factory=list)
    skipped: List[Tuple[str, str]] = field(default_factory=list)
    failed: List[Tuple[str, str]] = field(default_factory=list)
    cleanup_needed: Dict[Tuple[str, str], CleanupState] = field(default_factory=dict)

    def __len__(self) -> int:
        return len(self.results)

    def __getitem__(self, index: int) -> Any:
        return self.results[index]

    def __iter__(self):
        return iter(self.results)

    @property
    def all_succeeded(self) -> bool:
        """Returns True if all items succeeded (no failures, skipped is ok)."""
        return len(self.failed) == 0

    @property
    def success_count(self) -> int:
        """Number of successful items."""
        return len(self.succeeded)

    @property
    def skipped_count(self) -> int:
        """Number of skipped items."""
        return len(self.skipped)

    @property
    def failure_count(self) -> int:
        """Number of failed items."""
        return len(self.failed)

    @property
    def cleanup_required_count(self) -> int:
        """Number of items that require follow-up cleanup."""
        return len(self.cleanup_needed)
