"""
Encryption utilities for ClaudeTools.

This module provides secure encryption and decryption functions for sensitive data
such as credentials, passwords, and API keys. It uses Fernet symmetric encryption
which implements AES-128-CBC with HMAC authentication for data integrity.

Security considerations:
- Uses authenticated encryption (Fernet) to prevent tampering
- Encryption key is loaded from environment configuration
- All encrypted data is base64-encoded for safe storage
- Decrypted values are never logged
- Proper error handling for invalid keys or corrupted data
"""

import base64
import logging
from typing import Optional

from cryptography.fernet import Fernet, InvalidToken

from api.config import get_settings

logger = logging.getLogger(__name__)


def _get_fernet_key() -> bytes:
    """
    Get and validate the Fernet encryption key from configuration.

    The ENCRYPTION_KEY must be a 32-byte (256-bit) key encoded as hex.
    This function converts it to the base64-encoded format required by Fernet.

    Returns:
        bytes: Base64-encoded Fernet key

    Raises:
        ValueError: If the encryption key is invalid or incorrectly formatted

    Note:
        Fernet requires a 32-byte key that's base64-encoded. We store the key
        as hex in the config and convert it here.
    """
    settings = get_settings()

    try:
        # Decode hex key from config
        raw_key = bytes.fromhex(settings.ENCRYPTION_KEY)

        # Validate key length (must be 32 bytes for AES-256)
        if len(raw_key) != 32:
            raise ValueError(
                f"Encryption key must be 32 bytes, got {len(raw_key)} bytes"
            )

        # Convert to base64 format required by Fernet
        fernet_key = base64.urlsafe_b64encode(raw_key)
        return fernet_key

    except ValueError as e:
        logger.error("Invalid encryption key format in configuration")
        raise ValueError(
            f"Invalid encryption key: {str(e)}. "
            "Key must be a 64-character hex string (32 bytes)"
        ) from e


def encrypt_string(plaintext: str) -> str:
    """
    Encrypt a string using Fernet symmetric encryption.

    This function encrypts sensitive data such as passwords, API keys, and
    credentials for secure storage. The encrypted output is base64-encoded
    and can be safely stored in databases or configuration files.

    Args:
        plaintext: The string to encrypt

    Returns:
        str: Base64-encoded encrypted string

    Raises:
        ValueError: If the encryption key is invalid
        TypeError: If plaintext is not a string

    Example:
        ```python
        from api.utils.crypto import encrypt_string

        api_key = "sk-1234567890abcdef"
        encrypted = encrypt_string(api_key)
        # Store encrypted value in database
        ```

    Security notes:
        - Uses Fernet (AES-128-CBC + HMAC)
        - Includes authentication tag to prevent tampering
        - Adds timestamp for optional TTL validation
        - Each encryption produces different output (uses random IV)
    """
    if not isinstance(plaintext, str):
        raise TypeError(f"plaintext must be a string, got {type(plaintext)}")

    try:
        # Get Fernet cipher instance
        fernet_key = _get_fernet_key()
        cipher = Fernet(fernet_key)

        # Encrypt the plaintext (Fernet handles encoding internally)
        plaintext_bytes = plaintext.encode('utf-8')
        encrypted_bytes = cipher.encrypt(plaintext_bytes)

        # Return as string (already base64-encoded by Fernet)
        return encrypted_bytes.decode('ascii')

    except Exception as e:
        logger.error(f"Encryption failed: {type(e).__name__}")
        raise ValueError(f"Failed to encrypt data: {str(e)}") from e


def decrypt_string(ciphertext: str, default: Optional[str] = None) -> str:
    """
    Decrypt a Fernet-encrypted string back to plaintext.

    This function decrypts data that was encrypted using encrypt_string().
    It validates the authentication tag to ensure the data hasn't been
    tampered with.

    Args:
        ciphertext: Base64-encoded encrypted string from encrypt_string()
        default: Optional default value to return if decryption fails.
                If None, raises an exception on failure.

    Returns:
        str: Decrypted plaintext string

    Raises:
        ValueError: If ciphertext is invalid or decryption fails (when default=None)
        TypeError: If ciphertext is not a string

    Example:
        ```python
        from api.utils.crypto import decrypt_string

        encrypted = "gAAAAABf..."  # From database
        api_key = decrypt_string(encrypted)
        # Use decrypted api_key
        ```

        With error handling:
        ```python
        # Return empty string if decryption fails
        api_key = decrypt_string(encrypted, default="")
        ```

    Security notes:
        - Validates HMAC authentication tag
        - Prevents timing attacks through constant-time comparison
        - Decrypted values are never logged
        - Fails safely on tampered or corrupted data
    """
    if not isinstance(ciphertext, str):
        raise TypeError(f"ciphertext must be a string, got {type(ciphertext)}")

    try:
        # Get Fernet cipher instance
        fernet_key = _get_fernet_key()
        cipher = Fernet(fernet_key)

        # Decrypt the ciphertext
        ciphertext_bytes = ciphertext.encode('ascii')
        decrypted_bytes = cipher.decrypt(ciphertext_bytes)

        # Return as string
        return decrypted_bytes.decode('utf-8')

    except InvalidToken as e:
        # Data was tampered with or encrypted with different key
        logger.warning("Decryption failed: Invalid token or corrupted data")

        if default is not None:
            return default

        raise ValueError(
            "Failed to decrypt data: invalid ciphertext or wrong encryption key"
        ) from e

    except Exception as e:
        logger.error(f"Decryption failed: {type(e).__name__}")

        if default is not None:
            return default

        raise ValueError(f"Failed to decrypt data: {str(e)}") from e


def generate_encryption_key() -> str:
    """
    Generate a new random encryption key for use with this module.

    This is a utility function for initial setup or key rotation.
    The generated key should be stored in the ENCRYPTION_KEY environment
    variable or .env file.

    Returns:
        str: 64-character hex string representing a 32-byte key

    Example:
        ```python
        from api.utils.crypto import generate_encryption_key

        new_key = generate_encryption_key()
        print(f"ENCRYPTION_KEY={new_key}")
        # Add to .env file
        ```

    Warning:
        - Only use this during initial setup or key rotation
        - Never rotate keys without migrating existing encrypted data
        - Store the key securely (environment variables, secrets manager)
        - Never commit keys to version control
    """
    # Generate 32 random bytes
    raw_key = Fernet.generate_key()

    # Decode from base64 to get raw bytes, then encode as hex
    key_bytes = base64.urlsafe_b64decode(raw_key)
    hex_key = key_bytes.hex()

    return hex_key
