# SPDX-License-Identifier: MIT

"""
Low-level functions if you want to build your own higher level abstractions.

.. warning::
    This is a "Hazardous Materials" module.  You should **ONLY** use it if
    you're 100% absolutely sure that you know what you’re doing because this
    module is full of land mines, dragons, and dinosaurs with laser guns.
"""

from enum import Enum
from typing import Any

from _argon2_cffi_bindings import ffi, lib

from ._typing import Literal
from .exceptions import HashingError, VerificationError, VerifyMismatchError


__all__ = [
    "ARGON2_VERSION",
    "Type",
    "ffi",
    "hash_secret",
    "hash_secret_raw",
    "verify_secret",
]

ARGON2_VERSION = lib.ARGON2_VERSION_NUMBER
"""
The latest version of the Argon2 algorithm that is supported (and used by
default).

.. versionadded:: 16.1.0
"""


class Type(Enum):
    """
    Enum of Argon2 variants.

    Please see :doc:`parameters` on how to pick one.
    """

    D = lib.Argon2_d
    r"""
    Argon2\ **d** is faster and uses data-depending memory access, which makes
    it less suitable for hashing secrets and more suitable for cryptocurrencies
    and applications with no threats from side-channel timing attacks.
    """
    I = lib.Argon2_i
    r"""
    Argon2\ **i** uses data-independent memory access.  Argon2i is slower as
    it makes more passes over the memory to protect from tradeoff attacks.
    """
    ID = lib.Argon2_id
    r"""
    Argon2\ **id** is a hybrid of Argon2i and Argon2d, using a combination of
    data-depending and data-independent memory accesses, which gives some of
    Argon2i's resistance to side-channel cache timing attacks and much of
    Argon2d's resistance to GPU cracking attacks.

    That makes it the preferred type for password hashing and password-based
    key derivation.

    .. versionadded:: 16.3.0
    """


def hash_secret(
    secret: bytes,
    salt: bytes,
    time_cost: int,
    memory_cost: int,
    parallelism: int,
    hash_len: int,
    type: Type,
    version: int = ARGON2_VERSION,
) -> bytes:
    """
    Hash *secret* and return an **encoded** hash.

    An encoded hash can be directly passed into :func:`verify_secret` as it
    contains all parameters and the salt.

    :param bytes secret: Secret to hash.
    :param bytes salt: A salt_.  Should be random and different for each
        secret.
    :param Type type: Which Argon2 variant to use.
    :param int version: Which Argon2 version to use.

    For an explanation of the Argon2 parameters see :class:`PasswordHasher`.

    :rtype: bytes

    :raises argon2.exceptions.HashingError: If hashing fails.

    .. versionadded:: 16.0.0

    .. _salt: https://en.wikipedia.org/wiki/Salt_(cryptography)
    .. _kibibytes: https://en.wikipedia.org/wiki/Binary_prefix#kibi
    """
    size = (
        lib.argon2_encodedlen(
            time_cost,
            memory_cost,
            parallelism,
            len(salt),
            hash_len,
            type.value,
        )
        + 1
    )
    buf = ffi.new("char[]", size)
    rv = lib.argon2_hash(
        time_cost,
        memory_cost,
        parallelism,
        ffi.new("uint8_t[]", secret),
        len(secret),
        ffi.new("uint8_t[]", salt),
        len(salt),
        ffi.NULL,
        hash_len,
        buf,
        size,
        type.value,
        version,
    )
    if rv != lib.ARGON2_OK:
        raise HashingError(error_to_str(rv))

    return ffi.string(buf)


def hash_secret_raw(
    secret: bytes,
    salt: bytes,
    time_cost: int,
    memory_cost: int,
    parallelism: int,
    hash_len: int,
    type: Type,
    version: int = ARGON2_VERSION,
) -> bytes:
    """
    Hash *password* and return a **raw** hash.

    This function takes the same parameters as :func:`hash_secret`.

    .. versionadded:: 16.0.0
    """
    buf = ffi.new("uint8_t[]", hash_len)

    rv = lib.argon2_hash(
        time_cost,
        memory_cost,
        parallelism,
        ffi.new("uint8_t[]", secret),
        len(secret),
        ffi.new("uint8_t[]", salt),
        len(salt),
        buf,
        hash_len,
        ffi.NULL,
        0,
        type.value,
        version,
    )
    if rv != lib.ARGON2_OK:
        raise HashingError(error_to_str(rv))

    return bytes(ffi.buffer(buf, hash_len))


def verify_secret(hash: bytes, secret: bytes, type: Type) -> Literal[True]:
    """
    Verify whether *secret* is correct for *hash* of *type*.

    :param bytes hash: An encoded Argon2 hash as returned by
        :func:`hash_secret`.
    :param bytes secret: The secret to verify whether it matches the one
        in *hash*.
    :param Type type: Type for *hash*.

    :raises argon2.exceptions.VerifyMismatchError: If verification fails
        because *hash* is not valid for *secret* of *type*.
    :raises argon2.exceptions.VerificationError: If verification fails for
        other reasons.

    :return: ``True`` on success, raise
        :exc:`~argon2.exceptions.VerificationError` otherwise.
    :rtype: bool

    .. versionadded:: 16.0.0
    .. versionchanged:: 16.1.0
        Raise :exc:`~argon2.exceptions.VerifyMismatchError` on mismatches
        instead of its more generic superclass.
    """
    rv = lib.argon2_verify(
        ffi.new("char[]", hash),
        ffi.new("uint8_t[]", secret),
        len(secret),
        type.value,
    )
    if rv == lib.ARGON2_OK:
        return True
    elif rv == lib.ARGON2_VERIFY_MISMATCH:
        raise VerifyMismatchError(error_to_str(rv))
    else:
        raise VerificationError(error_to_str(rv))


def core(context: Any, type: int) -> int:
    """
    Direct binding to the ``argon2_ctx`` function.

    .. warning::
        This is a strictly advanced function working on raw C data structures.
        Both *Argon2*'s and *argon2-cffi*'s higher-level bindings do a lot of
        sanity checks and housekeeping work that *you* are now responsible for
        (e.g. clearing buffers). The structure of the *context* object can,
        has, and will change with *any* release!

        Use at your own peril; *argon2-cffi* does *not* use this binding
        itself.

    :param context: A CFFI *Argon2* context object (i.e. an ``struct
        Argon2_Context``/``argon2_context``).
    :param int type: Which *Argon2* variant to use.  You can use the ``value``
        field of :class:`Type`'s fields.

    :rtype: int
    :return: An *Argon2* error code.  Can be transformed into a string using
        :func:`error_to_str`.

    .. versionadded:: 16.0.0
    """
    return lib.argon2_ctx(context, type)


def error_to_str(error: int) -> str:
    """
    Convert an Argon2 error code into a native string.

    :param int error: An Argon2 error code as returned by :func:`core`.

    :rtype: str

    .. versionadded:: 16.0.0
    """
    msg = ffi.string(lib.argon2_error_message(error))
    msg = msg.decode("ascii")
    return msg
