"""
Service layer for orchestration.

This module coordinates between crypto, storage, and codec layers,
handles environment variable reading, and maps low-level exceptions
to Kilit exception types.
"""

from __future__ import annotations

import base64
import os
from pathlib import Path

from cryptography.exceptions import InvalidTag

from kilit.codecs import Codec, CodecRegistry
from kilit.crypto import Encryptor, validate_scrypt_params
from kilit.exceptions import KilitFileError, KilitFormatError, KilitKeyError
from kilit.storage import FileStorage


class CredentialsService:
    """
    Service layer for credentials operations.

    This layer:
    - Coordinates between crypto, storage, and codec layers
    - Reads environment variables (ONLY layer that does this)
    - Maps low-level exceptions to KilitError subclasses
    - Provides high-level operations for CLI and API layers

    Environment Variables:
    - KILIT_MASTER_KEY: Master key (base64 urlsafe encoded)
    - KILIT_SCRYPT_N: Scrypt N parameter (for NEW encryptions only)
    - KILIT_SCRYPT_R: Scrypt R parameter (for NEW encryptions only)
    - KILIT_SCRYPT_P: Scrypt P parameter (for NEW encryptions only)

    CRITICAL: Environment variables only affect NEW encryptions.
    Decryption ALWAYS uses parameters from the envelope.kdfp field.
    """

    def __init__(
        self, storage: FileStorage | None = None, codec_registry: CodecRegistry | None = None
    ):
        """
        Initialize service with storage and codec registry.

        Args:
            storage: FileStorage instance (creates default if None)
            codec_registry: CodecRegistry instance (creates default if None)
        """
        self.storage = storage or FileStorage()
        self.codec_registry = codec_registry or CodecRegistry()

    def load_master_key(
        self,
        env_var: bool = True,
        cli_key: str | None = None,
        cli_key_path: Path | None = None,
        default_path: Path | None = None,
    ) -> bytes:
        """
        Load master key from sources in priority order.

        Priority: cli_key > env_var > cli_key_path > default_path

        Args:
            env_var: Whether to check KILIT_MASTER_KEY environment variable
            cli_key: Master key provided via CLI option
            cli_key_path: Path to master key file provided via CLI option
            default_path: Default path to master key file

        Returns:
            Decoded master key (32 bytes)

        Raises:
            KilitKeyError: If no source available or invalid format
        """
        key_str = None

        # Priority 1: CLI key
        if cli_key:
            key_str = cli_key

        # Priority 2: Environment variable
        elif env_var:
            key_str = os.environ.get("KILIT_MASTER_KEY")

        # Priority 3: CLI key path
        if key_str is None and cli_key_path:
            try:
                key_str = self.storage.load_key(cli_key_path)
            except KilitFileError:
                pass  # Try next source

        # Priority 4: Default path
        if key_str is None and default_path:
            try:
                key_str = self.storage.load_key(default_path)
            except KilitFileError:
                pass  # No more sources

        # Check if we found a key
        if key_str is None:
            raise KilitKeyError(
                "Master key not found. Provide via:\n"
                "  1. --master-key option\n"
                "  2. KILIT_MASTER_KEY environment variable\n"
                "  3. --master-key-path option\n"
                "  4. config/master.key file (default)"
            )

        # Decode and validate
        try:
            # Add padding if needed
            padding = "=" * (4 - len(key_str) % 4) if len(key_str) % 4 else ""
            key_bytes = base64.urlsafe_b64decode(key_str + padding)

            if len(key_bytes) != 32:
                raise KilitKeyError(
                    f"Invalid master key: expected 32 bytes, got {len(key_bytes)} bytes"
                )

            return key_bytes

        except Exception as e:
            raise KilitKeyError(f"Invalid master key format: {e}")

    def get_scrypt_params_from_env(self) -> tuple[int, int, int]:
        """
        Get Scrypt parameters from environment variables.

        Returns:
            Tuple of (n, r, p) parameters

        Raises:
            KilitKeyError: If parameters are invalid

        Environment Variables:
        - KILIT_SCRYPT_N: CPU/memory cost (default: 2**14 = 16384)
        - KILIT_SCRYPT_R: Block size (default: 8)
        - KILIT_SCRYPT_P: Parallelization (default: 1)

        CRITICAL: These parameters only affect NEW encryptions.
        Decryption ALWAYS uses parameters from the envelope.kdfp field.
        """
        # Defaults
        n = 2**14  # 16384
        r = 8
        p = 1

        # Read from environment
        try:
            if "KILIT_SCRYPT_N" in os.environ:
                n = int(os.environ["KILIT_SCRYPT_N"])
            if "KILIT_SCRYPT_R" in os.environ:
                r = int(os.environ["KILIT_SCRYPT_R"])
            if "KILIT_SCRYPT_P" in os.environ:
                p = int(os.environ["KILIT_SCRYPT_P"])
        except ValueError as e:
            raise KilitKeyError(f"Invalid Scrypt parameter in environment: {e}")

        # Validate
        try:
            validate_scrypt_params(n, r, p)
        except ValueError as e:
            raise KilitKeyError(f"Invalid Scrypt parameters: {e}")

        return (n, r, p)

    def encrypt_credentials(
        self,
        secrets: dict,
        path: Path,
        master_key: bytes,
        codec: Codec | None = None,
    ) -> None:
        """
        Encrypt secrets and save to file.

        Args:
            secrets: Dictionary of secrets to encrypt
            path: Path to save encrypted file
            master_key: Master key (32 bytes)
            codec: Codec to use (auto-detects if None)

        Raises:
            KilitFormatError: If serialization fails
            KilitFileError: If file save fails
            KilitKeyError: If Scrypt parameters are invalid

        Process:
        1. Auto-detect codec if not provided
        2. Serialize secrets using codec
        3. Get Scrypt params from environment (for NEW encryption)
        4. Create Encryptor and encrypt (params stored in envelope.kdfp)
        5. Save envelope using storage
        """
        try:
            # Auto-detect codec if not provided
            if codec is None:
                codec = self.codec_registry.get_codec_for_path(path)

            # Serialize secrets
            try:
                plaintext = codec.dumps(secrets)
            except KilitFormatError:
                raise

            # Get Scrypt params from environment (for NEW encryption)
            n, r, p = self.get_scrypt_params_from_env()

            # Encrypt
            encryptor = Encryptor(master_key, n, r, p)
            envelope = encryptor.encrypt(plaintext)

            # Save
            self.storage.save_envelope(envelope, path)

        except (KilitFormatError, KilitFileError, KilitKeyError):
            raise
        except Exception as e:
            raise KilitFileError(f"Failed to encrypt credentials: {e}")

    def decrypt_credentials(
        self,
        path: Path,
        master_key: bytes,
        codec: Codec | None = None,
    ) -> dict:
        """
        Decrypt credentials from file.

        Args:
            path: Path to encrypted file
            master_key: Master key (32 bytes)
            codec: Codec to use (auto-detects if None)

        Returns:
            Decrypted secrets dictionary

        Raises:
            KilitFileError: If file load fails
            KilitKeyError: If decryption fails (wrong key)
            KilitFormatError: If deserialization fails

        Process:
        1. Load envelope from file
        2. Auto-detect codec if not provided
        3. Create Encryptor (decrypt will use envelope.kdfp, not environment)
        4. Decrypt envelope
        5. Deserialize using codec

        CRITICAL: Environment variables do NOT affect decryption.
        Decryption uses parameters from envelope.kdfp for deterministic behavior.
        """
        try:
            # Load envelope
            envelope = self.storage.load_envelope(path)

            # Auto-detect codec if not provided
            if codec is None:
                codec = self.codec_registry.get_codec_for_path(path)

            # Decrypt (uses envelope.kdfp, not environment)
            # We pass dummy params to Encryptor - decrypt() will use envelope.kdfp
            encryptor = Encryptor(master_key, n=2**14, r=8, p=1)
            try:
                plaintext = encryptor.decrypt(envelope)
            except InvalidTag:
                raise KilitKeyError(f"Failed to decrypt {path}: wrong master key or corrupted file")
            except ValueError as e:
                raise KilitFormatError(f"Invalid envelope format: {e}")

            # Deserialize
            try:
                secrets = codec.loads(plaintext)
            except KilitFormatError:
                raise

            return secrets

        except (KilitFileError, KilitKeyError, KilitFormatError):
            raise
        except Exception as e:
            raise KilitFileError(f"Failed to decrypt credentials: {e}")
