import hashlib
import hmac

from ._compat import constant_time_compare
from .encoding import _base64_alphabet
from .encoding import base64_decode
from .encoding import base64_encode
from .encoding import want_bytes
from .exc import BadSignature


class SigningAlgorithm(object):
    """Subclasses must implement :meth:`get_signature` to provide
    signature generation functionality.
    """

    def get_signature(self, key, value):
        """Returns the signature for the given key and value."""
        raise NotImplementedError()

    def verify_signature(self, key, value, sig):
        """Verifies the given signature matches the expected
        signature.
        """
        return constant_time_compare(sig, self.get_signature(key, value))


class NoneAlgorithm(SigningAlgorithm):
    """Provides an algorithm that does not perform any signing and
    returns an empty signature.
    """

    def get_signature(self, key, value):
        return b""


class HMACAlgorithm(SigningAlgorithm):
    """Provides signature generation using HMACs."""

    #: The digest method to use with the MAC algorithm. This defaults to
    #: SHA-512, but can be changed to any other function in the hashlib
    #: module.
    #:
    #: .. versionchanged:: 1.0
    #:     The default was changed from SHA-1 to SHA-512.
    default_digest_method = staticmethod(hashlib.sha512)

    def __init__(self, digest_method=None):
        if digest_method is None:
            digest_method = self.default_digest_method
        self.digest_method = digest_method

    def get_signature(self, key, value):
        mac = hmac.new(key, msg=value, digestmod=self.digest_method)
        return mac.digest()


class Signer(object):
    """This class can sign and unsign bytes, validating the signature
    provided.

    Salt can be used to namespace the hash, so that a signed string is
    only valid for a given namespace. Leaving this at the default value
    or re-using a salt value across different parts of your application
    where the same signed value in one part can mean something different
    in another part is a security risk.

    See :ref:`the-salt` for an example of what the salt is doing and how
    you can utilize it.

    .. versionadded:: 0.14
        ``key_derivation`` and ``digest_method`` were added as arguments
        to the class constructor.

    .. versionadded:: 0.18
        ``algorithm`` was added as an argument to the class constructor.
    """

    #: The digest method to use for the signer.  This defaults to
    #: SHA-512 but can be changed to any other function in the hashlib
    #: module.
    #:
    #: .. versionchanged:: 1.0
    #:     The default was changed from SHA-1 to SHA-512.
    #:
    #: .. versionadded:: 0.14
    default_digest_method = staticmethod(hashlib.sha512)

    #: Controls how the key is derived. The default is Django-style
    #: concatenation. Possible values are ``concat``, ``django-concat``
    #: and ``hmac``. This is used for deriving a key from the secret key
    #: with an added salt.
    #:
    #: .. versionadded:: 0.14
    default_key_derivation = "django-concat"

    def __init__(
        self,
        secret_key,
        salt=None,
        sep=".",
        key_derivation=None,
        digest_method=None,
        algorithm=None,
    ):
        self.secret_key = want_bytes(secret_key)
        self.sep = want_bytes(sep)
        if self.sep in _base64_alphabet:
            raise ValueError(
                "The given separator cannot be used because it may be"
                " contained in the signature itself. Alphanumeric"
                " characters and `-_=` must not be used."
            )
        self.salt = "itsdangerous.Signer" if salt is None else salt
        if key_derivation is None:
            key_derivation = self.default_key_derivation
        self.key_derivation = key_derivation
        if digest_method is None:
            digest_method = self.default_digest_method
        self.digest_method = digest_method
        if algorithm is None:
            algorithm = HMACAlgorithm(self.digest_method)
        self.algorithm = algorithm

    def derive_key(self):
        """This method is called to derive the key. The default key
        derivation choices can be overridden here. Key derivation is not
        intended to be used as a security method to make a complex key
        out of a short password. Instead you should use large random
        secret keys.
        """
        salt = want_bytes(self.salt)
        if self.key_derivation == "concat":
            return self.digest_method(salt + self.secret_key).digest()
        elif self.key_derivation == "django-concat":
            return self.digest_method(salt + b"signer" + self.secret_key).digest()
        elif self.key_derivation == "hmac":
            mac = hmac.new(self.secret_key, digestmod=self.digest_method)
            mac.update(salt)
            return mac.digest()
        elif self.key_derivation == "none":
            return self.secret_key
        else:
            raise TypeError("Unknown key derivation method")

    def get_signature(self, value):
        """Returns the signature for the given value."""
        value = want_bytes(value)
        key = self.derive_key()
        sig = self.algorithm.get_signature(key, value)
        return base64_encode(sig)

    def sign(self, value):
        """Signs the given string."""
        return want_bytes(value) + want_bytes(self.sep) + self.get_signature(value)

    def verify_signature(self, value, sig):
        """Verifies the signature for the given value."""
        key = self.derive_key()
        try:
            sig = base64_decode(sig)
        except Exception:
            return False
        return self.algorithm.verify_signature(key, value, sig)

    def unsign(self, signed_value):
        """Unsigns the given string."""
        signed_value = want_bytes(signed_value)
        sep = want_bytes(self.sep)
        if sep not in signed_value:
            raise BadSignature("No %r found in value" % self.sep)
        value, sig = signed_value.rsplit(sep, 1)
        if self.verify_signature(value, sig):
            return value
        raise BadSignature("Signature %r does not match" % sig, payload=value)

    def validate(self, signed_value):
        """Only validates the given signed value. Returns ``True`` if
        the signature exists and is valid.
        """
        try:
            self.unsign(signed_value)
            return True
        except BadSignature:
            return False
