"""btrfs backend for Copy-on-Write filesystem support."""

from __future__ import annotations

import os
import shutil
import subprocess
from datetime import datetime
from pathlib import Path

from gaol.cow.backends.base import CoWBackendBase
from gaol.cow.exceptions import (
    BackendNotAvailableError,
    CoWError,
    DatasetExistsError,
    InvalidMountPointError,
    SendReceiveError,
    SnapshotExistsError,
    SnapshotHasClonesError,
)
from gaol.cow.models import (
    CoWBackendType,
    DatasetConfig,
    DatasetState,
    PoolConfig,
    PoolState,
    PoolStatus,
    Snapshot,
)


def _run_btrfs(args: list[str], check: bool = True) -> subprocess.CompletedProcess[str]:
    """Run a btrfs command."""
    cmd = ["btrfs"] + args
    return subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        check=check,
    )


class BtrfsBackend(CoWBackendBase):
    """btrfs subvolume-based CoW backend."""

    @property
    def name(self) -> str:
        return "btrfs"

    def is_available(self) -> bool:
        """Check if btrfs tools are available."""
        return shutil.which("btrfs") is not None

    def _check_available(self) -> None:
        """Raise if btrfs is not available."""
        if not self.is_available():
            raise BackendNotAvailableError("btrfs", "btrfs-progs not installed")

    def validate_mount_point(self, path: Path) -> bool:
        """Check if path is a btrfs filesystem."""
        self._check_available()

        if not path.exists() or not path.is_dir():
            return False

        try:
            result = _run_btrfs(["filesystem", "show", str(path)], check=False)
            return result.returncode == 0
        except Exception:
            return False

    def _get_subvolume_path(self, pool: PoolState, name: str | None = None) -> Path:
        """Get the path to a subvolume."""
        base = pool.gaol_path
        if name:
            return base / name
        return base

    def _get_snapshot_path(self, pool: PoolState, dataset: str, snapshot: str) -> Path:
        """Get the path to a snapshot."""
        return pool.gaol_path / ".snapshots" / dataset / snapshot

    def _get_filesystem_info(self, path: Path) -> dict[str, int]:
        """Get filesystem usage information."""
        try:
            stat = os.statvfs(path)
            total = stat.f_blocks * stat.f_frsize
            free = stat.f_bavail * stat.f_frsize
            used = total - free
            return {"total": total, "used": used, "free": free}
        except Exception:
            return {"total": 0, "used": 0, "free": 0}

    def _get_subvolume_usage(self, path: Path) -> dict[str, int]:
        """Get subvolume usage via btrfs qgroup (if available)."""
        try:
            result = _run_btrfs(["qgroup", "show", "-r", str(path)], check=False)
            if result.returncode == 0:
                # Parse qgroup output - format varies
                lines = result.stdout.strip().split("\n")
                for line in lines[2:]:  # Skip headers
                    parts = line.split()
                    if len(parts) >= 3:
                        return {
                            "used": int(parts[1]),
                            "referenced": int(parts[2]) if len(parts) > 2 else 0,
                        }
        except Exception:
            pass
        return {"used": 0, "referenced": 0}

    # Pool operations

    def init_pool(self, config: PoolConfig) -> PoolState:
        """Initialize a btrfs pool."""
        self._check_available()

        if not self.validate_mount_point(config.mount_point):
            raise InvalidMountPointError(str(config.mount_point), "btrfs")

        # Create gaol subvolume
        gaol_path = config.mount_point / "gaol"

        if gaol_path.exists():
            # Check if it's already a subvolume
            result = _run_btrfs(["subvolume", "show", str(gaol_path)], check=False)
            if result.returncode != 0:
                raise CoWError(
                    f"Path {gaol_path} exists but is not a btrfs subvolume"
                )
        else:
            try:
                _run_btrfs(["subvolume", "create", str(gaol_path)])
            except subprocess.CalledProcessError as e:
                raise CoWError(f"Failed to create subvolume: {e.stderr}") from e

        # Create snapshots directory
        snapshots_dir = gaol_path / ".snapshots"
        snapshots_dir.mkdir(exist_ok=True)

        # Get filesystem info
        fs_info = self._get_filesystem_info(config.mount_point)

        return PoolState(
            name=config.name,
            backend=CoWBackendType.BTRFS,
            status=PoolStatus.ONLINE,
            mount_point=config.mount_point,
            gaol_path=gaol_path,
            total_bytes=fs_info["total"],
            used_bytes=fs_info["used"],
            free_bytes=fs_info["free"],
            compression=config.compression,
            created_at=datetime.now(),
        )

    def get_pool_info(self, state: PoolState) -> PoolState:
        """Get updated pool information."""
        self._check_available()

        fs_info = self._get_filesystem_info(state.mount_point)

        # Check if subvolume still exists
        status = PoolStatus.ONLINE
        if not state.gaol_path.exists():
            status = PoolStatus.ERROR

        return PoolState(
            name=state.name,
            backend=state.backend,
            status=status,
            mount_point=state.mount_point,
            gaol_path=state.gaol_path,
            total_bytes=fs_info["total"],
            used_bytes=fs_info["used"],
            free_bytes=fs_info["free"],
            compression=state.compression,
            created_at=state.created_at,
        )

    def destroy_pool(self, state: PoolState) -> None:
        """Destroy a btrfs pool."""
        self._check_available()

        if not state.gaol_path.exists():
            return

        # Delete all subvolumes recursively
        self._delete_subvolume_recursive(state.gaol_path)

    def _delete_subvolume_recursive(self, path: Path) -> None:
        """Recursively delete a subvolume and all nested subvolumes."""
        if not path.exists():
            return

        # List child subvolumes
        try:
            result = _run_btrfs(
                ["subvolume", "list", "-o", str(path)], check=False
            )
            if result.returncode == 0:
                for line in result.stdout.strip().split("\n"):
                    if line:
                        # Parse "ID xxx gen xxx top level xxx path <path>"
                        parts = line.split("path ")
                        if len(parts) > 1:
                            child_rel_path = parts[1]
                            # Reconstruct full path
                            child_path = path.parent / child_rel_path
                            if child_path.exists():
                                self._delete_subvolume_recursive(child_path)
        except Exception:
            pass

        # Delete this subvolume
        try:
            _run_btrfs(["subvolume", "delete", str(path)])
        except subprocess.CalledProcessError:
            # Try with force
            try:
                shutil.rmtree(path, ignore_errors=True)
            except Exception:
                pass

    # Dataset operations

    def create_dataset(self, pool: PoolState, config: DatasetConfig) -> DatasetState:
        """Create a new btrfs subvolume."""
        self._check_available()

        subvol_path = self._get_subvolume_path(pool, config.name)

        if subvol_path.exists():
            raise DatasetExistsError(pool.name, config.name)

        try:
            _run_btrfs(["subvolume", "create", str(subvol_path)])
        except subprocess.CalledProcessError as e:
            raise CoWError(f"Failed to create subvolume: {e.stderr}") from e

        # Create snapshot dir for this dataset
        snapshot_dir = pool.gaol_path / ".snapshots" / config.name
        snapshot_dir.mkdir(parents=True, exist_ok=True)

        # Set quota if requested
        if config.quota_mb:
            self._set_quota_internal(subvol_path, config.quota_mb)

        usage = self._get_subvolume_usage(subvol_path)

        return DatasetState(
            name=config.name,
            pool=pool.name,
            path=subvol_path,
            quota_mb=config.quota_mb,
            used_bytes=usage["used"],
            referenced_bytes=usage.get("referenced", 0),
            compression=config.compression or pool.compression,
            created_at=datetime.now(),
            snapshots=[],
        )

    def get_dataset_info(self, pool: PoolState, state: DatasetState) -> DatasetState:
        """Get updated dataset information."""
        self._check_available()

        usage = self._get_subvolume_usage(state.path)

        # List snapshots
        snapshot_dir = pool.gaol_path / ".snapshots" / state.name
        snapshots = []
        if snapshot_dir.exists():
            snapshots = [d.name for d in snapshot_dir.iterdir() if d.is_dir()]

        return DatasetState(
            name=state.name,
            pool=state.pool,
            path=state.path,
            quota_mb=state.quota_mb,
            used_bytes=usage["used"],
            referenced_bytes=usage.get("referenced", 0),
            compression=state.compression,
            created_at=state.created_at,
            snapshots=snapshots,
        )

    def destroy_dataset(self, pool: PoolState, state: DatasetState) -> None:
        """Destroy a dataset and its snapshots."""
        self._check_available()

        # Delete snapshots first
        snapshot_dir = pool.gaol_path / ".snapshots" / state.name
        if snapshot_dir.exists():
            for snap in snapshot_dir.iterdir():
                if snap.is_dir():
                    self._delete_subvolume_recursive(snap)
            snapshot_dir.rmdir()

        # Delete the dataset subvolume
        if state.path.exists():
            self._delete_subvolume_recursive(state.path)

    def _set_quota_internal(self, path: Path, quota_mb: int | None) -> None:
        """Set or remove quota on a subvolume."""
        try:
            # Enable quotas first
            _run_btrfs(["quota", "enable", str(path.parent)], check=False)

            if quota_mb:
                quota_bytes = quota_mb * 1024 * 1024
                # Get subvolume ID
                result = _run_btrfs(["subvolume", "show", str(path)])
                for line in result.stdout.split("\n"):
                    if "Subvolume ID:" in line:
                        subvol_id = line.split()[-1]
                        _run_btrfs([
                            "qgroup", "limit", str(quota_bytes),
                            f"0/{subvol_id}", str(path)
                        ], check=False)
                        break
            else:
                # Remove quota by setting to 'none'
                result = _run_btrfs(["subvolume", "show", str(path)])
                for line in result.stdout.split("\n"):
                    if "Subvolume ID:" in line:
                        subvol_id = line.split()[-1]
                        _run_btrfs([
                            "qgroup", "limit", "none",
                            f"0/{subvol_id}", str(path)
                        ], check=False)
                        break
        except Exception:
            pass  # Quota might not be supported

    def set_quota(self, pool: PoolState, state: DatasetState, quota_mb: int | None) -> DatasetState:
        """Set or remove quota on a dataset."""
        self._check_available()
        self._set_quota_internal(state.path, quota_mb)

        return DatasetState(
            name=state.name,
            pool=state.pool,
            path=state.path,
            quota_mb=quota_mb,
            used_bytes=state.used_bytes,
            referenced_bytes=state.referenced_bytes,
            compression=state.compression,
            created_at=state.created_at,
            snapshots=state.snapshots,
        )

    # Snapshot operations

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

        snapshot_path = self._get_snapshot_path(pool, dataset.name, name)

        if snapshot_path.exists():
            raise SnapshotExistsError(pool.name, dataset.name, name)

        snapshot_path.parent.mkdir(parents=True, exist_ok=True)

        try:
            _run_btrfs([
                "subvolume", "snapshot", "-r",
                str(dataset.path), str(snapshot_path)
            ])
        except subprocess.CalledProcessError as e:
            raise CoWError(f"Failed to create snapshot: {e.stderr}") from e

        usage = self._get_subvolume_usage(snapshot_path)

        return Snapshot(
            name=name,
            dataset=dataset.name,
            pool=pool.name,
            created_at=datetime.now(),
            used_bytes=usage["used"],
            referenced_bytes=usage.get("referenced", 0),
            readonly=True,
        )

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

        snapshot_dir = pool.gaol_path / ".snapshots" / dataset.name
        snapshots = []

        if not snapshot_dir.exists():
            return snapshots

        for snap_path in sorted(snapshot_dir.iterdir()):
            if snap_path.is_dir():
                usage = self._get_subvolume_usage(snap_path)
                # Get creation time from path stat
                try:
                    created = datetime.fromtimestamp(snap_path.stat().st_ctime)
                except Exception:
                    created = datetime.now()

                snapshots.append(Snapshot(
                    name=snap_path.name,
                    dataset=dataset.name,
                    pool=pool.name,
                    created_at=created,
                    used_bytes=usage["used"],
                    referenced_bytes=usage.get("referenced", 0),
                    readonly=True,
                ))

        return snapshots

    def delete_snapshot(
        self, pool: PoolState, dataset: DatasetState, snapshot: Snapshot
    ) -> None:
        """Delete a snapshot."""
        self._check_available()

        snapshot_path = self._get_snapshot_path(pool, dataset.name, snapshot.name)

        if not snapshot_path.exists():
            return

        # Check for clones (btrfs doesn't have direct clone tracking,
        # so we skip this check for btrfs)

        try:
            _run_btrfs(["subvolume", "delete", str(snapshot_path)])
        except subprocess.CalledProcessError as e:
            raise CoWError(f"Failed to delete snapshot: {e.stderr}") from e

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

        snapshot_path = self._get_snapshot_path(pool, dataset.name, snapshot.name)

        if not snapshot_path.exists():
            raise CoWError(f"Snapshot not found: {snapshot_path}")

        # Rename current subvolume
        backup_path = dataset.path.with_suffix(".rollback-backup")
        if backup_path.exists():
            self._delete_subvolume_recursive(backup_path)

        try:
            # Rename current to backup
            dataset.path.rename(backup_path)

            # Create writable snapshot from the readonly snapshot
            _run_btrfs([
                "subvolume", "snapshot",
                str(snapshot_path), str(dataset.path)
            ])

            # Delete backup
            self._delete_subvolume_recursive(backup_path)

        except Exception as e:
            # Restore backup on failure
            if backup_path.exists() and not dataset.path.exists():
                backup_path.rename(dataset.path)
            raise CoWError(f"Rollback failed: {e}") from e

        return self.get_dataset_info(pool, dataset)

    # Clone operations

    def create_clone(
        self, pool: PoolState, snapshot: Snapshot, name: str
    ) -> DatasetState:
        """Create a writable clone from a snapshot."""
        self._check_available()

        snapshot_path = self._get_snapshot_path(pool, snapshot.dataset, snapshot.name)
        clone_path = self._get_subvolume_path(pool, name)

        if clone_path.exists():
            raise DatasetExistsError(pool.name, name)

        try:
            # Create writable snapshot (clone)
            _run_btrfs([
                "subvolume", "snapshot",
                str(snapshot_path), str(clone_path)
            ])
        except subprocess.CalledProcessError as e:
            raise CoWError(f"Failed to create clone: {e.stderr}") from e

        # Create snapshot dir for the clone
        snapshot_dir = pool.gaol_path / ".snapshots" / name
        snapshot_dir.mkdir(parents=True, exist_ok=True)

        usage = self._get_subvolume_usage(clone_path)

        return DatasetState(
            name=name,
            pool=pool.name,
            path=clone_path,
            quota_mb=None,
            used_bytes=usage["used"],
            referenced_bytes=usage.get("referenced", 0),
            compression=pool.compression,
            created_at=datetime.now(),
            snapshots=[],
        )

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

        btrfs clones are already independent (no dependency tracking),
        so this is a no-op.
        """
        return clone

    # Send/Receive operations

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

        snapshot_path = self._get_snapshot_path(pool, dataset.name, snapshot.name)

        if not snapshot_path.exists():
            raise SendReceiveError("send", f"Snapshot not found: {snapshot_path}")

        try:
            cmd = ["btrfs", "send"]

            if base_snapshot:
                base_path = self._get_snapshot_path(
                    pool, dataset.name, base_snapshot.name
                )
                cmd.extend(["-p", str(base_path)])

            cmd.append(str(snapshot_path))

            # Pipe to file
            with open(dest, "wb") as f:
                result = subprocess.run(
                    cmd,
                    stdout=f,
                    stderr=subprocess.PIPE,
                    check=True,
                )

            return dest

        except subprocess.CalledProcessError as e:
            raise SendReceiveError("send", e.stderr.decode()) from e

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

        if not source.exists():
            raise SendReceiveError("receive", f"Source file not found: {source}")

        # Receive into gaol path
        dest_dir = pool.gaol_path

        try:
            with open(source, "rb") as f:
                result = subprocess.run(
                    ["btrfs", "receive", str(dest_dir)],
                    stdin=f,
                    capture_output=True,
                    check=True,
                )

            # Parse output to find received subvolume name
            # Output format: "At subvol <name>"
            output = result.stdout.decode()
            received_name = name
            for line in output.split("\n"):
                if "At subvol" in line or "At snapshot" in line:
                    received_name = line.split()[-1]
                    break

            if not received_name:
                received_name = source.stem

            # Find the received subvolume
            received_path = dest_dir / received_name

            if not received_path.exists():
                raise SendReceiveError(
                    "receive", f"Received subvolume not found: {received_path}"
                )

            # Rename if different name requested
            final_path = received_path
            if name and name != received_name:
                final_path = dest_dir / name
                received_path.rename(final_path)
                received_name = name

            # Create snapshot dir
            snapshot_dir = pool.gaol_path / ".snapshots" / received_name
            snapshot_dir.mkdir(parents=True, exist_ok=True)

            usage = self._get_subvolume_usage(final_path)

            return DatasetState(
                name=received_name,
                pool=pool.name,
                path=final_path,
                quota_mb=None,
                used_bytes=usage["used"],
                referenced_bytes=usage.get("referenced", 0),
                compression=pool.compression,
                created_at=datetime.now(),
                snapshots=[],
            )

        except subprocess.CalledProcessError as e:
            raise SendReceiveError("receive", e.stderr.decode()) from e
