"""
Bundle models for releaseops.

A bundle is an immutable, content-addressed composition of artifacts
(prompts + policies + model config) that defines AI behavior at a point in time.
"""

from __future__ import annotations

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

from llmhq_releaseops.models.artifact import ArtifactRef, ArtifactType


@dataclass(frozen=True)
class ModelConfig:
    """LLM model configuration included in a bundle."""

    provider: str  # "anthropic", "openai", etc.
    model: str  # "claude-sonnet-4-5-20250929", "gpt-4-turbo", etc.
    temperature: float = 0.7
    max_tokens: int = 4096
    parameters: Dict[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        if not 0.0 <= self.temperature <= 2.0:
            raise ValueError(f"temperature must be between 0.0 and 2.0, got {self.temperature}")
        if self.max_tokens < 1:
            raise ValueError(f"max_tokens must be positive, got {self.max_tokens}")

    def to_dict(self) -> dict:
        d = {
            "provider": self.provider,
            "model": self.model,
            "temperature": self.temperature,
            "max_tokens": self.max_tokens,
        }
        if self.parameters:
            d["parameters"] = dict(self.parameters)
        return d

    @classmethod
    def from_dict(cls, data: dict) -> ModelConfig:
        for key in ("provider", "model"):
            if key not in data:
                raise ValueError(f"ModelConfig missing required field: '{key}'")
        return cls(
            provider=data["provider"],
            model=data["model"],
            temperature=data.get("temperature", 0.7),
            max_tokens=data.get("max_tokens", 4096),
            parameters=data.get("parameters", {}),
        )


@dataclass(frozen=True)
class BundleMetadata:
    """Metadata for a bundle manifest."""

    id: str  # e.g., "support-agent"
    version: str  # e.g., "2.1.0"
    created_at: str  # ISO 8601 timestamp
    created_by: str  # e.g., "jision"
    description: str = ""

    def __post_init__(self) -> None:
        if not self.id:
            raise ValueError("Bundle id cannot be empty")
        if not self.version:
            raise ValueError("Bundle version cannot be empty")

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "version": self.version,
            "created_at": self.created_at,
            "created_by": self.created_by,
            "description": self.description,
        }

    @classmethod
    def from_dict(cls, data: dict) -> BundleMetadata:
        for key in ("id", "version"):
            if key not in data:
                raise ValueError(f"BundleMetadata missing required field: '{key}'")
        return cls(
            id=data["id"],
            version=data["version"],
            created_at=data.get("created_at", ""),
            created_by=data.get("created_by", ""),
            description=data.get("description", ""),
        )


CURRENT_SCHEMA_VERSION = 2


@dataclass(frozen=True)
class BundleManifest:
    """
    Immutable bundle manifest — the core abstraction of releaseops.

    Composes versioned prompts, policies, and model config into a single
    content-addressed artifact. Stored as YAML in .releaseops/bundles/{id}/{version}.yaml.
    """

    metadata: BundleMetadata
    prompts: Dict[str, ArtifactRef] = field(default_factory=dict)  # role -> ArtifactRef
    policies: Dict[str, ArtifactRef] = field(default_factory=dict)  # role -> ArtifactRef
    model_config: Optional[ModelConfig] = None
    bundle_hash: Optional[str] = None  # "sha256:..." computed over all content
    schema_version: int = CURRENT_SCHEMA_VERSION

    @property
    def id(self) -> str:
        return self.metadata.id

    @property
    def version(self) -> str:
        return self.metadata.version

    @property
    def all_artifacts(self) -> List[ArtifactRef]:
        """All artifact references in this bundle."""
        return list(self.prompts.values()) + list(self.policies.values())

    def to_dict(self) -> dict:
        """Serialize to dictionary for YAML output."""
        d: dict = {
            "schema_version": self.schema_version,
            "metadata": self.metadata.to_dict(),
            "artifacts": {
                "prompts": {
                    role: ref.to_dict() for role, ref in self.prompts.items()
                },
                "policies": {
                    role: ref.to_dict() for role, ref in self.policies.items()
                },
            },
        }
        if self.model_config:
            d["model_config"] = self.model_config.to_dict()
        if self.bundle_hash:
            d["bundle_hash"] = self.bundle_hash
        return d

    @classmethod
    def from_dict(cls, data: dict) -> BundleManifest:
        """Deserialize from dictionary (loaded from YAML)."""
        import logging

        if "metadata" not in data:
            raise ValueError("BundleManifest missing required field: 'metadata'")

        schema_version = data.get("schema_version", 1)
        if schema_version > CURRENT_SCHEMA_VERSION:
            logging.getLogger(__name__).warning(
                "Bundle has schema_version %d but this code understands up to %d. "
                "Some fields may not be interpreted correctly.",
                schema_version,
                CURRENT_SCHEMA_VERSION,
            )

        metadata = BundleMetadata.from_dict(data["metadata"])

        prompts: Dict[str, ArtifactRef] = {}
        policies: Dict[str, ArtifactRef] = {}

        artifacts = data.get("artifacts", {})
        for role, ref_data in artifacts.get("prompts", {}).items():
            prompts[role] = ArtifactRef.from_dict(role, ArtifactType.PROMPT, ref_data)

        policy_type_map = {
            "context": ArtifactType.POLICY_CONTEXT,
            "tools": ArtifactType.POLICY_TOOLS,
            "safety": ArtifactType.POLICY_SAFETY,
        }
        for role, ref_data in artifacts.get("policies", {}).items():
            art_type = policy_type_map.get(role, ArtifactType.POLICY_CONTEXT)
            policies[role] = ArtifactRef.from_dict(role, art_type, ref_data)

        model_config = None
        if "model_config" in data:
            model_config = ModelConfig.from_dict(data["model_config"])

        return cls(
            metadata=metadata,
            prompts=prompts,
            policies=policies,
            model_config=model_config,
            bundle_hash=data.get("bundle_hash"),
            schema_version=schema_version,
        )
