# Copyright 2013 Donald Stufft and individual contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Tuple

from nacl import exceptions as exc
from nacl._sodium import ffi, lib
from nacl.exceptions import ensure


__all__ = ["crypto_box_keypair", "crypto_box"]


crypto_box_SECRETKEYBYTES: int = lib.crypto_box_secretkeybytes()
crypto_box_PUBLICKEYBYTES: int = lib.crypto_box_publickeybytes()
crypto_box_SEEDBYTES: int = lib.crypto_box_seedbytes()
crypto_box_NONCEBYTES: int = lib.crypto_box_noncebytes()
crypto_box_ZEROBYTES: int = lib.crypto_box_zerobytes()
crypto_box_BOXZEROBYTES: int = lib.crypto_box_boxzerobytes()
crypto_box_BEFORENMBYTES: int = lib.crypto_box_beforenmbytes()
crypto_box_SEALBYTES: int = lib.crypto_box_sealbytes()
crypto_box_MACBYTES: int = lib.crypto_box_macbytes()


def crypto_box_keypair() -> Tuple[bytes, bytes]:
    """
    Returns a randomly generated public and secret key.

    :rtype: (bytes(public_key), bytes(secret_key))
    """
    pk = ffi.new("unsigned char[]", crypto_box_PUBLICKEYBYTES)
    sk = ffi.new("unsigned char[]", crypto_box_SECRETKEYBYTES)

    rc = lib.crypto_box_keypair(pk, sk)
    ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError)

    return (
        ffi.buffer(pk, crypto_box_PUBLICKEYBYTES)[:],
        ffi.buffer(sk, crypto_box_SECRETKEYBYTES)[:],
    )


def crypto_box_seed_keypair(seed: bytes) -> Tuple[bytes, bytes]:
    """
    Returns a (public, secret) key pair deterministically generated
    from an input ``seed``.

    .. warning:: The seed **must** be high-entropy; therefore,
        its generator **must** be a cryptographic quality
        random function like, for example, :func:`~nacl.utils.random`.

    .. warning:: The seed **must** be protected and remain secret.
        Anyone who knows the seed is really in possession of
        the corresponding PrivateKey.


    :param seed: bytes
    :rtype: (bytes(public_key), bytes(secret_key))
    """
    ensure(isinstance(seed, bytes), "seed must be bytes", raising=TypeError)

    if len(seed) != crypto_box_SEEDBYTES:
        raise exc.ValueError("Invalid seed")

    pk = ffi.new("unsigned char[]", crypto_box_PUBLICKEYBYTES)
    sk = ffi.new("unsigned char[]", crypto_box_SECRETKEYBYTES)

    rc = lib.crypto_box_seed_keypair(pk, sk, seed)
    ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError)

    return (
        ffi.buffer(pk, crypto_box_PUBLICKEYBYTES)[:],
        ffi.buffer(sk, crypto_box_SECRETKEYBYTES)[:],
    )


def crypto_box(message: bytes, nonce: bytes, pk: bytes, sk: bytes) -> bytes:
    """
    Encrypts and returns a message ``message`` using the secret key ``sk``,
    public key ``pk``, and the nonce ``nonce``.

    :param message: bytes
    :param nonce: bytes
    :param pk: bytes
    :param sk: bytes
    :rtype: bytes
    """
    if len(nonce) != crypto_box_NONCEBYTES:
        raise exc.ValueError("Invalid nonce size")

    if len(pk) != crypto_box_PUBLICKEYBYTES:
        raise exc.ValueError("Invalid public key")

    if len(sk) != crypto_box_SECRETKEYBYTES:
        raise exc.ValueError("Invalid secret key")

    padded = (b"\x00" * crypto_box_ZEROBYTES) + message
    ciphertext = ffi.new("unsigned char[]", len(padded))

    rc = lib.crypto_box(ciphertext, padded, len(padded), nonce, pk, sk)
    ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError)

    return ffi.buffer(ciphertext, len(padded))[crypto_box_BOXZEROBYTES:]


def crypto_box_open(
    ciphertext: bytes, nonce: bytes, pk: bytes, sk: bytes
) -> bytes:
    """
    Decrypts and returns an encrypted message ``ciphertext``, using the secret
    key ``sk``, public key ``pk``, and the nonce ``nonce``.

    :param ciphertext: bytes
    :param nonce: bytes
    :param pk: bytes
    :param sk: bytes
    :rtype: bytes
    """
    if len(nonce) != crypto_box_NONCEBYTES:
        raise exc.ValueError("Invalid nonce size")

    if len(pk) != crypto_box_PUBLICKEYBYTES:
        raise exc.ValueError("Invalid public key")

    if len(sk) != crypto_box_SECRETKEYBYTES:
        raise exc.ValueError("Invalid secret key")

    padded = (b"\x00" * crypto_box_BOXZEROBYTES) + ciphertext
    plaintext = ffi.new("unsigned char[]", len(padded))

    res = lib.crypto_box_open(plaintext, padded, len(padded), nonce, pk, sk)
    ensure(
        res == 0,
        "An error occurred trying to decrypt the message",
        raising=exc.CryptoError,
    )

    return ffi.buffer(plaintext, len(padded))[crypto_box_ZEROBYTES:]


def crypto_box_beforenm(pk: bytes, sk: bytes) -> bytes:
    """
    Computes and returns the shared key for the public key ``pk`` and the
    secret key ``sk``. This can be used to speed up operations where the same
    set of keys is going to be used multiple times.

    :param pk: bytes
    :param sk: bytes
    :rtype: bytes
    """
    if len(pk) != crypto_box_PUBLICKEYBYTES:
        raise exc.ValueError("Invalid public key")

    if len(sk) != crypto_box_SECRETKEYBYTES:
        raise exc.ValueError("Invalid secret key")

    k = ffi.new("unsigned char[]", crypto_box_BEFORENMBYTES)

    rc = lib.crypto_box_beforenm(k, pk, sk)
    ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError)

    return ffi.buffer(k, crypto_box_BEFORENMBYTES)[:]


def crypto_box_afternm(message: bytes, nonce: bytes, k: bytes) -> bytes:
    """
    Encrypts and returns the message ``message`` using the shared key ``k`` and
    the nonce ``nonce``.

    :param message: bytes
    :param nonce: bytes
    :param k: bytes
    :rtype: bytes
    """
    if len(nonce) != crypto_box_NONCEBYTES:
        raise exc.ValueError("Invalid nonce")

    if len(k) != crypto_box_BEFORENMBYTES:
        raise exc.ValueError("Invalid shared key")

    padded = b"\x00" * crypto_box_ZEROBYTES + message
    ciphertext = ffi.new("unsigned char[]", len(padded))

    rc = lib.crypto_box_afternm(ciphertext, padded, len(padded), nonce, k)
    ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError)

    return ffi.buffer(ciphertext, len(padded))[crypto_box_BOXZEROBYTES:]


def crypto_box_open_afternm(
    ciphertext: bytes, nonce: bytes, k: bytes
) -> bytes:
    """
    Decrypts and returns the encrypted message ``ciphertext``, using the shared
    key ``k`` and the nonce ``nonce``.

    :param ciphertext: bytes
    :param nonce: bytes
    :param k: bytes
    :rtype: bytes
    """
    if len(nonce) != crypto_box_NONCEBYTES:
        raise exc.ValueError("Invalid nonce")

    if len(k) != crypto_box_BEFORENMBYTES:
        raise exc.ValueError("Invalid shared key")

    padded = (b"\x00" * crypto_box_BOXZEROBYTES) + ciphertext
    plaintext = ffi.new("unsigned char[]", len(padded))

    res = lib.crypto_box_open_afternm(plaintext, padded, len(padded), nonce, k)
    ensure(
        res == 0,
        "An error occurred trying to decrypt the message",
        raising=exc.CryptoError,
    )

    return ffi.buffer(plaintext, len(padded))[crypto_box_ZEROBYTES:]


def crypto_box_easy(
    message: bytes, nonce: bytes, pk: bytes, sk: bytes
) -> bytes:
    """
    Encrypts and returns a message ``message`` using the secret key ``sk``,
    public key ``pk``, and the nonce ``nonce``.

    :param message: bytes
    :param nonce: bytes
    :param pk: bytes
    :param sk: bytes
    :rtype: bytes
    """
    if len(nonce) != crypto_box_NONCEBYTES:
        raise exc.ValueError("Invalid nonce size")

    if len(pk) != crypto_box_PUBLICKEYBYTES:
        raise exc.ValueError("Invalid public key")

    if len(sk) != crypto_box_SECRETKEYBYTES:
        raise exc.ValueError("Invalid secret key")

    _mlen = len(message)
    _clen = crypto_box_MACBYTES + _mlen

    ciphertext = ffi.new("unsigned char[]", _clen)

    rc = lib.crypto_box_easy(ciphertext, message, _mlen, nonce, pk, sk)
    ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError)

    return ffi.buffer(ciphertext, _clen)[:]


def crypto_box_open_easy(
    ciphertext: bytes, nonce: bytes, pk: bytes, sk: bytes
) -> bytes:
    """
    Decrypts and returns an encrypted message ``ciphertext``, using the secret
    key ``sk``, public key ``pk``, and the nonce ``nonce``.

    :param ciphertext: bytes
    :param nonce: bytes
    :param pk: bytes
    :param sk: bytes
    :rtype: bytes
    """
    if len(nonce) != crypto_box_NONCEBYTES:
        raise exc.ValueError("Invalid nonce size")

    if len(pk) != crypto_box_PUBLICKEYBYTES:
        raise exc.ValueError("Invalid public key")

    if len(sk) != crypto_box_SECRETKEYBYTES:
        raise exc.ValueError("Invalid secret key")

    _clen = len(ciphertext)

    ensure(
        _clen >= crypto_box_MACBYTES,
        "Input ciphertext must be at least {} long".format(
            crypto_box_MACBYTES
        ),
        raising=exc.TypeError,
    )

    _mlen = _clen - crypto_box_MACBYTES

    plaintext = ffi.new("unsigned char[]", max(1, _mlen))

    res = lib.crypto_box_open_easy(plaintext, ciphertext, _clen, nonce, pk, sk)
    ensure(
        res == 0,
        "An error occurred trying to decrypt the message",
        raising=exc.CryptoError,
    )

    return ffi.buffer(plaintext, _mlen)[:]


def crypto_box_easy_afternm(message: bytes, nonce: bytes, k: bytes) -> bytes:
    """
    Encrypts and returns the message ``message`` using the shared key ``k`` and
    the nonce ``nonce``.

    :param message: bytes
    :param nonce: bytes
    :param k: bytes
    :rtype: bytes
    """
    if len(nonce) != crypto_box_NONCEBYTES:
        raise exc.ValueError("Invalid nonce")

    if len(k) != crypto_box_BEFORENMBYTES:
        raise exc.ValueError("Invalid shared key")

    _mlen = len(message)
    _clen = crypto_box_MACBYTES + _mlen

    ciphertext = ffi.new("unsigned char[]", _clen)

    rc = lib.crypto_box_easy_afternm(ciphertext, message, _mlen, nonce, k)
    ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError)

    return ffi.buffer(ciphertext, _clen)[:]


def crypto_box_open_easy_afternm(
    ciphertext: bytes, nonce: bytes, k: bytes
) -> bytes:
    """
    Decrypts and returns the encrypted message ``ciphertext``, using the shared
    key ``k`` and the nonce ``nonce``.

    :param ciphertext: bytes
    :param nonce: bytes
    :param k: bytes
    :rtype: bytes
    """
    if len(nonce) != crypto_box_NONCEBYTES:
        raise exc.ValueError("Invalid nonce")

    if len(k) != crypto_box_BEFORENMBYTES:
        raise exc.ValueError("Invalid shared key")

    _clen = len(ciphertext)

    ensure(
        _clen >= crypto_box_MACBYTES,
        "Input ciphertext must be at least {} long".format(
            crypto_box_MACBYTES
        ),
        raising=exc.TypeError,
    )

    _mlen = _clen - crypto_box_MACBYTES

    plaintext = ffi.new("unsigned char[]", max(1, _mlen))

    res = lib.crypto_box_open_easy_afternm(
        plaintext, ciphertext, _clen, nonce, k
    )
    ensure(
        res == 0,
        "An error occurred trying to decrypt the message",
        raising=exc.CryptoError,
    )

    return ffi.buffer(plaintext, _mlen)[:]


def crypto_box_seal(message: bytes, pk: bytes) -> bytes:
    """
    Encrypts and returns a message ``message`` using an ephemeral secret key
    and the public key ``pk``.
    The ephemeral public key, which is embedded in the sealed box, is also
    used, in combination with ``pk``, to derive the nonce needed for the
    underlying box construct.

    :param message: bytes
    :param pk: bytes
    :rtype: bytes

    .. versionadded:: 1.2
    """
    ensure(
        isinstance(message, bytes),
        "input message must be bytes",
        raising=TypeError,
    )

    ensure(
        isinstance(pk, bytes), "public key must be bytes", raising=TypeError
    )

    if len(pk) != crypto_box_PUBLICKEYBYTES:
        raise exc.ValueError("Invalid public key")

    _mlen = len(message)
    _clen = crypto_box_SEALBYTES + _mlen

    ciphertext = ffi.new("unsigned char[]", _clen)

    rc = lib.crypto_box_seal(ciphertext, message, _mlen, pk)
    ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError)

    return ffi.buffer(ciphertext, _clen)[:]


def crypto_box_seal_open(ciphertext: bytes, pk: bytes, sk: bytes) -> bytes:
    """
    Decrypts and returns an encrypted message ``ciphertext``, using the
    recipent's secret key ``sk`` and the sender's ephemeral public key
    embedded in the sealed box. The box construct nonce is derived from
    the recipient's public key ``pk`` and the sender's public key.

    :param ciphertext: bytes
    :param pk: bytes
    :param sk: bytes
    :rtype: bytes

    .. versionadded:: 1.2
    """
    ensure(
        isinstance(ciphertext, bytes),
        "input ciphertext must be bytes",
        raising=TypeError,
    )

    ensure(
        isinstance(pk, bytes), "public key must be bytes", raising=TypeError
    )

    ensure(
        isinstance(sk, bytes), "secret key must be bytes", raising=TypeError
    )

    if len(pk) != crypto_box_PUBLICKEYBYTES:
        raise exc.ValueError("Invalid public key")

    if len(sk) != crypto_box_SECRETKEYBYTES:
        raise exc.ValueError("Invalid secret key")

    _clen = len(ciphertext)

    ensure(
        _clen >= crypto_box_SEALBYTES,
        ("Input ciphertext must be at least {} long").format(
            crypto_box_SEALBYTES
        ),
        raising=exc.TypeError,
    )

    _mlen = _clen - crypto_box_SEALBYTES

    # zero-length malloc results are implementation.dependent
    plaintext = ffi.new("unsigned char[]", max(1, _mlen))

    res = lib.crypto_box_seal_open(plaintext, ciphertext, _clen, pk, sk)
    ensure(
        res == 0,
        "An error occurred trying to decrypt the message",
        raising=exc.CryptoError,
    )

    return ffi.buffer(plaintext, _mlen)[:]
