"""LUKS encryption backend using dm-crypt."""

from __future__ import annotations

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

from gaol.config import get_data_dir
from gaol.storage.backends.base import EncryptionBackendBase
from gaol.storage.exceptions import (
    BackendNotAvailableError,
    InsufficientPermissionsError,
    MountError,
    VolumeKeyError,
    VolumeMountedError,
    VolumeUnlockedError,
)
from gaol.storage.models import (
    EncryptionBackend,
    VolumeConfig,
    VolumeState,
    VolumeStatus,
)


class LuksBackend(EncryptionBackendBase):
    """LUKS2 encryption using cryptsetup and loopback devices.

    This backend provides block-level encryption using Linux's dm-crypt
    subsystem. It requires root privileges for most operations.
    """

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

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

    def _check_root(self) -> None:
        """Check if running as root."""
        if os.geteuid() != 0:
            raise InsufficientPermissionsError("LUKS operations")

    def _run_cmd(
        self, cmd: list[str], key: bytes | None = None
    ) -> subprocess.CompletedProcess:
        """Run a command, optionally passing key via stdin."""
        try:
            return subprocess.run(
                cmd,
                input=key,
                capture_output=True,
                check=True,
            )
        except subprocess.CalledProcessError as e:
            raise MountError(cmd[0], e.stderr.decode()) from e

    def _get_dm_name(self, volume_name: str) -> str:
        """Generate device mapper name for volume."""
        return f"gaol-{volume_name}"

    def _find_free_loop(self) -> str:
        """Find a free loop device."""
        result = subprocess.run(
            ["losetup", "-f"],
            capture_output=True,
            check=True,
        )
        return result.stdout.decode().strip()

    def _is_loop_attached(self, image_path: Path) -> str | None:
        """Check if image is already attached to a loop device."""
        try:
            result = subprocess.run(
                ["losetup", "-j", str(image_path)],
                capture_output=True,
                check=True,
            )
            output = result.stdout.decode().strip()
            if output:
                # Format: /dev/loop0: [64769]:123456 (/path/to/image)
                return output.split(":")[0]
        except subprocess.CalledProcessError:
            pass
        return None

    def create(self, config: VolumeConfig, key: bytes, storage_dir: Path) -> VolumeState:
        """Create a new LUKS-encrypted volume."""
        self._check_root()

        if not self.is_available():
            raise BackendNotAvailableError("luks", "cryptsetup not found in PATH")

        # Create sparse image file
        image_path = storage_dir / f"{config.name}.img"
        image_path.parent.mkdir(parents=True, exist_ok=True)

        # Create sparse file
        with open(image_path, "wb") as f:
            f.truncate(config.size_mb * 1024 * 1024)

        # Setup loop device
        loop_device = self._find_free_loop()
        self._run_cmd(["losetup", loop_device, str(image_path)])

        dm_name = self._get_dm_name(config.name)

        try:
            # Format with LUKS2
            self._run_cmd(
                [
                    "cryptsetup",
                    "luksFormat",
                    "--type",
                    "luks2",
                    "--cipher",
                    config.cipher,
                    "--key-size",
                    str(config.key_size),
                    "--key-file",
                    "-",
                    "--batch-mode",
                    loop_device,
                ],
                key=key,
            )

            # Open the volume to create filesystem
            self._run_cmd(
                ["cryptsetup", "open", "--key-file", "-", loop_device, dm_name],
                key=key,
            )

            try:
                # Create filesystem
                dm_path = f"/dev/mapper/{dm_name}"
                self._run_cmd(["mkfs", "-t", config.filesystem, dm_path])
            finally:
                # Close the volume
                self._run_cmd(["cryptsetup", "close", dm_name])
        finally:
            # Detach loop device
            self._run_cmd(["losetup", "-d", loop_device])

        return VolumeState(
            name=config.name,
            backend=EncryptionBackend.LUKS,
            status=VolumeStatus.LOCKED,
            storage_path=image_path,
            size_mb=config.size_mb,
            filesystem=config.filesystem,
            key_secret_name=f"volume_{config.name}_key",
            description=config.description,
            created_at=datetime.now(),
        )

    def unlock(self, state: VolumeState, key: bytes) -> VolumeState:
        """Unlock a LUKS volume."""
        self._check_root()

        if state.status in (VolumeStatus.UNLOCKED, VolumeStatus.MOUNTED):
            raise VolumeUnlockedError(state.name)

        if not self.is_available():
            raise BackendNotAvailableError("luks", "cryptsetup not found in PATH")

        # Check if already attached
        loop_device = self._is_loop_attached(state.storage_path)
        if not loop_device:
            loop_device = self._find_free_loop()
            self._run_cmd(["losetup", loop_device, str(state.storage_path)])

        dm_name = self._get_dm_name(state.name)

        try:
            # Open LUKS volume
            try:
                self._run_cmd(
                    ["cryptsetup", "open", "--key-file", "-", loop_device, dm_name],
                    key=key,
                )
            except MountError as e:
                if "No key available" in str(e) or "wrong key" in str(e).lower():
                    raise VolumeKeyError(state.name, "Incorrect encryption key") from e
                raise

            # Create mount point
            mount_point = get_data_dir() / "volumes" / "mounts" / state.name
            mount_point.mkdir(parents=True, exist_ok=True)

            # Mount filesystem
            dm_path = f"/dev/mapper/{dm_name}"
            self._run_cmd(["mount", dm_path, str(mount_point)])

        except Exception:
            # Cleanup on failure
            subprocess.run(["cryptsetup", "close", dm_name], capture_output=True)
            subprocess.run(["losetup", "-d", loop_device], capture_output=True)
            raise

        return state.model_copy(
            update={
                "status": VolumeStatus.UNLOCKED,
                "loop_device": loop_device,
                "dm_name": dm_name,
                "mount_point": mount_point,
                "unlocked_at": datetime.now(),
            }
        )

    def lock(self, state: VolumeState) -> VolumeState:
        """Lock a LUKS volume."""
        self._check_root()

        if state.status == VolumeStatus.LOCKED:
            return state

        if state.status == VolumeStatus.MOUNTED:
            raise VolumeMountedError(state.name)

        if state.mount_point:
            try:
                self._run_cmd(["umount", str(state.mount_point)])
            except MountError:
                # Try lazy unmount
                try:
                    self._run_cmd(["umount", "-l", str(state.mount_point)])
                except MountError:
                    pass

        if state.dm_name:
            try:
                self._run_cmd(["cryptsetup", "close", state.dm_name])
            except MountError:
                pass

        if state.loop_device:
            try:
                self._run_cmd(["losetup", "-d", state.loop_device])
            except MountError:
                pass

        return state.model_copy(
            update={
                "status": VolumeStatus.LOCKED,
                "loop_device": None,
                "dm_name": None,
                "mount_point": None,
            }
        )

    def destroy(self, state: VolumeState) -> None:
        """Destroy a LUKS volume."""
        if state.status != VolumeStatus.LOCKED:
            raise VolumeUnlockedError(state.name)

        # Remove image file
        if state.storage_path.exists():
            state.storage_path.unlink()

    def get_mount_path(self, state: VolumeState) -> Path | None:
        """Get mount path if unlocked."""
        if state.status in (VolumeStatus.UNLOCKED, VolumeStatus.MOUNTED):
            return state.mount_point
        return None
