"""
API to sanitation session.

Sanitation session allows having a state within a single sanitation
process.

One important thing stored to the session is a secret key which is
generated to a new random value for each sanitation session, but it
stays constant during the whole sanitation process. Its value is never
revealed, so that it is possible to generate such one way hashes with
it, that should not be redoable afterwards. I.e. during the sanitation
session it's possible to do ``hash(C) -> H`` for any clear text C, but
it is not possible to check if H is the hashed value of C after the
sanitation session has ended.
"""

import hashlib
import hmac
import random
import sys
import threading

from six import int2byte

if sys.version_info >= (3, 6):
    from typing import Callable, Optional, Sequence  # noqa


SECRET_KEY_BITS = 128


_thread_local_storage = threading.local()


def hash_text_to_int(value, bit_length=32):
    # type: (str, int) -> int
    """
    Hash a text value to an integer.

    Generates an integer number based on the hash derived with
    `hash_text` from the given text value.

    :param bit_length: Number of bits to use from the hash value.
    :return: Integer value within ``0 <= result < 2**bit_length``
    """
    hash_value = hash_text(value)
    return int(hash_value[0:(bit_length // 4)], 16)


def hash_text_to_ints(value, bit_lengths=(16, 16, 16, 16)):
    # type: (str, Sequence[int]) -> Sequence[int]
    """
    Hash a text value to a sequence of integers.

    Generates a sequence of integer values with given bit-lengths
    similarly to `hash_text_to_int`, but allowing generating many
    separate numbers with a single call.

    :param bit_lengths:
      Tuple of bit lengths for the resulting integers.  Defines also the
      length of the result tuple.
    :return:
      Tuple of ``n`` integers ``(R_1, ... R_n)`` with the requested
      bit-lengths ``(L_1, ..., L_n)`` and values ranging within
      ``0 <= R_i < 2**L_i`` for each ``i``.
    """
    hash_value = hash_text(value)
    hex_lengths = [x // 4 for x in bit_lengths]
    hex_ranges = (
        (sum(hex_lengths[0:i]), sum(hex_lengths[0:(i + 1)]))
        for i in range(len(hex_lengths)))
    return tuple(int(hash_value[a:b], 16) for (a, b) in hex_ranges)


def hash_text(value, hasher=hashlib.sha256, encoding='utf-8'):
    # type: (str, Callable, str) -> str
    """
    Generate a hash for a text value.

    The hash will be generated by encoding the text to bytes with given
    encoding and then generating a hash with HMAC using the session
    secret as the key and the given hash function.

    :param value: Text value to hash
    :param hasher: Hash function to use, SHA256 by default
    :param encoding: Encoding to use, UTF-8 by default
    :return: Hexadecimal presentation of the hash as a string
    """
    return hash_bytes(value.encode(encoding), hasher)


def hash_bytes(value, hasher=hashlib.sha256):
    # type: (bytes, Callable) -> str
    """
    Generate a hash for a bytes value.

    The hash will be generated by generating a hash with HMAC using the
    session secret as the key and the given hash function.

    :param value: Bytes value to hash
    :param hasher: Hash function to use.
    :return: Hexadecimal presentation of the hash as a string
    """
    return hmac.new(get_secret(), value, hasher).hexdigest()


def get_secret():
    # type: () -> bytes
    """
    Get session specific secret key.

    :return: Session key as bytes
    """
    if not getattr(_thread_local_storage, 'secret_key', None):
        _initialize_session()
    return _thread_local_storage.secret_key  # type: ignore


def reset(secret_key=None):
    # type: (Optional[bytes]) -> None
    """
    Reset the session.

    By default, this resets the value of the secret to None so that, if
    there was an earlier sanitation process ran on the same thread, then
    a next call that needs the secret key of the session will generate a
    new value for it.

    This may also be used to set a predefined value for the secret key.

    :param secret_key:
      Value to set as the new session secret key or None if a new one
      should be generated as soon as one is needed.
    """
    _thread_local_storage.secret_key = secret_key


def _initialize_session():
    # type: () -> None
    """
    Generate a new session key and store it to thread local storage.
    """
    sys_random = random.SystemRandom()
    _thread_local_storage.secret_key = b''.join(
        int2byte(sys_random.randint(0, 255))
        for _ in range(SECRET_KEY_BITS // 8))
