"""Cryptographic operations for ECP Auth.

Provides challenge generation, digital signature creation and
verification using RSA and ECDSA algorithms. All operations use
the ``cryptography`` library for key handling and the ``Crypto``
(pycryptodome) library for additional algorithm support.
"""

from __future__ import annotations

import base64
import hashlib
import logging
import secrets
import time
from dataclasses import dataclass
from typing import Any

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa, utils
from cryptography.x509 import Certificate

from ecp_auth.exceptions import (
    ChallengeError,
    ChallengeExpiredError,
    SignatureVerificationError,
    UnsupportedAlgorithmError,
)
from ecp_auth.settings import get_challenge_length, get_challenge_timeout

logger: logging.Logger = logging.getLogger(__name__)


# --- Hash Algorithm Mapping ---

HASH_ALGORITHMS: dict[str, type[hashes.HashAlgorithm]] = {
    "sha256": hashes.SHA256,
    "sha384": hashes.SHA384,
    "sha512": hashes.SHA512,
}


@dataclass(frozen=True)
class Challenge:
    """Represents a cryptographic challenge for the challenge-response protocol.

    Attributes:
        nonce: The random challenge nonce as a hex string.
        nonce_bytes: The raw nonce bytes.
        created_at: Unix timestamp when the challenge was created.
        timeout: Timeout in seconds after which the challenge expires.
    """

    nonce: str
    nonce_bytes: bytes
    created_at: float
    timeout: int

    @property
    def is_expired(self) -> bool:
        """Check if the challenge has expired.

        Returns:
            True if the challenge has expired, False otherwise.
        """
        return (time.time() - self.created_at) > self.timeout

    def to_dict(self) -> dict[str, Any]:
        """Serialize the challenge to a dictionary for session storage.

        Returns:
            Dictionary representation of the challenge.
        """
        return {
            "nonce": self.nonce,
            "created_at": self.created_at,
            "timeout": self.timeout,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> Challenge:
        """Deserialize a challenge from a dictionary.

        Args:
            data: Dictionary with challenge data.

        Returns:
            Reconstructed Challenge instance.

        Raises:
            ChallengeError: If the dictionary is missing required keys.
        """
        try:
            nonce_hex: str = data["nonce"]
            return cls(
                nonce=nonce_hex,
                nonce_bytes=bytes.fromhex(nonce_hex),
                created_at=data["created_at"],
                timeout=data["timeout"],
            )
        except (KeyError, ValueError) as exc:
            raise ChallengeError(
                f"Invalid challenge data: {exc}"
            ) from exc


def generate_challenge() -> Challenge:
    """Generate a new cryptographic challenge nonce.

    Creates a random nonce using ``secrets.token_bytes`` for use
    in the challenge-response authentication protocol.

    Returns:
        A ``Challenge`` instance with a fresh nonce.

    Example:
        >>> challenge = generate_challenge()
        >>> print(challenge.nonce)
        'a1b2c3d4...'
    """
    nonce_length: int = get_challenge_length()
    timeout: int = get_challenge_timeout()

    nonce_bytes: bytes = secrets.token_bytes(nonce_length)
    nonce_hex: str = nonce_bytes.hex()

    challenge = Challenge(
        nonce=nonce_hex,
        nonce_bytes=nonce_bytes,
        created_at=time.time(),
        timeout=timeout,
    )

    logger.debug(
        "Generated challenge nonce (length: %d bytes, timeout: %ds).",
        nonce_length,
        timeout,
    )

    return challenge


def verify_challenge_not_expired(challenge: Challenge) -> None:
    """Verify that a challenge has not expired.

    Args:
        challenge: The challenge to validate.

    Raises:
        ChallengeExpiredError: If the challenge has expired.
    """
    if challenge.is_expired:
        elapsed: float = time.time() - challenge.created_at
        raise ChallengeExpiredError(
            f"Challenge expired. Elapsed: {elapsed:.1f}s, "
            f"timeout: {challenge.timeout}s."
        )


def sign_data(
    private_key: Any,
    data: bytes,
    hash_algorithm: str = "sha256",
) -> bytes:
    """Sign data using a private key.

    Supports RSA and ECDSA private keys. The signature is created
    using PKCS#1 v1.5 padding for RSA and the default algorithm
    for ECDSA.

    Args:
        private_key: The private key to sign with (RSA or EC).
        data: The data to sign.
        hash_algorithm: Hash algorithm name (sha256, sha384, sha512).

    Returns:
        The digital signature as raw bytes.

    Raises:
        UnsupportedAlgorithmError: If the key type or hash is unsupported.
        SignatureVerificationError: If signing fails.

    Example:
        >>> signature = sign_data(private_key, challenge.nonce_bytes)
    """
    hash_cls = HASH_ALGORITHMS.get(hash_algorithm)
    if hash_cls is None:
        raise UnsupportedAlgorithmError(
            f"Unsupported hash algorithm: {hash_algorithm}. "
            f"Supported: {', '.join(HASH_ALGORITHMS.keys())}"
        )

    try:
        if isinstance(private_key, rsa.RSAPrivateKey):
            signature: bytes = private_key.sign(
                data,
                padding.PKCS1v15(),
                hash_cls(),
            )
        elif isinstance(private_key, ec.EllipticCurvePrivateKey):
            signature = private_key.sign(
                data,
                ec.ECDSA(hash_cls()),
            )
        else:
            raise UnsupportedAlgorithmError(
                f"Unsupported private key type: {type(private_key).__name__}"
            )

        logger.debug(
            "Data signed successfully (algorithm: %s, key type: %s).",
            hash_algorithm,
            type(private_key).__name__,
        )

        return signature

    except UnsupportedAlgorithmError:
        raise
    except Exception as exc:
        logger.error("Signing failed: %s", exc)
        raise SignatureVerificationError(
            f"Failed to sign data: {exc}"
        ) from exc


def verify_signature(
    certificate: Certificate,
    signature: bytes,
    data: bytes,
    hash_algorithm: str = "sha256",
) -> bool:
    """Verify a digital signature using the certificate's public key.

    Supports RSA (PKCS#1 v1.5) and ECDSA signature verification.

    Args:
        certificate: The X.509 certificate containing the public key.
        signature: The digital signature to verify.
        data: The original data that was signed.
        hash_algorithm: Hash algorithm name (sha256, sha384, sha512).

    Returns:
        ``True`` if the signature is valid.

    Raises:
        SignatureVerificationError: If the signature is invalid.
        UnsupportedAlgorithmError: If the algorithm is unsupported.

    Example:
        >>> is_valid = verify_signature(cert, sig, nonce_bytes, "sha256")
    """
    hash_cls = HASH_ALGORITHMS.get(hash_algorithm)
    if hash_cls is None:
        raise UnsupportedAlgorithmError(
            f"Unsupported hash algorithm: {hash_algorithm}. "
            f"Supported: {', '.join(HASH_ALGORITHMS.keys())}"
        )

    public_key = certificate.public_key()

    try:
        if isinstance(public_key, rsa.RSAPublicKey):
            public_key.verify(
                signature,
                data,
                padding.PKCS1v15(),
                hash_cls(),
            )
        elif isinstance(public_key, ec.EllipticCurvePublicKey):
            public_key.verify(
                signature,
                data,
                ec.ECDSA(hash_cls()),
            )
        else:
            raise UnsupportedAlgorithmError(
                f"Unsupported public key type: {type(public_key).__name__}"
            )

        logger.info("Signature verification succeeded.")
        return True

    except InvalidSignature:
        logger.warning("Signature verification failed: invalid signature.")
        raise SignatureVerificationError(
            "The digital signature is invalid. "
            "The data may have been tampered with."
        )
    except UnsupportedAlgorithmError:
        raise
    except Exception as exc:
        logger.error("Signature verification error: %s", exc)
        raise SignatureVerificationError(
            f"Signature verification failed: {exc}"
        ) from exc


def encode_signature_b64(signature: bytes) -> str:
    """Encode a signature to a Base64 string.

    Args:
        signature: Raw signature bytes.

    Returns:
        Base64-encoded signature string.
    """
    return base64.b64encode(signature).decode("ascii")


def decode_signature_b64(signature_b64: str) -> bytes:
    """Decode a Base64-encoded signature.

    Args:
        signature_b64: Base64-encoded signature string.

    Returns:
        Raw signature bytes.

    Raises:
        SignatureVerificationError: If the Base64 data is invalid.
    """
    try:
        return base64.b64decode(signature_b64)
    except Exception as exc:
        raise SignatureVerificationError(
            f"Invalid Base64 signature data: {exc}"
        ) from exc


def get_certificate_fingerprint(certificate: Certificate) -> str:
    """Calculate the SHA-256 fingerprint of a certificate.

    Args:
        certificate: The X.509 certificate.

    Returns:
        Hex-encoded SHA-256 fingerprint.
    """
    der_data: bytes = certificate.public_bytes(serialization.Encoding.DER)
    return hashlib.sha256(der_data).hexdigest()
