import hashlib
import random
import string
from contextlib import contextmanager

# Работаем с избыточностью
# Берём биты необходимые для N значений, они есть полезная нагрузка
# Избыточные биты это контрольная сумма для предыдущих


class Spreader:
    def __init__(self, n, k, secret):
        self.N = n
        self.K = k
        self.secret = secret
        self.mul = k // n
        assert self.mul > 1

    def value_hash(self, value):
        bitlen = value.bit_length()
        bytelen = bitlen // 8
        if bitlen % 8:
            bytelen += 1

        m = hashlib.sha1()
        m.update(value.to_bytes(bytelen, 'little'))
        m.update(self.secret)
        return int.from_bytes(m.digest(), 'little') % self.mul

    def spread(self, value):
        assert 0 <= value < self.N

        return value * self.mul + self.value_hash(value)

    def unspread(self, value):
        assert 0 <= value < self.K

        h = value % self.mul
        value //= self.mul

        if self.value_hash(value) != h:
            raise ValueError

        return value


def test_spreader():
    s = Spreader(10, 100, b'a cat is fine too')

    # Значения не повторяются
    assert len({s.spread(i) for i in range(10)}) == 10

    # Расшифровываются обратно
    for i in range(10):
        enc = s.spread(i)
        assert s.unspread(enc) == i

    # Плохие значения падают
    bads = 0
    for i in range(100):
        try:
            s.unspread(i)
        except ValueError:
            bads += 1
    assert bads == 90


test_spreader()


# Делаем набор случайных перестановок в коде купона


@contextmanager
def unaffect_random(rand_seed):
    rstate = random.getstate()
    random.seed(rand_seed)
    yield
    random.setstate(rstate)


class Shuffler:
    def __init__(self, slen, rand_seed=None):
        self.slen = slen
        len_range = list(range(slen))
        with unaffect_random(rand_seed):
            self.sequence = tuple(random.sample(len_range, 2) for _ in range(self.slen))

    @staticmethod
    def switch(string, a, b):
        av = string[a]
        bv = string[b]
        string = string[:a] + bv + string[a + 1:]
        string = string[:b] + av + string[b + 1:]
        return string

    def forward(self, string):
        for a, b in self.sequence:
            string = self.switch(string, a, b)
        return string

    def reverse(self, string):
        for a, b in reversed(self.sequence):
            string = self.switch(string, a, b)
        return string


def test_shuffler():
    assert Shuffler.switch('ABCD', 0, 2) == 'CBAD'
    s = Shuffler(8, 3387434)
    assert s.reverse(s.forward('12345678')) == '12345678'


test_shuffler()


# Далее собственно генерируем купоны


class CouponGenerator:
    def __init__(self, coupon_count, token_length, token_chars, secret, rand_seed):
        self.spreader = Spreader(coupon_count, len(token_chars) ** token_length, secret)
        self.shuffler = Shuffler(token_length, rand_seed)
        self.coupon_count = coupon_count
        self.token_length = token_length
        self.token_chars = token_chars
        self.base = len(token_chars)

    def int_to_code(self, n):
        code = []
        for i in range(self.token_length):
            code.append(self.token_chars[n % self.base])
            n //= self.base
        return ''.join(reversed(code))

    def code_to_int(self, code):
        m = 1
        n = 0
        for char in reversed(code):
            n += m * self.token_chars.index(char)
            m *= self.base
        return n

    def __iter__(self):
        for i in range(self.coupon_count):
            value = self.spreader.spread(i)
            yield self.shuffler.forward(self.int_to_code(value))

    def recognize(self, code):
        value = self.code_to_int(self.shuffler.reverse(code))
        return self.spreader.unspread(value)


cg = CouponGenerator(100, 10, string.ascii_uppercase, b'a cat is fine too', 3387434)
assert cg.int_to_code(0) == 'AAAAAAAAAA'
assert cg.int_to_code(1) == 'AAAAAAAAAB'
assert cg.int_to_code(2) == 'AAAAAAAAAC'
assert cg.code_to_int('AAAAAAAAAA') == 0
assert cg.code_to_int('AAAAAAAAAB') == 1
assert cg.code_to_int('AAAAAAAAAC') == 2
assert cg.code_to_int('AAAAAAAABA') == len(string.ascii_uppercase)

for code_id, code in enumerate(cg):
    print(code)
    assert cg.recognize(code) == code_id

# SXAEJJNDDT
# VCAHJAMKRA
# DNAJKTHRZA
# OSBZDVGALY
# ZOBOIXYBSF
# HBBTCRKOCK
# HIBAGUEUDM
# QSCIYMQBSG
# ALCGIYAEAQ
# NWCDRNNOHZ
# NLCJVGJUMF
# RFDTGMHCPB
# UQDITQUGWP
# LBDDOSCPUP
# QGDGLSWVAR
# PMDAPDXXZM
# QHEOBGJKJT
# DQENBQQLXI
# TDEJRTHTYM
# RRFNJDEBWX
# ...

cg.recognize('AAAAAAAAAA')  # ValueError
