import abc
import unicodedata

import rsa
from aws_lambda_tools.logging.factory import LOGGING_FACTORY
from rsa import PublicKey
from rsa.pem import load_pem
from vault_client.client.conio_user import ConioUser

from conio_sdk.generated_protobuf import v1_pb2
from conio_sdk.services.conio.sdk.crypto_proof import any_field
from conio_sdk.services.conio.sdk.crypto_proof.crypto_proof_verification_strategy import CryptoProofVerificationStrategy


class CryptoProofOwnerMixin(metaclass=abc.ABCMeta):
    @property
    @abc.abstractmethod
    def cryptoProof(self) -> bytes:
        pass


class SignupCryptoRequestMixin(CryptoProofOwnerMixin, metaclass=abc.ABCMeta):
    @property
    @abc.abstractmethod
    def proofID(self) -> str:
        pass

    @property
    @abc.abstractmethod
    def externalUserID(self) -> str:
        pass

    @property
    @abc.abstractmethod
    def userLevel(self) -> str:
        pass

    @property
    @abc.abstractmethod
    def proofExpiration(self) -> int:
        pass

    @property
    @abc.abstractmethod
    def iban(self) -> str:
        pass

    @property
    @abc.abstractmethod
    def email(self) -> str:
        pass

    @property
    @abc.abstractmethod
    def firstName(self) -> str:
        pass

    @property
    @abc.abstractmethod
    def lastName(self) -> str:
        pass


class RSACryptoProofVerificationStrategy(CryptoProofVerificationStrategy):
    def __init__(self, *rsa_public_keys: str):
        self._rsa_public_keys = tuple(
            PublicKey.load_pkcs1_openssl_der(load_pem(rsa_public_key, 'PUBLIC KEY'))
            for rsa_public_key in rsa_public_keys)

    def verify(self, crypto_proof: SignupCryptoRequestMixin) -> bool:
        data = '|'.join(
            filter(
                # If first_name or last_name are missing, we simply do not include
                # them in the crypto proof
                bool,
                (
                    any_field(crypto_proof, 'proofID', 'proof_id'),
                    'SIGNUP',
                    any_field(crypto_proof, 'externalUserID', 'external_user_id'),
                    any_field(crypto_proof, 'userLevel', 'user_level'),
                    str(any_field(crypto_proof, 'proofExpiration', 'proof_expiration')),
                    crypto_proof.iban,
                    crypto_proof.email,
                    any_field(crypto_proof, 'firstName', 'first_name'),
                    any_field(crypto_proof, 'lastName', 'last_name'),
                )
            )
        )
        data = unicodedata.normalize('NFC', data).encode('utf-8')
        return any(
            self._verify(rsa_public_key, data, crypto_proof)
            for rsa_public_key in self._rsa_public_keys
        )

    def verify_pay_for_bid(self, crypto_proof: v1_pb2.PayForBidCryptoRequest, user: ConioUser) -> bool:
        if len(user.external_reference_ids) != 1:
            LOGGING_FACTORY.security.error(
                'User %s has %s external references, but should be only one: %s',
                user.reference_key_id, len(user.external_reference_ids), user.external_reference_ids)
            raise ValueError
        data = '|'.join(
            (
                any_field(crypto_proof, 'proofID', 'proof_id'),
                'PAY_FOR_BID',
                user.external_reference_ids[0],
                str(any_field(crypto_proof, 'proofExpiration', 'proof_expiration')),
                crypto_proof.external_reference_id
            )
        )
        data = unicodedata.normalize('NFC', data).encode('utf-8')
        return any(
            self._verify(rsa_public_key, data, crypto_proof)
            for rsa_public_key in self._rsa_public_keys
        )

    def verify_pay_for_bid_using_wiretransfer(
            self, bid_id: str, crypto_proof: v1_pb2.PayForBidWTCryptoRequest, user: ConioUser) -> bool:
        if len(user.external_reference_ids) != 1:
            LOGGING_FACTORY.security.error(
                'User %s has %s external references, but should be only one: %s',
                user.reference_key_id, len(user.external_reference_ids), user.external_reference_ids)
            raise ValueError
        data = '|'.join(
            (
                any_field(crypto_proof, 'proofID', 'proof_id'),
                'PAY_FOR_BID_WT',
                bid_id,
                user.external_reference_ids[0],
                str(any_field(crypto_proof, 'proofExpiration', 'proof_expiration'))
            )
        )
        data = unicodedata.normalize('NFC', data).encode('utf-8')
        return any(
            self._verify(rsa_public_key, data, crypto_proof)
            for rsa_public_key in self._rsa_public_keys
        )

    @classmethod
    def _verify(
            cls, rsa_public_key, data, crypto_proof: CryptoProofOwnerMixin):
        try:
            rsa.pkcs1.verify(data, any_field(crypto_proof, 'cryptoProof', 'crypto_proof'), rsa_public_key)
            return True
        except rsa.pkcs1.VerificationError as e:
            LOGGING_FACTORY.security.warning('Could not verify signature:', e, exc_info=True)
            return False
