"""
Bundle creation and verification for releaseops.

Creates immutable, content-addressed bundles from artifacts (prompts + policies + model config).
"""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from llmhq_releaseops.core.hasher import hash_bundle, hash_content, hash_dict, verify_hash
from llmhq_releaseops.models.artifact import ArtifactRef, ArtifactType
from llmhq_releaseops.models.bundle import BundleManifest, BundleMetadata, ModelConfig
from llmhq_releaseops.storage.git_store import GitStore

logger = logging.getLogger(__name__)


class Bundler:
    """
    Creates and verifies immutable bundles.

    A bundle composes versioned prompts (from promptops), policies, and
    model config into a single content-addressed artifact.
    """

    def __init__(self, store: GitStore):
        self.store = store

    def create(
        self,
        bundle_id: str,
        version: str,
        prompts: Optional[Dict[str, ArtifactRef]] = None,
        policies: Optional[Dict[str, ArtifactRef]] = None,
        model_config: Optional[ModelConfig] = None,
        description: str = "",
        created_by: str = "",
    ) -> BundleManifest:
        """
        Create a new bundle manifest and save it.

        Args:
            bundle_id: Unique bundle identifier (e.g., "support-agent")
            version: Semantic version (e.g., "1.0.0")
            prompts: Map of role -> ArtifactRef for prompts
            policies: Map of role -> ArtifactRef for policies
            model_config: LLM model configuration
            description: Human-readable description
            created_by: Who created this bundle

        Returns:
            The created BundleManifest with computed bundle_hash.
        """
        prompts = prompts or {}
        policies = policies or {}

        # Resolve policy content hashes if not already set
        for role, ref in policies.items():
            if ref.content_hash is None and ref.path:
                content = self.store.read_policy(ref.path)
                content_hash = hash_content(content)
                # Replace with hash-populated ref (frozen dataclass)
                policies[role] = ArtifactRef(
                    artifact_type=ref.artifact_type,
                    role=ref.role,
                    path=ref.path,
                    content_hash=content_hash,
                )

        # Compute bundle hash from all artifact hashes
        all_hashes = []
        for ref in list(prompts.values()) + list(policies.values()):
            if ref.content_hash:
                all_hashes.append(ref.content_hash)

        model_hash = None
        if model_config:
            model_hash = hash_dict(model_config.to_dict())

        bundle_hash = hash_bundle(all_hashes, model_hash)

        metadata = BundleMetadata(
            id=bundle_id,
            version=version,
            created_at=datetime.now(timezone.utc).isoformat(),
            created_by=created_by,
            description=description,
        )

        manifest = BundleManifest(
            metadata=metadata,
            prompts=prompts,
            policies=policies,
            model_config=model_config,
            bundle_hash=bundle_hash,
        )

        # Persist to disk
        self.store.save_bundle(manifest)
        logger.info(f"Created bundle {bundle_id} v{version} [{bundle_hash[:20]}...]")
        return manifest

    def verify(self, bundle_id: str, version: str) -> List[str]:
        """
        Verify bundle integrity by recomputing all content hashes.

        Returns:
            List of verification errors (empty if all OK).
        """
        manifest = self.store.load_bundle(bundle_id, version)
        errors: List[str] = []

        # Verify policy content hashes
        for role, ref in manifest.policies.items():
            if ref.content_hash and ref.path:
                try:
                    content = self.store.read_policy(ref.path)
                    if not verify_hash(content, ref.content_hash):
                        errors.append(
                            f"Policy '{role}' ({ref.path}): content hash mismatch"
                        )
                except FileNotFoundError:
                    errors.append(f"Policy '{role}' ({ref.path}): file not found")

        # Verify bundle hash
        all_hashes = []
        for ref in manifest.all_artifacts:
            if ref.content_hash:
                all_hashes.append(ref.content_hash)

        model_hash = None
        if manifest.model_config:
            model_hash = hash_dict(manifest.model_config.to_dict())

        expected_hash = hash_bundle(all_hashes, model_hash)
        if manifest.bundle_hash and manifest.bundle_hash != expected_hash:
            errors.append(
                f"Bundle hash mismatch: expected {expected_hash}, "
                f"got {manifest.bundle_hash}"
            )

        if errors:
            logger.warning(f"Bundle {bundle_id} v{version} failed verification: {errors}")
        else:
            logger.info(f"Bundle {bundle_id} v{version} verified OK")

        return errors

    def diff(self, bundle_id: str, version1: str, version2: str) -> Dict[str, Any]:
        """
        Compare two versions of a bundle.

        Returns dict with keys: added, removed, changed, unchanged.
        """
        b1 = self.store.load_bundle(bundle_id, version1)
        b2 = self.store.load_bundle(bundle_id, version2)

        result: Dict[str, Any] = {
            "bundle_id": bundle_id,
            "version1": version1,
            "version2": version2,
            "prompts": self._diff_artifacts(b1.prompts, b2.prompts),
            "policies": self._diff_artifacts(b1.policies, b2.policies),
            "model_config_changed": (
                b1.model_config != b2.model_config
                if b1.model_config and b2.model_config
                else b1.model_config is not b2.model_config
            ),
            "bundle_hash_changed": b1.bundle_hash != b2.bundle_hash,
        }
        return result

    def _diff_artifacts(
        self, a: Dict[str, ArtifactRef], b: Dict[str, ArtifactRef]
    ) -> Dict[str, List[str]]:
        """Compare two artifact dictionaries."""
        keys_a = set(a.keys())
        keys_b = set(b.keys())

        added = sorted(keys_b - keys_a)
        removed = sorted(keys_a - keys_b)
        common = keys_a & keys_b

        changed = []
        unchanged = []
        for key in sorted(common):
            if a[key].content_hash != b[key].content_hash:
                changed.append(key)
            else:
                unchanged.append(key)

        return {
            "added": added,
            "removed": removed,
            "changed": changed,
            "unchanged": unchanged,
        }

    def list_bundles(self) -> List[str]:
        """List all bundle IDs."""
        return self.store.list_bundles()

    def list_versions(self, bundle_id: str) -> List[str]:
        """List all versions of a bundle."""
        return self.store.list_bundle_versions(bundle_id)

    def inspect(self, bundle_id: str, version: str) -> Dict[str, Any]:
        """Get human-readable bundle info."""
        manifest = self.store.load_bundle(bundle_id, version)
        d = manifest.to_dict()
        d["artifact_count"] = len(manifest.all_artifacts)
        d["prompt_roles"] = list(manifest.prompts.keys())
        d["policy_roles"] = list(manifest.policies.keys())
        return d
