from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import base64
import hashlib
import os
import uuid

MAGIC = b"OSYNCENC1"
SALT_BYTES = 16
HEADER_BYTES = len(MAGIC) + 4 + SALT_BYTES
DEFAULT_KDF_ITERATIONS = 600_000
ENCRYPTED_SUFFIX = ".osync.enc"


class CryptoUnavailableError(RuntimeError):
    pass


class EncryptionConfigError(ValueError):
    pass


class DecryptionError(ValueError):
    pass


@dataclass(slots=True)
class EncryptionManager:
    passphrase: str
    kdf_iterations: int = DEFAULT_KDF_ITERATIONS

    @classmethod
    def from_env(
        cls,
        *,
        env_var: str,
        kdf_iterations: int = DEFAULT_KDF_ITERATIONS,
        required: bool = True,
    ) -> EncryptionManager | None:
        value = os.environ.get(env_var, "")
        if not value:
            if required:
                raise EncryptionConfigError(
                    f"Encryption is enabled but environment variable {env_var!r} is empty."
                )
            return None
        return cls(passphrase=value, kdf_iterations=max(100_000, int(kdf_iterations)))

    def encrypt_bytes(self, plaintext: bytes) -> bytes:
        salt = os.urandom(SALT_BYTES)
        fernet = self._build_fernet(salt, self.kdf_iterations)
        token = fernet.encrypt(plaintext)
        return MAGIC + self.kdf_iterations.to_bytes(4, byteorder="big") + salt + token

    def decrypt_bytes(self, payload: bytes) -> bytes:
        if len(payload) < HEADER_BYTES:
            raise DecryptionError("Encrypted payload is too short.")

        if not payload.startswith(MAGIC):
            raise DecryptionError("Encrypted payload header is invalid.")

        iterations = int.from_bytes(payload[len(MAGIC) : len(MAGIC) + 4], byteorder="big")
        salt_start = len(MAGIC) + 4
        salt_end = salt_start + SALT_BYTES
        salt = payload[salt_start:salt_end]
        token = payload[salt_end:]
        if not token:
            raise DecryptionError("Encrypted payload token is empty.")

        fernet = self._build_fernet(salt, max(100_000, iterations))

        try:
            return fernet.decrypt(token)
        except Exception as exc:  # noqa: BLE001
            raise DecryptionError("Failed to decrypt payload. Check passphrase.") from exc

    def encrypt_file(self, src: Path, dst: Path) -> None:
        plaintext = src.read_bytes()
        salt = os.urandom(SALT_BYTES)
        fernet = self._build_fernet(salt, self.kdf_iterations)
        token = fernet.encrypt(plaintext)
        payload = MAGIC + self.kdf_iterations.to_bytes(4, byteorder="big") + salt + token
        atomic_write_bytes(payload, dst)

    def decrypt_file(self, src: Path, dst: Path) -> None:
        payload = src.read_bytes()
        plaintext = self.decrypt_bytes(payload)
        atomic_write_bytes(plaintext, dst)

    def _build_fernet(self, salt: bytes, iterations: int):
        Fernet = _load_fernet()
        key = _derive_fernet_key(
            passphrase=self.passphrase,
            salt=salt,
            iterations=iterations,
        )
        return Fernet(key)


def encrypted_artifact_path(path: Path) -> Path:
    return Path(str(path) + ENCRYPTED_SUFFIX)


def is_encrypted_artifact_path(path: Path) -> bool:
    return str(path).endswith(ENCRYPTED_SUFFIX)


def encrypted_vault_path(path: Path) -> Path:
    return Path(str(path) + ENCRYPTED_SUFFIX)


def logical_vault_path(path: str) -> str:
    if path.endswith(ENCRYPTED_SUFFIX):
        return path[: -len(ENCRYPTED_SUFFIX)]
    return path


def is_encrypted_payload(payload: bytes) -> bool:
    return len(payload) >= HEADER_BYTES and payload.startswith(MAGIC)


def _derive_fernet_key(*, passphrase: str, salt: bytes, iterations: int) -> bytes:
    material = hashlib.pbkdf2_hmac(
        "sha256",
        passphrase.encode("utf-8"),
        salt,
        max(100_000, int(iterations)),
        dklen=32,
    )
    return base64.urlsafe_b64encode(material)


def _load_fernet():
    try:
        from cryptography.fernet import Fernet  # type: ignore
    except ImportError as exc:  # pragma: no cover
        raise CryptoUnavailableError(
            "Package 'cryptography' is required for encryption. "
            "Install with: pip install 'obsidian-synology-sync[crypto]'"
        ) from exc
    return Fernet


def atomic_write_bytes(content: bytes, dst: Path) -> None:
    dst.parent.mkdir(parents=True, exist_ok=True)
    tmp = dst.parent / f".{dst.name}.tmp-{uuid.uuid4().hex}"
    try:
        tmp.write_bytes(content)
        os.replace(tmp, dst)
    finally:
        if tmp.exists():
            tmp.unlink(missing_ok=True)
