from base64 import b64decode, b64encode
from functools import lru_cache
from hashlib import sha3_224

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import (Encoding,
                                                        NoEncryption,
                                                        PrivateFormat,
                                                        PublicFormat)

from bundlewrap.utils import Fault


@lru_cache(maxsize=None)
def generate_ed25519_private_key(username, node):
    return Fault(
        f'private key {username}@{node.name}',
        lambda username, node: _generate_ed25519_private_key(username, node),
        username=username,
        node=node,
    )


@lru_cache(maxsize=None)
def generate_ed25519_public_key(username, node):
    return Fault(
        f'public key {username}@{node.name}',
        lambda username, node: _generate_ed25519_public_key(username, node),
        username=username,
        node=node,
    )


def _generate_ed25519_private_key(username, node):
    privkey_bytes = Ed25519PrivateKey.from_private_bytes(_secret(username, node))

    nondeterministic_privatekey = privkey_bytes.private_bytes(
        encoding=Encoding.PEM,
        format=PrivateFormat.OpenSSH,
        encryption_algorithm=NoEncryption(),
    ).decode()

    # get relevant lines from string
    nondeterministic_bytes = b64decode(
        ''.join(nondeterministic_privatekey.split('\n')[1:-2])
    )

    # sanity check
    if nondeterministic_bytes[98:102] != nondeterministic_bytes[102:106]:
        raise Exception("checksums should be the same: whats going on here?")

    # replace random bytes with deterministic values
    random_bytes = sha3_224(_secret(username, node)).digest()[0:4]
    deterministic_bytes = (
        nondeterministic_bytes[:98]
        + random_bytes
        + random_bytes
        + nondeterministic_bytes[106:]
    )

    # reassemble file
    deterministic_privatekey = '\n'.join(
        [
            '-----BEGIN OPENSSH PRIVATE KEY-----',
            b64encode(deterministic_bytes).decode(),
            '-----END OPENSSH PRIVATE KEY-----',
        ]
    ) + '\n'

    return deterministic_privatekey


def _generate_ed25519_public_key(username, node):
    return (
        Ed25519PrivateKey.from_private_bytes(_secret(username, node))
        .public_key()
        .public_bytes(
            encoding=Encoding.OpenSSH,
            format=PublicFormat.OpenSSH,
        )
        .decode()
        + f' {username}@{node.name}'
    )


@lru_cache(maxsize=None)
def _secret(username, node):
    return b64decode(
        str(
            node.repo.vault.random_bytes_as_base64_for(
                f"{username}@{node.name}", length=32
            )
        )
    )
