"""Event Sourced Repository implementation.

Stores providers by persisting their domain events and rebuilding state on load.
"""

import threading
from typing import Any, Dict, List, Optional

from ..domain.events import DomainEvent
from ..domain.model.event_sourced_provider import EventSourcedProvider, ProviderSnapshot
from ..domain.repository import IProviderRepository, ProviderLike
from ..logging_config import get_logger
from .event_bus import EventBus, get_event_bus
from .event_store import EventStore, EventStoreSnapshot, get_event_store, StoredEvent

logger = get_logger(__name__)


class ProviderConfigStore:
    """Stores provider configuration (command, image, env, etc.)"""

    def __init__(self):
        self._configs: Dict[str, Dict[str, Any]] = {}
        self._lock = threading.RLock()

    def save(self, provider_id: str, config: Dict[str, Any]) -> None:
        """Save provider configuration."""
        with self._lock:
            self._configs[provider_id] = dict(config)

    def load(self, provider_id: str) -> Optional[Dict[str, Any]]:
        """Load provider configuration."""
        with self._lock:
            if provider_id in self._configs:
                return dict(self._configs[provider_id])
            return None

    def remove(self, provider_id: str) -> bool:
        """Remove provider configuration."""
        with self._lock:
            if provider_id in self._configs:
                del self._configs[provider_id]
                return True
            return False

    def get_all_ids(self) -> List[str]:
        """Get all provider IDs."""
        with self._lock:
            return list(self._configs.keys())

    def clear(self) -> None:
        """Clear all configurations."""
        with self._lock:
            self._configs.clear()


class EventSourcedProviderRepository(IProviderRepository):
    """
    Repository that persists providers using event sourcing.

    Features:
    - Stores events in EventStore
    - Rebuilds provider state from events
    - Supports snapshots for performance
    - Publishes events to EventBus after save
    - Caches loaded providers

    Thread-safe implementation.
    """

    def __init__(
        self,
        event_store: Optional[EventStore] = None,
        event_bus: Optional[EventBus] = None,
        snapshot_store: Optional[EventStoreSnapshot] = None,
        snapshot_interval: int = 50,
    ):
        """
        Initialize the event sourced repository.

        Args:
            event_store: Event store for persistence (defaults to global)
            event_bus: Event bus for publishing (defaults to global)
            snapshot_store: Optional snapshot store for performance
            snapshot_interval: Events between snapshots
        """
        self._event_store = event_store or get_event_store()
        self._event_bus = event_bus or get_event_bus()
        self._snapshot_store = snapshot_store
        self._snapshot_interval = snapshot_interval

        # Configuration store (for non-event data like command, env)
        self._config_store = ProviderConfigStore()

        # In-memory cache for loaded providers
        self._cache: Dict[str, EventSourcedProvider] = {}
        self._lock = threading.RLock()

    def add(self, provider_id: str, provider: ProviderLike) -> None:
        """
        Add or update a provider by persisting its uncommitted events.

        If provider has uncommitted events, they are appended to the event store.
        Then the events are published to the event bus.

        Args:
            provider_id: Provider identifier
            provider: Provider instance (should be EventSourcedProvider)
        """
        if not provider_id:
            raise ValueError("Provider ID cannot be empty")

        with self._lock:
            # Save configuration if it's a new provider or config changed
            self._save_config(provider_id, provider)

            # Handle non-event-sourced providers
            if not isinstance(provider, EventSourcedProvider):
                # For backward compatibility, just cache it
                self._cache[provider_id] = provider
                return

            # Get uncommitted events
            events = provider.get_uncommitted_events()

            if events:
                # Get current version from event store
                current_version = self._event_store.get_version(provider_id)

                # Append events
                new_version = self._event_store.append(
                    stream_id=provider_id,
                    events=events,
                    expected_version=current_version,
                )

                # Mark events as committed
                provider.mark_events_committed()

                # Create snapshot if needed
                if self._snapshot_store:
                    events_since_snapshot = self._get_events_since_snapshot(provider_id)
                    if events_since_snapshot >= self._snapshot_interval:
                        self._create_snapshot(provider)

                # Publish events
                for event in events:
                    self._event_bus.publish(event)

                logger.debug(
                    f"Saved {len(events)} events for provider {provider_id}, "
                    f"version {current_version} -> {new_version}"
                )

            # Update cache
            self._cache[provider_id] = provider

    def get(self, provider_id: str) -> Optional[ProviderLike]:
        """
        Load a provider by rebuilding from events.

        First checks cache, then loads from event store.
        Uses snapshots if available for performance.

        Args:
            provider_id: Provider identifier

        Returns:
            Provider if found, None otherwise
        """
        with self._lock:
            # Check cache first
            if provider_id in self._cache:
                return self._cache[provider_id]

            # Load from event store
            provider = self._load_from_events(provider_id)

            if provider:
                self._cache[provider_id] = provider

            return provider

    def _load_from_events(self, provider_id: str) -> Optional[EventSourcedProvider]:
        """Load provider from event store."""
        # Load configuration
        config = self._config_store.load(provider_id)
        if not config:
            # Check if there are events for this provider
            if not self._event_store.stream_exists(provider_id):
                return None
            # Use default config
            config = {"mode": "subprocess"}

        # Try loading from snapshot first
        snapshot = None
        snapshot_version = -1

        if self._snapshot_store:
            snapshot_data = self._snapshot_store.load_snapshot(provider_id)
            if snapshot_data:
                snapshot = ProviderSnapshot.from_dict(snapshot_data["state"])
                snapshot_version = snapshot_data["version"]

        # Load events (from snapshot version or beginning)
        events = self._event_store.load(stream_id=provider_id, from_version=snapshot_version + 1)

        # Convert stored events to domain events
        domain_events = self._hydrate_events(events)

        if snapshot:
            # Load from snapshot + subsequent events
            provider = EventSourcedProvider.from_snapshot(snapshot, domain_events)
        else:
            if not domain_events and not self._event_store.stream_exists(provider_id):
                return None

            # Load from scratch
            provider = EventSourcedProvider.from_events(
                provider_id=provider_id,
                mode=config.get("mode", "subprocess"),
                events=domain_events,
                command=config.get("command"),
                image=config.get("image"),
                endpoint=config.get("endpoint"),
                env=config.get("env"),
                idle_ttl_s=config.get("idle_ttl_s", 300),
                health_check_interval_s=config.get("health_check_interval_s", 60),
                max_consecutive_failures=config.get("max_consecutive_failures", 3),
            )

        return provider

    def _hydrate_events(self, stored_events: List[StoredEvent]) -> List[DomainEvent]:
        """Convert stored events to domain events."""
        from ..domain.events import (
            HealthCheckFailed,
            HealthCheckPassed,
            ProviderDegraded,
            ProviderIdleDetected,
            ProviderStarted,
            ProviderStateChanged,
            ProviderStopped,
            ToolInvocationCompleted,
            ToolInvocationFailed,
            ToolInvocationRequested,
        )

        event_classes = {
            "ProviderStarted": ProviderStarted,
            "ProviderStopped": ProviderStopped,
            "ProviderDegraded": ProviderDegraded,
            "ProviderStateChanged": ProviderStateChanged,
            "ToolInvocationRequested": ToolInvocationRequested,
            "ToolInvocationCompleted": ToolInvocationCompleted,
            "ToolInvocationFailed": ToolInvocationFailed,
            "HealthCheckPassed": HealthCheckPassed,
            "HealthCheckFailed": HealthCheckFailed,
            "ProviderIdleDetected": ProviderIdleDetected,
        }

        domain_events = []

        for stored in stored_events:
            event_class = event_classes.get(stored.event_type)
            if event_class:
                # Extract event data (remove event_type from data dict)
                event_data = {
                    k: v for k, v in stored.data.items() if k not in ("event_type", "event_id", "occurred_at")
                }

                try:
                    event = event_class(**event_data)
                    # Restore original event_id and occurred_at
                    event.event_id = stored.event_id
                    event.occurred_at = stored.occurred_at
                    domain_events.append(event)
                except Exception as e:
                    logger.warning(f"Failed to hydrate event {stored.event_type}: {e}")

        return domain_events

    def _save_config(self, provider_id: str, provider: ProviderLike) -> None:
        """Save provider configuration."""
        if hasattr(provider, "_command"):
            config = {
                "mode": getattr(provider, "_mode", "subprocess"),
                "command": getattr(provider, "_command", None),
                "image": getattr(provider, "_image", None),
                "endpoint": getattr(provider, "_endpoint", None),
                "env": getattr(provider, "_env", {}),
                "idle_ttl_s": getattr(provider, "_idle_ttl_s", 300),
                "health_check_interval_s": getattr(provider, "_health_check_interval_s", 60),
                "max_consecutive_failures": (
                    getattr(provider._health, "_max_consecutive_failures", 3) if hasattr(provider, "_health") else 3
                ),
            }
            self._config_store.save(provider_id, config)

    def _get_events_since_snapshot(self, provider_id: str) -> int:
        """Get number of events since last snapshot."""
        if not self._snapshot_store:
            return self._event_store.get_version(provider_id) + 1

        snapshot_data = self._snapshot_store.load_snapshot(provider_id)
        snapshot_version = snapshot_data["version"] if snapshot_data else -1

        current_version = self._event_store.get_version(provider_id)
        return current_version - snapshot_version

    def _create_snapshot(self, provider: EventSourcedProvider) -> None:
        """Create a snapshot for the provider."""
        if not self._snapshot_store:
            return

        snapshot = provider.create_snapshot()
        version = self._event_store.get_version(provider.provider_id)

        self._snapshot_store.save_snapshot(stream_id=provider.provider_id, version=version, state=snapshot.to_dict())

        logger.debug(f"Created snapshot for provider {provider.provider_id} at version {version}")

    def exists(self, provider_id: str) -> bool:
        """Check if provider exists."""
        with self._lock:
            if provider_id in self._cache:
                return True
            return self._event_store.stream_exists(provider_id) or self._config_store.load(provider_id) is not None

    def remove(self, provider_id: str) -> bool:
        """
        Remove a provider.

        Note: In event sourcing, we typically don't delete events.
        This removes from cache and config store only.
        """
        with self._lock:
            removed = False

            if provider_id in self._cache:
                del self._cache[provider_id]
                removed = True

            if self._config_store.remove(provider_id):
                removed = True

            return removed

    def get_all(self) -> Dict[str, ProviderLike]:
        """Get all providers."""
        with self._lock:
            # Get all known provider IDs
            provider_ids = set(self._cache.keys())
            provider_ids.update(self._event_store.get_all_stream_ids())
            provider_ids.update(self._config_store.get_all_ids())

            result = {}
            for pid in provider_ids:
                provider = self.get(pid)
                if provider:
                    result[pid] = provider

            return result

    def get_all_ids(self) -> List[str]:
        """Get all provider IDs."""
        with self._lock:
            provider_ids = set(self._cache.keys())
            provider_ids.update(self._event_store.get_all_stream_ids())
            provider_ids.update(self._config_store.get_all_ids())
            return list(provider_ids)

    def count(self) -> int:
        """Get number of providers."""
        return len(self.get_all_ids())

    def clear(self) -> None:
        """Clear all providers from cache and config store."""
        with self._lock:
            self._cache.clear()
            self._config_store.clear()
            # Note: Event store is not cleared as events are immutable

    def invalidate_cache(self, provider_id: Optional[str] = None) -> None:
        """Invalidate cache to force reload from event store."""
        with self._lock:
            if provider_id:
                self._cache.pop(provider_id, None)
            else:
                self._cache.clear()

    def get_event_history(self, provider_id: str) -> List[StoredEvent]:
        """
        Get full event history for a provider.

        Useful for debugging and audit.
        """
        return self._event_store.load(provider_id)

    def replay_provider(self, provider_id: str, to_version: int) -> Optional[EventSourcedProvider]:
        """
        Replay provider to a specific version (time travel).

        Args:
            provider_id: Provider identifier
            to_version: Target version to replay to

        Returns:
            Provider at the target version, or None if not found
        """
        config = self._config_store.load(provider_id)
        if not config:
            return None

        events = self._event_store.load(provider_id, from_version=0, to_version=to_version)
        domain_events = self._hydrate_events(events)

        return EventSourcedProvider.from_events(
            provider_id=provider_id,
            mode=config.get("mode", "subprocess"),
            events=domain_events,
            command=config.get("command"),
            image=config.get("image"),
            endpoint=config.get("endpoint"),
            env=config.get("env"),
        )


# Singleton instance
_event_sourced_repository: Optional[EventSourcedProviderRepository] = None


def get_event_sourced_repository() -> EventSourcedProviderRepository:
    """Get the global event sourced repository instance."""
    global _event_sourced_repository
    if _event_sourced_repository is None:
        _event_sourced_repository = EventSourcedProviderRepository()
    return _event_sourced_repository


def set_event_sourced_repository(repository: EventSourcedProviderRepository) -> None:
    """Set the global event sourced repository instance."""
    global _event_sourced_repository
    _event_sourced_repository = repository
