"""
FFI bindings for the Rust cryptographic library (crypto_core).
Provides Python interfaces to cryptographic functions implemented in Rust.
"""

import os
import sys
from ctypes import (
    CDLL,
    POINTER,
    c_uint8,
    c_size_t,
    c_uint64,
    c_bool,
    create_string_buffer,
    byref,
)
from pathlib import Path


class CryptoFFIError(Exception):
    """Exception raised for cryptographic FFI errors."""
    pass


class CryptoFFI:
    """Wrapper for Rust cryptographic library using ctypes FFI."""

    def __init__(self):
        """Initialize the FFI wrapper by loading the Rust library."""
        self._lib = None
        self._load_library()

    def _load_library(self) -> None:
        """Load the compiled Rust library (crypto_core.dll on Windows, .so on Linux)."""
        try:
            # Try to find the library in the crypto_core/target/release directory
            base_path = Path(__file__).parent.parent
            crypto_core_path = base_path / "crypto_core" / "target" / "release"

            if sys.platform == "win32":
                lib_name = "crypto_core.dll"
            elif sys.platform == "darwin":
                lib_name = "libcrypto_core.dylib"
            else:
                lib_name = "libcrypto_core.so"

            lib_path = crypto_core_path / lib_name

            if not lib_path.exists():
                raise CryptoFFIError(
                    f"Rust library not found at {lib_path}. "
                    "Please build the crypto_core library first: "
                    "cd crypto_core && cargo build --release"
                )

            self._lib = CDLL(str(lib_path))
            self._setup_function_signatures()

        except OSError as e:
            raise CryptoFFIError(f"Failed to load crypto_core library: {e}")

    def _setup_function_signatures(self) -> None:
        """Setup ctypes function signatures for all exported functions."""
        # derive_key(seed: u64, tick: u64, salt_ptr, salt_len, out_ptr, out_len) -> bool
        self._lib.derive_key.argtypes = [
            c_uint64,
            c_uint64,
            POINTER(c_uint8),
            c_size_t,
            POINTER(c_uint8),
            c_size_t,
        ]
        self._lib.derive_key.restype = c_bool

        # generate_keystream(key_ptr, key_len, salt_ptr, salt_len, out_ptr, out_len) -> bool
        self._lib.generate_keystream.argtypes = [
            POINTER(c_uint8),
            c_size_t,
            POINTER(c_uint8),
            c_size_t,
            POINTER(c_uint8),
            c_size_t,
        ]
        self._lib.generate_keystream.restype = c_bool

        # encrypt_xor(input_ptr, len, key_ptr, out_ptr)
        self._lib.encrypt_xor.argtypes = [
            POINTER(c_uint8),
            c_size_t,
            POINTER(c_uint8),
            POINTER(c_uint8),
        ]
        self._lib.encrypt_xor.restype = None

        # fast_hash(data_ptr, data_len, out_ptr, out_len) -> bool
        self._lib.fast_hash.argtypes = [
            POINTER(c_uint8),
            c_size_t,
            POINTER(c_uint8),
            c_size_t,
        ]
        self._lib.fast_hash.restype = c_bool

    def derive_key(self, seed: int, tick: int, salt: bytes, output_len: int) -> bytes:
        """
        Derive a key using Blake2b hash with seed, tick, and salt.

        Args:
            seed: 64-bit seed value
            tick: 64-bit tick value
            salt: Salt bytes
            output_len: Desired output length

        Returns:
            Derived key bytes

        Raises:
            CryptoFFIError: If key derivation fails
        """
        out = (c_uint8 * output_len)()
        salt_array = (c_uint8 * len(salt)).from_buffer_copy(salt)

        result = self._lib.derive_key(
            c_uint64(seed),
            c_uint64(tick),
            salt_array,
            c_size_t(len(salt)),
            out,
            c_size_t(output_len),
        )

        if not result:
            raise CryptoFFIError("Key derivation failed")

        return bytes(out)

    def generate_keystream(
        self, key: bytes, salt: bytes, output_len: int
    ) -> bytes:
        """
        Generate a keystream using ChaCha20 cipher.

        Args:
            key: Key bytes (must be at least 32 bytes)
            salt: Salt bytes
            output_len: Desired output length

        Returns:
            Generated keystream bytes

        Raises:
            CryptoFFIError: If keystream generation fails
        """
        if len(key) < 32:
            raise CryptoFFIError("Key must be at least 32 bytes")

        out = (c_uint8 * output_len)()
        key_array = (c_uint8 * len(key)).from_buffer_copy(key)
        salt_array = (c_uint8 * len(salt)).from_buffer_copy(salt)

        result = self._lib.generate_keystream(
            key_array,
            c_size_t(len(key)),
            salt_array,
            c_size_t(len(salt)),
            out,
            c_size_t(output_len),
        )

        if not result:
            raise CryptoFFIError("Keystream generation failed")

        return bytes(out)

    def encrypt_xor(self, data: bytes, keystream: bytes) -> bytes:
        """
        Encrypt data using XOR with a keystream.

        Args:
            data: Data to encrypt
            keystream: Keystream bytes (must be same length as data)

        Returns:
            Encrypted data bytes

        Raises:
            ValueError: If data and keystream lengths don't match
        """
        if len(data) != len(keystream):
            raise ValueError("Data and keystream must have the same length")

        out = (c_uint8 * len(data))()
        data_array = (c_uint8 * len(data)).from_buffer_copy(data)
        keystream_array = (c_uint8 * len(keystream)).from_buffer_copy(keystream)

        self._lib.encrypt_xor(
            data_array,
            c_size_t(len(data)),
            keystream_array,
            out,
        )

        return bytes(out)

    def fast_hash(self, data: bytes, output_len: int = 32) -> bytes:
        """
        Fast hash using Blake2b.

        Args:
            data: Data to hash
            output_len: Desired output length (default: 32 bytes)

        Returns:
            Hash bytes

        Raises:
            CryptoFFIError: If hashing fails
        """
        out = (c_uint8 * output_len)()
        data_array = (c_uint8 * len(data)).from_buffer_copy(data)

        result = self._lib.fast_hash(
            data_array,
            c_size_t(len(data)),
            out,
            c_size_t(output_len),
        )

        if not result:
            raise CryptoFFIError("Hashing failed")

        return bytes(out)


# Global instance for easy access
_crypto_ffi = None


def get_crypto_ffi() -> CryptoFFI:
    """Get or create the global CryptoFFI instance."""
    global _crypto_ffi
    if _crypto_ffi is None:
        _crypto_ffi = CryptoFFI()
    return _crypto_ffi
