"""Manager for Copy-on-Write filesystem operations."""

from __future__ import annotations

from pathlib import Path

from gaol.cow.backends import CoWBackendBase, get_backend
from gaol.cow.exceptions import (
    DatasetExistsError,
    DatasetNotEmptyError,
    DatasetNotFoundError,
    PoolExistsError,
    PoolNotEmptyError,
    PoolNotFoundError,
    SnapshotNotFoundError,
)
from gaol.cow.models import (
    CloneConfig,
    CoWBackendType,
    DatasetConfig,
    DatasetState,
    PoolConfig,
    PoolState,
    Snapshot,
)
from gaol.cow.store import CoWStore


class CoWManager:
    """Manage Copy-on-Write filesystem operations.

    Handles pool initialization, dataset creation, snapshots, clones,
    and send/receive operations.

    Example:
        >>> manager = CoWManager()
        >>> config = PoolConfig(name="mypool", mount_point="/mnt/btrfs")
        >>> pool = manager.init_pool(config)
        >>> dataset = manager.create_dataset(DatasetConfig(name="data", pool="mypool"))
        >>> snap = manager.create_snapshot("mypool", "data", "v1")
        >>> manager.create_clone(CloneConfig(
        ...     name="data-test",
        ...     source_snapshot="mypool/data@v1",
        ...     pool="mypool"
        ... ))
    """

    def __init__(self, store: CoWStore | None = None) -> None:
        """Initialize the CoW manager."""
        self._store = store or CoWStore()
        self._backends: dict[CoWBackendType, CoWBackendBase] = {}

    def _get_backend(self, backend_type: CoWBackendType) -> CoWBackendBase:
        """Get or create a backend instance."""
        if backend_type not in self._backends:
            self._backends[backend_type] = get_backend(backend_type)
        return self._backends[backend_type]

    # Pool operations

    def init_pool(self, config: PoolConfig) -> PoolState:
        """Initialize a CoW pool on an existing filesystem.

        Args:
            config: Pool configuration

        Returns:
            New pool state

        Raises:
            PoolExistsError: If pool name already exists
            InvalidMountPointError: If mount point is invalid
            BackendNotAvailableError: If backend is not available
        """
        if self._store.pool_exists(config.name):
            raise PoolExistsError(config.name)

        backend = self._get_backend(config.backend)
        state = backend.init_pool(config)
        self._store.save_pool(state)

        return state

    def list_pools(self) -> list[PoolState]:
        """List all pools.

        Returns:
            List of pool states
        """
        return self._store.list_pools()

    def get_pool(self, name: str) -> PoolState | None:
        """Get a pool by name.

        Args:
            name: Pool name

        Returns:
            Pool state or None if not found
        """
        state = self._store.get_pool(name)
        if state:
            backend = self._get_backend(state.backend)
            return backend.get_pool_info(state)
        return None

    def destroy_pool(self, name: str, force: bool = False) -> None:
        """Destroy a pool.

        Args:
            name: Pool name
            force: Force destruction even if pool has datasets

        Raises:
            PoolNotFoundError: If pool doesn't exist
            PoolNotEmptyError: If pool has datasets and force=False
        """
        state = self._store.get_pool(name)
        if not state:
            raise PoolNotFoundError(name)

        # Check for datasets
        dataset_count = self._store.count_datasets_for_pool(name)
        if dataset_count > 0 and not force:
            raise PoolNotEmptyError(name, dataset_count)

        # Delete all datasets first if force
        if force:
            for dataset in self._store.get_datasets_for_pool(name):
                try:
                    self.destroy_dataset(name, dataset.name, force=True)
                except Exception:
                    pass  # Best effort

        backend = self._get_backend(state.backend)
        backend.destroy_pool(state)
        self._store.delete_pool(name)

    # Dataset operations

    def create_dataset(self, config: DatasetConfig) -> DatasetState:
        """Create a new dataset.

        Args:
            config: Dataset configuration

        Returns:
            New dataset state

        Raises:
            PoolNotFoundError: If pool doesn't exist
            DatasetExistsError: If dataset already exists
        """
        pool = self._store.get_pool(config.pool)
        if not pool:
            raise PoolNotFoundError(config.pool)

        if self._store.dataset_exists(config.pool, config.name):
            raise DatasetExistsError(config.pool, config.name)

        backend = self._get_backend(pool.backend)
        state = backend.create_dataset(pool, config)
        self._store.save_dataset(state)

        return state

    def list_datasets(self, pool: str | None = None) -> list[DatasetState]:
        """List datasets, optionally filtered by pool.

        Args:
            pool: Optional pool name to filter by

        Returns:
            List of dataset states
        """
        return self._store.list_datasets(pool)

    def get_dataset(self, pool: str, name: str) -> DatasetState | None:
        """Get a dataset by pool and name.

        Args:
            pool: Pool name
            name: Dataset name

        Returns:
            Dataset state or None if not found
        """
        pool_state = self._store.get_pool(pool)
        if not pool_state:
            return None

        state = self._store.get_dataset(pool, name)
        if state:
            backend = self._get_backend(pool_state.backend)
            return backend.get_dataset_info(pool_state, state)
        return None

    def destroy_dataset(self, pool: str, name: str, force: bool = False) -> None:
        """Destroy a dataset.

        Args:
            pool: Pool name
            name: Dataset name
            force: Force destruction even if dataset has snapshots

        Raises:
            PoolNotFoundError: If pool doesn't exist
            DatasetNotFoundError: If dataset doesn't exist
            DatasetNotEmptyError: If dataset has snapshots and force=False
        """
        pool_state = self._store.get_pool(pool)
        if not pool_state:
            raise PoolNotFoundError(pool)

        state = self._store.get_dataset(pool, name)
        if not state:
            raise DatasetNotFoundError(pool, name)

        # Get current info with snapshots
        backend = self._get_backend(pool_state.backend)
        state = backend.get_dataset_info(pool_state, state)

        if state.snapshots and not force:
            raise DatasetNotEmptyError(pool, name, len(state.snapshots))

        backend.destroy_dataset(pool_state, state)
        self._store.delete_dataset(pool, name)

    def set_quota(self, pool: str, name: str, quota_mb: int | None) -> DatasetState:
        """Set or remove quota on a dataset.

        Args:
            pool: Pool name
            name: Dataset name
            quota_mb: Quota in MB, or None to remove

        Returns:
            Updated dataset state

        Raises:
            PoolNotFoundError: If pool doesn't exist
            DatasetNotFoundError: If dataset doesn't exist
        """
        pool_state = self._store.get_pool(pool)
        if not pool_state:
            raise PoolNotFoundError(pool)

        state = self._store.get_dataset(pool, name)
        if not state:
            raise DatasetNotFoundError(pool, name)

        backend = self._get_backend(pool_state.backend)
        new_state = backend.set_quota(pool_state, state, quota_mb)
        self._store.save_dataset(new_state)

        return new_state

    # Snapshot operations

    def create_snapshot(self, pool: str, dataset: str, name: str) -> Snapshot:
        """Create a snapshot.

        Args:
            pool: Pool name
            dataset: Dataset name
            name: Snapshot name

        Returns:
            New snapshot

        Raises:
            PoolNotFoundError: If pool doesn't exist
            DatasetNotFoundError: If dataset doesn't exist
            SnapshotExistsError: If snapshot already exists
        """
        pool_state = self._store.get_pool(pool)
        if not pool_state:
            raise PoolNotFoundError(pool)

        dataset_state = self._store.get_dataset(pool, dataset)
        if not dataset_state:
            raise DatasetNotFoundError(pool, dataset)

        backend = self._get_backend(pool_state.backend)
        snapshot = backend.create_snapshot(pool_state, dataset_state, name)

        # Update dataset state with new snapshot
        updated_dataset = backend.get_dataset_info(pool_state, dataset_state)
        self._store.save_dataset(updated_dataset)

        return snapshot

    def list_snapshots(self, pool: str, dataset: str) -> list[Snapshot]:
        """List snapshots of a dataset.

        Args:
            pool: Pool name
            dataset: Dataset name

        Returns:
            List of snapshots

        Raises:
            PoolNotFoundError: If pool doesn't exist
            DatasetNotFoundError: If dataset doesn't exist
        """
        pool_state = self._store.get_pool(pool)
        if not pool_state:
            raise PoolNotFoundError(pool)

        dataset_state = self._store.get_dataset(pool, dataset)
        if not dataset_state:
            raise DatasetNotFoundError(pool, dataset)

        backend = self._get_backend(pool_state.backend)
        return backend.list_snapshots(pool_state, dataset_state)

    def delete_snapshot(self, pool: str, dataset: str, name: str) -> None:
        """Delete a snapshot.

        Args:
            pool: Pool name
            dataset: Dataset name
            name: Snapshot name

        Raises:
            PoolNotFoundError: If pool doesn't exist
            DatasetNotFoundError: If dataset doesn't exist
            SnapshotNotFoundError: If snapshot doesn't exist
            SnapshotHasClonesError: If snapshot has dependent clones
        """
        pool_state = self._store.get_pool(pool)
        if not pool_state:
            raise PoolNotFoundError(pool)

        dataset_state = self._store.get_dataset(pool, dataset)
        if not dataset_state:
            raise DatasetNotFoundError(pool, dataset)

        backend = self._get_backend(pool_state.backend)
        snapshots = backend.list_snapshots(pool_state, dataset_state)

        snapshot = None
        for s in snapshots:
            if s.name == name:
                snapshot = s
                break

        if not snapshot:
            raise SnapshotNotFoundError(pool, dataset, name)

        backend.delete_snapshot(pool_state, dataset_state, snapshot)

        # Update dataset state
        updated_dataset = backend.get_dataset_info(pool_state, dataset_state)
        self._store.save_dataset(updated_dataset)

    def rollback(self, pool: str, dataset: str, snapshot: str) -> DatasetState:
        """Rollback a dataset to a snapshot.

        Args:
            pool: Pool name
            dataset: Dataset name
            snapshot: Snapshot name

        Returns:
            Updated dataset state

        Raises:
            PoolNotFoundError: If pool doesn't exist
            DatasetNotFoundError: If dataset doesn't exist
            SnapshotNotFoundError: If snapshot doesn't exist
        """
        pool_state = self._store.get_pool(pool)
        if not pool_state:
            raise PoolNotFoundError(pool)

        dataset_state = self._store.get_dataset(pool, dataset)
        if not dataset_state:
            raise DatasetNotFoundError(pool, dataset)

        backend = self._get_backend(pool_state.backend)
        snapshots = backend.list_snapshots(pool_state, dataset_state)

        snap = None
        for s in snapshots:
            if s.name == snapshot:
                snap = s
                break

        if not snap:
            raise SnapshotNotFoundError(pool, dataset, snapshot)

        new_state = backend.rollback(pool_state, dataset_state, snap)
        self._store.save_dataset(new_state)

        return new_state

    # Clone operations

    def create_clone(self, config: CloneConfig) -> DatasetState:
        """Create a clone from a snapshot.

        Args:
            config: Clone configuration

        Returns:
            New dataset state for the clone

        Raises:
            PoolNotFoundError: If pool doesn't exist
            SnapshotNotFoundError: If source snapshot doesn't exist
            DatasetExistsError: If clone name already exists
        """
        pool_state = self._store.get_pool(config.pool)
        if not pool_state:
            raise PoolNotFoundError(config.pool)

        # Parse source_snapshot (pool/dataset@snapshot)
        if "@" not in config.source_snapshot:
            raise ValueError("source_snapshot must be in format 'pool/dataset@snapshot'")

        dataset_part, snapshot_name = config.source_snapshot.rsplit("@", 1)
        if "/" in dataset_part:
            source_pool, source_dataset = dataset_part.rsplit("/", 1)
        else:
            source_pool = config.pool
            source_dataset = dataset_part

        # Get source dataset
        source_pool_state = self._store.get_pool(source_pool)
        if not source_pool_state:
            raise PoolNotFoundError(source_pool)

        source_dataset_state = self._store.get_dataset(source_pool, source_dataset)
        if not source_dataset_state:
            raise DatasetNotFoundError(source_pool, source_dataset)

        # Find the snapshot
        backend = self._get_backend(source_pool_state.backend)
        snapshots = backend.list_snapshots(source_pool_state, source_dataset_state)

        snapshot = None
        for s in snapshots:
            if s.name == snapshot_name:
                snapshot = s
                break

        if not snapshot:
            raise SnapshotNotFoundError(source_pool, source_dataset, snapshot_name)

        # Check if clone name exists
        if self._store.dataset_exists(config.pool, config.name):
            raise DatasetExistsError(config.pool, config.name)

        # Create the clone
        clone_state = backend.create_clone(pool_state, snapshot, config.name)
        self._store.save_dataset(clone_state)

        return clone_state

    def promote_clone(self, pool: str, name: str) -> DatasetState:
        """Promote a clone to an independent dataset.

        Args:
            pool: Pool name
            name: Clone name

        Returns:
            Updated dataset state

        Raises:
            PoolNotFoundError: If pool doesn't exist
            DatasetNotFoundError: If clone doesn't exist
        """
        pool_state = self._store.get_pool(pool)
        if not pool_state:
            raise PoolNotFoundError(pool)

        clone_state = self._store.get_dataset(pool, name)
        if not clone_state:
            raise DatasetNotFoundError(pool, name)

        backend = self._get_backend(pool_state.backend)
        new_state = backend.promote_clone(pool_state, clone_state)
        self._store.save_dataset(new_state)

        return new_state

    # Send/Receive operations

    def send(
        self,
        pool: str,
        dataset: str,
        snapshot: str,
        dest: Path,
        base_snapshot: str | None = None,
    ) -> Path:
        """Send a snapshot to a file.

        Args:
            pool: Pool name
            dataset: Dataset name
            snapshot: Snapshot name
            dest: Destination file path
            base_snapshot: Optional base snapshot for incremental send

        Returns:
            Path to created stream file

        Raises:
            PoolNotFoundError: If pool doesn't exist
            DatasetNotFoundError: If dataset doesn't exist
            SnapshotNotFoundError: If snapshot doesn't exist
            SendReceiveError: If send fails
        """
        pool_state = self._store.get_pool(pool)
        if not pool_state:
            raise PoolNotFoundError(pool)

        dataset_state = self._store.get_dataset(pool, dataset)
        if not dataset_state:
            raise DatasetNotFoundError(pool, dataset)

        backend = self._get_backend(pool_state.backend)
        snapshots = backend.list_snapshots(pool_state, dataset_state)

        snap = None
        base_snap = None
        for s in snapshots:
            if s.name == snapshot:
                snap = s
            if base_snapshot and s.name == base_snapshot:
                base_snap = s

        if not snap:
            raise SnapshotNotFoundError(pool, dataset, snapshot)

        if base_snapshot and not base_snap:
            raise SnapshotNotFoundError(pool, dataset, base_snapshot)

        return backend.send(pool_state, dataset_state, snap, dest, base_snap)

    def receive(
        self,
        pool: str,
        source: Path,
        name: str | None = None,
    ) -> DatasetState:
        """Receive a snapshot stream into a new dataset.

        Args:
            pool: Pool to receive into
            source: Path to stream file
            name: Optional name for received dataset

        Returns:
            New dataset state

        Raises:
            PoolNotFoundError: If pool doesn't exist
            SendReceiveError: If receive fails
        """
        pool_state = self._store.get_pool(pool)
        if not pool_state:
            raise PoolNotFoundError(pool)

        backend = self._get_backend(pool_state.backend)
        state = backend.receive(pool_state, source, name)
        self._store.save_dataset(state)

        return state


# Global instance
_manager: CoWManager | None = None


def get_cow_manager() -> CoWManager:
    """Get the global CoW manager instance."""
    global _manager
    if _manager is None:
        _manager = CoWManager()
    return _manager
