"""
Data models for incremental execution.

Implements FR-7: Incremental Recomputation
Implements FR-8: Impact Analysis

Provides ChangeType enum, ArtifactChange, ImpactDebt, RecomputeConfig,
and RecomputeResult dataclasses.
"""

import uuid
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional


class ChangeType(Enum):
    """Type of artifact change detected."""
    CREATED = "created"
    MODIFIED = "modified"
    DELETED = "deleted"


@dataclass
class ArtifactChange:
    """
    Record of a detected artifact change.

    Attributes:
        id: Unique change identifier
        artifact_id: ID of changed artifact
        old_digest: Previous content digest (None for created)
        new_digest: New content digest
        change_type: Type of change
        detected_at: When change was detected
    """
    id: str
    artifact_id: str
    old_digest: Optional[str]
    new_digest: str
    change_type: ChangeType
    detected_at: datetime = field(default_factory=datetime.utcnow)

    @classmethod
    def create(
        cls,
        artifact_id: str,
        old_digest: Optional[str],
        new_digest: str,
        change_type: ChangeType,
    ) -> "ArtifactChange":
        """
        Create a new ArtifactChange record.

        Args:
            artifact_id: ID of changed artifact
            old_digest: Previous digest (None for created)
            new_digest: New digest
            change_type: Type of change

        Returns:
            New ArtifactChange instance
        """
        return cls(
            id=str(uuid.uuid4()),
            artifact_id=artifact_id,
            old_digest=old_digest,
            new_digest=new_digest,
            change_type=change_type,
        )

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "id": self.id,
            "artifact_id": self.artifact_id,
            "old_digest": self.old_digest,
            "new_digest": self.new_digest,
            "change_type": self.change_type.value,
            "detected_at": self.detected_at.isoformat(),
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "ArtifactChange":
        """Create from dictionary."""
        return cls(
            id=data["id"],
            artifact_id=data["artifact_id"],
            old_digest=data.get("old_digest"),
            new_digest=data["new_digest"],
            change_type=ChangeType(data["change_type"]),
            detected_at=datetime.fromisoformat(data["detected_at"]),
        )


@dataclass
class ImpactDebt:
    """
    Record of a suppressed recomputation.

    When a dependent is not recomputed (due to circular dependency,
    below-threshold impact, or budget exhaustion), an ImpactDebt
    record is created to track the outstanding work.

    Attributes:
        id: Unique debt identifier
        artifact_id: ID of the changed artifact that triggered this
        dependent_run_id: Run ID of the dependent that was not recomputed
        change_magnitude: Measured impact magnitude (0.0-1.0)
        suppression_reason: Why recomputation was suppressed
        recorded_at: When debt was recorded
    """
    id: str
    artifact_id: str
    dependent_run_id: str
    change_magnitude: float
    suppression_reason: str
    recorded_at: datetime = field(default_factory=datetime.utcnow)

    @classmethod
    def create(
        cls,
        artifact_id: str,
        dependent_run_id: str,
        change_magnitude: float,
        suppression_reason: str,
    ) -> "ImpactDebt":
        """
        Create a new ImpactDebt record.

        Args:
            artifact_id: Changed artifact ID
            dependent_run_id: Dependent run ID
            change_magnitude: Impact magnitude (0.0-1.0)
            suppression_reason: Reason for suppression

        Returns:
            New ImpactDebt instance
        """
        return cls(
            id=str(uuid.uuid4()),
            artifact_id=artifact_id,
            dependent_run_id=dependent_run_id,
            change_magnitude=change_magnitude,
            suppression_reason=suppression_reason,
        )

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "id": self.id,
            "artifact_id": self.artifact_id,
            "dependent_run_id": self.dependent_run_id,
            "change_magnitude": self.change_magnitude,
            "suppression_reason": self.suppression_reason,
            "recorded_at": self.recorded_at.isoformat(),
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "ImpactDebt":
        """Create from dictionary."""
        return cls(
            id=data["id"],
            artifact_id=data["artifact_id"],
            dependent_run_id=data["dependent_run_id"],
            change_magnitude=data["change_magnitude"],
            suppression_reason=data["suppression_reason"],
            recorded_at=datetime.fromisoformat(data["recorded_at"]),
        )


@dataclass
class RecomputeConfig:
    """
    Configuration for incremental recomputation.

    Attributes:
        max_depth: Maximum BFS depth for finding dependents (FR-7.2)
        impact_threshold: Minimum magnitude to trigger recompute (FR-8.2)
        max_recomputes: Budget limit on recomputations per change
        suppress_circular: Whether to suppress circular dependencies
    """
    max_depth: int = 1
    impact_threshold: float = 0.0
    max_recomputes: int = 100
    suppress_circular: bool = True

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "max_depth": self.max_depth,
            "impact_threshold": self.impact_threshold,
            "max_recomputes": self.max_recomputes,
            "suppress_circular": self.suppress_circular,
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "RecomputeConfig":
        """Create from dictionary."""
        return cls(
            max_depth=data.get("max_depth", 1),
            impact_threshold=data.get("impact_threshold", 0.0),
            max_recomputes=data.get("max_recomputes", 100),
            suppress_circular=data.get("suppress_circular", True),
        )


@dataclass
class RecomputeResult:
    """
    Result of an incremental recomputation triggered by a change.

    Attributes:
        changed_artifact_id: ID of the artifact that changed
        change: The detected change record
        executed_run_ids: Run IDs that were recomputed
        suppressed: List of suppressed recomputation debt records
        total_dependents: Total number of dependents found
        recomputed_count: Number actually recomputed
        suppressed_count: Number suppressed
    """
    changed_artifact_id: str
    change: ArtifactChange
    executed_run_ids: List[str] = field(default_factory=list)
    suppressed: List[ImpactDebt] = field(default_factory=list)
    total_dependents: int = 0
    recomputed_count: int = 0
    suppressed_count: int = 0

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "changed_artifact_id": self.changed_artifact_id,
            "change": self.change.to_dict(),
            "executed_run_ids": self.executed_run_ids,
            "suppressed": [d.to_dict() for d in self.suppressed],
            "total_dependents": self.total_dependents,
            "recomputed_count": self.recomputed_count,
            "suppressed_count": self.suppressed_count,
        }
