"""Automatic S3 sync with multi-writer support for mosh pits."""

import threading
import time
from dataclasses import dataclass, field
from typing import Any, Callable, TypeVar

from pydantic import BaseModel

from mosh.metadata import StoredObject
from mosh.pit import DuckDBConfig, Pit
from mosh.sync import S3Sync

T = TypeVar("T", bound=BaseModel)


@dataclass
class SyncConfig:
    """Configuration for automatic S3 synchronization.

    Attributes:
        enabled: Whether auto-sync is enabled. Default True.
        interval_seconds: Time between sync operations. Default 60.0.
        sync_on_close: Whether to sync when closing the pit. Default True.
        max_retries: Maximum retry attempts for failed syncs. Default 3.
        retry_delay_seconds: Delay between retry attempts. Default 5.0.
    """

    enabled: bool = True
    interval_seconds: float = 60.0
    sync_on_close: bool = True
    max_retries: int = 3
    retry_delay_seconds: float = 5.0


@dataclass
class SyncResult:
    """Result of a sync operation.

    Attributes:
        success: Whether the sync completed successfully.
        stats: Sync statistics (uploaded, downloaded, conflicts_resolved).
        error: Exception if sync failed, None otherwise.
        retries: Number of retries needed.
    """

    success: bool
    stats: dict[str, int] = field(default_factory=dict)
    error: Exception | None = None
    retries: int = 0


class _SyncWorker(threading.Thread):
    """Daemon thread for periodic S3 synchronization.

    Runs in the background and periodically calls S3Sync.sync() at the
    configured interval. Uses threading.Event for clean shutdown.
    """

    def __init__(
        self,
        pit: Pit,
        s3_sync: S3Sync,
        lock: threading.RLock,
        config: SyncConfig,
        on_sync_complete: Callable[[SyncResult], None] | None = None,
        on_sync_error: Callable[[Exception], None] | None = None,
    ) -> None:
        """Initialize the sync worker.

        Args:
            pit: The Pit to sync.
            s3_sync: S3Sync instance for S3 operations.
            lock: RLock for thread-safe Pit access.
            config: Sync configuration.
            on_sync_complete: Optional callback after successful sync.
            on_sync_error: Optional callback on sync error.
        """
        super().__init__(daemon=True, name=f"SyncWorker-{pit.name}")
        self._pit = pit
        self._s3_sync = s3_sync
        self._lock = lock
        self._config = config
        self._stop_event = threading.Event()
        self._on_sync_complete = on_sync_complete
        self._on_sync_error = on_sync_error

    def run(self) -> None:
        """Run the sync loop until stopped."""
        while not self._stop_event.is_set():
            # Wait for interval or stop event
            if self._stop_event.wait(timeout=self._config.interval_seconds):
                break

            # Perform sync with retry logic
            result = self._sync_with_retry()

            if result.success and self._on_sync_complete:
                try:
                    self._on_sync_complete(result)
                except Exception:
                    pass
            elif not result.success and self._on_sync_error and result.error:
                try:
                    self._on_sync_error(result.error)
                except Exception:
                    pass

    def _sync_with_retry(self) -> SyncResult:
        """Perform sync with retry logic."""
        last_error: Exception | None = None
        retries = 0

        for attempt in range(self._config.max_retries):
            try:
                with self._lock:
                    stats = self._s3_sync.sync(self._pit)
                return SyncResult(success=True, stats=stats, retries=retries)
            except Exception as e:
                last_error = e
                retries = attempt + 1
                if attempt < self._config.max_retries - 1:
                    time.sleep(self._config.retry_delay_seconds)

        return SyncResult(success=False, error=last_error, retries=retries)

    def stop(self, timeout: float = 5.0) -> bool:
        """Signal the worker to stop and wait for termination.

        Args:
            timeout: Maximum seconds to wait for thread to finish.

        Returns:
            True if thread terminated cleanly, False if timeout expired.
        """
        self._stop_event.set()
        self.join(timeout=timeout)
        return not self.is_alive()


class AutoSyncPit:
    """Thread-safe Pit wrapper with automatic S3 synchronization.

    Wraps a Pit with an RLock for thread safety and manages a background
    _SyncWorker for periodic S3 synchronization. Supports context manager
    protocol for automatic resource cleanup.

    Example:
        ```python
        from mosh import AutoSyncPit, SyncConfig
        from mosh.sync import S3Sync

        s3_sync = S3Sync(bucket="my-bucket", prefix="data")

        with AutoSyncPit(
            name="my_pit",
            s3_sync=s3_sync,
            object_types=[Person],
        ) as pit:
            pit.put(Person(name="Alice", age=30))
            # Background sync runs automatically
        ```
    """

    def __init__(
        self,
        name: str,
        s3_sync: S3Sync,
        db_path: str | None = None,
        object_types: list[type[BaseModel]] | None = None,
        id_field: str | None = None,
        config: SyncConfig | None = None,
        duckdb_config: DuckDBConfig | None = None,
        on_sync_complete: Callable[[SyncResult], None] | None = None,
        on_sync_error: Callable[[Exception], None] | None = None,
    ) -> None:
        """Initialize AutoSyncPit.

        Args:
            name: Name of the pit.
            s3_sync: S3Sync instance for cloud synchronization.
            db_path: Path to DuckDB database file. If None, uses in-memory.
            object_types: List of BaseModel subclasses this pit stores.
            id_field: Optional field name to extract ID from objects.
            config: Sync configuration. Uses defaults if None.
            duckdb_config: DuckDB performance configuration. Controls memory limits,
                          thread count, and other settings for RAM-based performance.
            on_sync_complete: Optional callback after successful sync.
            on_sync_error: Optional callback on sync error.
        """
        self._pit = Pit(
            name=name,
            db_path=db_path,
            object_types=object_types,
            id_field=id_field,
            duckdb_config=duckdb_config,
        )
        self._s3_sync = s3_sync
        self._config = config or SyncConfig()
        self._lock = threading.RLock()
        self._worker: _SyncWorker | None = None
        self._on_sync_complete = on_sync_complete
        self._on_sync_error = on_sync_error
        self._closed = False

        if self._config.enabled:
            self.start_auto_sync()

    @property
    def name(self) -> str:
        """Return the pit name."""
        return self._pit.name

    @property
    def db_path(self) -> str | None:
        """Return the database path."""
        return self._pit.db_path

    @property
    def id_field(self) -> str | None:
        """Return the id_field setting."""
        return self._pit.id_field

    @property
    def duckdb_config(self) -> DuckDBConfig:
        """Return the DuckDB configuration."""
        return self._pit.duckdb_config

    def start_auto_sync(self) -> None:
        """Start the background sync worker.

        If a worker is already running, this is a no-op.
        """
        with self._lock:
            if self._worker is not None and self._worker.is_alive():
                return

            self._worker = _SyncWorker(
                pit=self._pit,
                s3_sync=self._s3_sync,
                lock=self._lock,
                config=self._config,
                on_sync_complete=self._on_sync_complete,
                on_sync_error=self._on_sync_error,
            )
            self._worker.start()

    def stop_auto_sync(self) -> None:
        """Stop the background sync worker.

        Waits up to 5 seconds for the worker to terminate.
        """
        with self._lock:
            if self._worker is not None:
                self._worker.stop(timeout=5.0)
                self._worker = None

    def sync_now(self) -> SyncResult:
        """Perform an immediate synchronization.

        Bypasses the background worker and syncs immediately.
        Uses the configured retry logic.

        Returns:
            SyncResult with success status and statistics.
        """
        last_error: Exception | None = None
        retries = 0

        for attempt in range(self._config.max_retries):
            try:
                with self._lock:
                    stats = self._s3_sync.sync(self._pit)
                return SyncResult(success=True, stats=stats, retries=retries)
            except Exception as e:
                last_error = e
                retries = attempt + 1
                if attempt < self._config.max_retries - 1:
                    time.sleep(self._config.retry_delay_seconds)

        return SyncResult(success=False, error=last_error, retries=retries)

    def close(self) -> None:
        """Close the pit and stop background sync.

        If sync_on_close is enabled, performs a final sync before closing.
        """
        if self._closed:
            return

        self._closed = True

        # Stop background worker first
        self.stop_auto_sync()

        # Sync on close if configured
        if self._config.sync_on_close:
            try:
                with self._lock:
                    self._s3_sync.sync(self._pit)
            except Exception:
                pass

        # Close the underlying pit
        with self._lock:
            self._pit.close()

    # Thread-safe wrappers for Pit methods

    def put(
        self,
        obj: T,
        id: str | None = None,
        tags: list[str] | None = None,
    ) -> StoredObject[T]:
        """Store or update an object (thread-safe)."""
        with self._lock:
            return self._pit.put(obj, id=id, tags=tags)

    def get(self, obj_id: str) -> BaseModel | None:
        """Retrieve object by ID (thread-safe).

        Use get_stored() if you need the full StoredObject with metadata.
        """
        with self._lock:
            return self._pit.get(obj_id)

    def get_stored(self, obj_id: str) -> StoredObject[BaseModel] | None:
        """Retrieve object by ID with metadata (thread-safe)."""
        with self._lock:
            return self._pit.get_stored(obj_id)

    def delete(self, obj_id: str) -> bool:
        """Delete object by ID (thread-safe)."""
        with self._lock:
            return self._pit.delete(obj_id)

    def search(self, **criteria: Any) -> list[BaseModel]:
        """Search objects by field values (thread-safe).

        Use search_stored() if you need the full StoredObject with metadata.
        """
        with self._lock:
            return self._pit.search(**criteria)

    def search_stored(self, **criteria: Any) -> list[StoredObject[BaseModel]]:
        """Search objects by field values with metadata (thread-safe)."""
        with self._lock:
            return self._pit.search_stored(**criteria)

    def search_tags(self, tags: list[str], match_all: bool = False) -> list[BaseModel]:
        """Search objects by tags (thread-safe).

        Use search_tags_stored() if you need the full StoredObject with metadata.
        """
        with self._lock:
            return self._pit.search_tags(tags, match_all)

    def search_tags_stored(self, tags: list[str], match_all: bool = False) -> list[StoredObject[BaseModel]]:
        """Search objects by tags with metadata (thread-safe)."""
        with self._lock:
            return self._pit.search_tags_stored(tags, match_all)

    def all(self) -> list[BaseModel]:
        """Return all objects (thread-safe).

        Use all_stored() if you need the full StoredObject with metadata.
        """
        with self._lock:
            return self._pit.all()

    def all_stored(self) -> list[StoredObject[BaseModel]]:
        """Return all objects with metadata (thread-safe)."""
        with self._lock:
            return self._pit.all_stored()

    def add_tag(self, obj_id: str, tag: str) -> bool:
        """Add a tag to an object (thread-safe)."""
        with self._lock:
            return self._pit.add_tag(obj_id, tag)

    def remove_tag(self, obj_id: str, tag: str) -> bool:
        """Remove a tag from an object (thread-safe)."""
        with self._lock:
            return self._pit.remove_tag(obj_id, tag)

    def get_tags(self, obj_id: str) -> list[str] | None:
        """Get tags for an object (thread-safe)."""
        with self._lock:
            return self._pit.get_tags(obj_id)

    def count(self) -> int:
        """Return count of objects (thread-safe)."""
        with self._lock:
            return self._pit.count()

    def clear(self) -> int:
        """Remove all objects (thread-safe)."""
        with self._lock:
            return self._pit.clear()

    def register_type(self, obj_type: type[BaseModel]) -> None:
        """Register an object type for deserialization (thread-safe)."""
        with self._lock:
            self._pit.register_type(obj_type)

    def __enter__(self) -> "AutoSyncPit":
        """Context manager entry."""
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Context manager exit - closes pit and stops sync."""
        self.close()

    def __len__(self) -> int:
        """Return count of objects (thread-safe)."""
        return self.count()

    def __contains__(self, obj_id: str) -> bool:
        """Check if object with ID exists (thread-safe)."""
        with self._lock:
            return obj_id in self._pit
