"""
NAVIG Vault Encryption Module

Provides Fernet symmetric encryption for credential data.
Master key is derived from OS keyring or machine-specific data.
"""

import base64
import os
import platform
import socket
from pathlib import Path

# Try to import cryptography, provide helpful error if missing
try:
    from cryptography.fernet import Fernet, InvalidToken
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

    CRYPTOGRAPHY_AVAILABLE = True
except ImportError:
    CRYPTOGRAPHY_AVAILABLE = False
    Fernet = None  # type: ignore
    InvalidToken = Exception  # type: ignore


class VaultEncryptionError(Exception):
    """Raised when encryption/decryption fails."""

    pass


class VaultEncryption:
    """
    Handles vault encryption and decryption using Fernet.

    The master key is derived from:
    1. OS keyring (if available) - most secure
    2. Machine-specific data + salt file - fallback

    All credential data is encrypted before storage and decrypted
    on retrieval.
    """

    SALT_FILE = "vault.salt"
    KEYRING_SERVICE = "navig-vault"
    KEYRING_USERNAME = "master-key"

    def __init__(self, vault_dir: Path):
        """
        Initialize encryption manager.

        Args:
            vault_dir: Directory where vault files are stored
        """
        if not CRYPTOGRAPHY_AVAILABLE:
            raise ImportError(
                "cryptography package is required for vault encryption. "
                "Install it with: pip install cryptography"
            )

        self.vault_dir = vault_dir
        self._fernet: Fernet | None = None

    def _get_machine_id(self) -> str:
        """
        Get a unique machine identifier scoped to the current OS user.

        Combines system properties with the running user's account name so that
        the derived vault key is unique per (machine, user) pair.  This prevents
        any local user on a shared workstation from replicating another user's
        master key simply from publicly visible OS metrics.
        """
        import getpass

        components = [
            platform.node(),  # Hostname
            socket.gethostname(),  # Network hostname
            platform.system(),  # OS name
            platform.machine(),  # CPU architecture
            getpass.getuser(),  # Current OS user — isolates key per account
        ]

        # Try to get more stable identifiers on Windows
        if platform.system() == "Windows":
            try:
                import winreg

                key = winreg.OpenKey(
                    winreg.HKEY_LOCAL_MACHINE,
                    r"SOFTWARE\Microsoft\Cryptography",
                )
                machine_guid, _ = winreg.QueryValueEx(key, "MachineGuid")
                components.append(machine_guid)
            except Exception:
                pass  # Not critical, use other components

        return "-".join(filter(None, components))

    def _get_or_create_salt(self) -> bytes:
        """Get or generate the encryption salt."""
        salt_path = self.vault_dir / self.SALT_FILE

        if salt_path.exists():
            return salt_path.read_bytes()

        # Generate new salt
        salt = os.urandom(16)

        # Ensure directory exists with proper permissions
        self.vault_dir.mkdir(parents=True, exist_ok=True)
        salt_path.write_bytes(salt)

        # Set restrictive permissions (Unix only)
        try:
            try:
                os.chmod(salt_path, 0o600)
            except (OSError, PermissionError):
                pass
        except OSError:
            pass  # best-effort cleanup

        return salt

    def _try_keyring(self) -> bytes | None:
        """
        Try to get/set master key from OS keyring.

        Returns None if keyring is not available.
        The key is stored and returned as a complete Fernet key (base64-encoded).
        """
        try:
            import keyring

            # Try to get existing key
            stored_key = keyring.get_password(self.KEYRING_SERVICE, self.KEYRING_USERNAME)
            if stored_key:
                # The stored key is already a valid Fernet key (base64-encoded string)
                # Just convert it to bytes
                return stored_key.encode()

            # Generate and store new key
            new_key = Fernet.generate_key()
            keyring.set_password(
                self.KEYRING_SERVICE,
                self.KEYRING_USERNAME,
                new_key.decode(),  # Store as string
            )
            return new_key

        except ImportError:
            return None  # keyring not installed
        except Exception:
            return None  # keyring backend not available

    def _derive_key(self, salt: bytes) -> bytes:
        """
        Derive encryption key from machine ID and salt.

        Uses PBKDF2 with SHA256 and 480,000 iterations for
        strong key derivation.
        """
        machine_id = self._get_machine_id()

        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=480000,  # OWASP recommended minimum for PBKDF2-SHA256
        )

        derived = kdf.derive(machine_id.encode())
        return base64.urlsafe_b64encode(derived)

    def _get_master_key(self) -> bytes:
        """
        Get the master encryption key.

        Tries OS keyring first, falls back to derived key.
        """
        # Try keyring first (most secure)
        keyring_key = self._try_keyring()
        if keyring_key:
            return keyring_key

        # Fall back to derived key
        salt = self._get_or_create_salt()
        return self._derive_key(salt)

    @property
    def fernet(self) -> Fernet:
        """
        Get the Fernet cipher instance.

        Lazily initializes on first access.
        """
        if self._fernet is None:
            key = self._get_master_key()
            self._fernet = Fernet(key)
        return self._fernet

    def encrypt(self, data: str) -> bytes:
        """
        Encrypt string data.

        Args:
            data: Plaintext string to encrypt

        Returns:
            Encrypted bytes (base64-encoded Fernet token)

        Raises:
            VaultEncryptionError: If encryption fails
        """
        try:
            return self.fernet.encrypt(data.encode("utf-8"))
        except Exception as e:
            raise VaultEncryptionError(f"Encryption failed: {e}") from e

    def decrypt(self, token: bytes) -> str:
        """
        Decrypt data back to string.

        Args:
            token: Encrypted Fernet token bytes

        Returns:
            Decrypted plaintext string

        Raises:
            VaultEncryptionError: If decryption fails (invalid key or corrupted data)
        """
        try:
            return self.fernet.decrypt(token).decode("utf-8")
        except InvalidToken as e:
            raise VaultEncryptionError(
                "Decryption failed: Invalid key or corrupted data. "
                "The vault may have been created on a different machine."
            ) from e
        except Exception as e:
            raise VaultEncryptionError(f"Decryption failed: {e}") from e

    def rotate_key(self, new_key: bytes | None = None) -> None:
        """
        Rotate the encryption key.

        This will require re-encrypting all stored data with the new key.
        Use with caution!

        Args:
            new_key: Optional specific key to use (generates random if None)
        """
        # This is a placeholder for future key rotation support
        # Implementation would need to:
        # 1. Decrypt all data with old key
        # 2. Generate/set new key
        # 3. Re-encrypt all data with new key
        raise NotImplementedError(
            "Key rotation is not yet implemented. Please backup and recreate credentials."
        )


def check_encryption_available() -> bool:
    """Check if encryption is available."""
    return CRYPTOGRAPHY_AVAILABLE
