"""gocryptfs FUSE encryption backend."""

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,
    MountError,
    VolumeKeyError,
    VolumeMountedError,
    VolumeUnlockedError,
)
from gaol.storage.models import (
    EncryptionBackend,
    VolumeConfig,
    VolumeState,
    VolumeStatus,
)


class GocryptfsBackend(EncryptionBackendBase):
    """gocryptfs FUSE-based file-level encryption.

    This backend uses gocryptfs to provide transparent file-level encryption
    using FUSE. It does not require root privileges and is suitable for
    portable encrypted storage.
    """

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

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

    def _run_cmd(
        self, cmd: list[str], key: bytes | None = None, env: dict | None = None
    ) -> subprocess.CompletedProcess:
        """Run a command."""
        full_env = os.environ.copy()
        if env:
            full_env.update(env)

        try:
            return subprocess.run(
                cmd,
                input=key,
                capture_output=True,
                check=True,
                env=full_env,
            )
        except subprocess.CalledProcessError as e:
            raise MountError(cmd[0], e.stderr.decode()) from e

    def create(self, config: VolumeConfig, key: bytes, storage_dir: Path) -> VolumeState:
        """Create a new gocryptfs volume."""
        if not self.is_available():
            raise BackendNotAvailableError("gocryptfs", "gocryptfs not found in PATH")

        # Create cipher directory
        cipher_dir = storage_dir / config.name / "cipher"
        cipher_dir.mkdir(parents=True, exist_ok=True)

        # Initialize gocryptfs with the key
        # gocryptfs expects password via stdin
        password = key.hex()  # Convert to hex string for gocryptfs

        try:
            self._run_cmd(
                ["gocryptfs", "-init", "-q", str(cipher_dir)],
                key=password.encode(),
            )
        except MountError:
            # Clean up on failure
            if cipher_dir.exists():
                shutil.rmtree(cipher_dir.parent)
            raise

        return VolumeState(
            name=config.name,
            backend=EncryptionBackend.GOCRYPTFS,
            status=VolumeStatus.LOCKED,
            storage_path=cipher_dir,
            size_mb=config.size_mb,  # Note: gocryptfs doesn't enforce size
            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 gocryptfs volume."""
        if state.status in (VolumeStatus.UNLOCKED, VolumeStatus.MOUNTED):
            raise VolumeUnlockedError(state.name)

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

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

        # Mount with gocryptfs
        password = key.hex()

        try:
            self._run_cmd(
                [
                    "gocryptfs",
                    "-q",
                    str(state.storage_path),
                    str(mount_point),
                ],
                key=password.encode(),
            )
        except MountError as e:
            if "Password incorrect" in str(e) or "empty password" in str(e):
                raise VolumeKeyError(state.name, "Incorrect encryption key") from e
            raise

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

    def lock(self, state: VolumeState) -> VolumeState:
        """Lock a gocryptfs volume by unmounting."""
        if state.status == VolumeStatus.LOCKED:
            return state

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

        if state.mount_point:
            # Unmount FUSE filesystem
            try:
                self._run_cmd(["fusermount", "-u", str(state.mount_point)])
            except MountError:
                # Try harder - lazy unmount
                try:
                    self._run_cmd(["fusermount", "-uz", str(state.mount_point)])
                except MountError:
                    pass

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

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

        # Remove cipher directory (parent contains cipher/ subdir)
        if state.storage_path.exists():
            shutil.rmtree(state.storage_path.parent)

    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
