import binascii
import datetime
import hashlib
import re
import string
import typing
import uuid

import bitcoin
import pycountry

from conio_sdk.common import ACCEPTANCE_TYPES_PB2_BY_NAME
from conio_sdk.generated_protobuf import v1_pb2
from pycomb import context as pycomb_ctx, examples
from pycomb import combinators as pycomb_combinators
from conio_sdk.logging.factory import LOGGING_FACTORY

_log = LOGGING_FACTORY.validation.warning


# see https://github.com/bitcoin/bitcoin/blob/08a7316c144f9f2516db8fa62400893f4358c5ae/src/policy/policy.h#L26
_MAX_TX_SIZE = 400000
_MAX_FIAT = 1000000
_MAX_NUM_PRICE_POINTS = 1000
_MAX_TX_BYTES = 500 * 1024
_MAX_RSA_SIGNATURE_BYTES = 4096
_MIN_INTERVAL_MARKET_INFO = 15 * 60
_COMPOUND_OBJECT_TYPE_0X30 = 1
_VARIABLE_SIZE_COMPOUND_OBJECT = 1
_INT_OBJECT_TYPE_0X02 = 1
_VARIABLE_SIZE_INT = 1
_MAX_INT_SIZE = 0x33
_LETTERS = {ord(d): str(i) for i, d in enumerate(string.digits + string.ascii_uppercase)}
_DER_ENCODED_SIGNATURE_SIZE = \
    _COMPOUND_OBJECT_TYPE_0X30 + \
    _VARIABLE_SIZE_COMPOUND_OBJECT + \
    (
        _INT_OBJECT_TYPE_0X02 +
        _VARIABLE_SIZE_INT +
        _MAX_INT_SIZE
    ) * 2


_unlimited_bytes = pycomb_combinators.irreducible(lambda d: isinstance(d, bytes), b'bytes example', name='Bytes')


def _either_field(*fields: str) -> typing.Callable[[typing.Any], bool]:
    def _f(d):
        return any(map(lambda f: bool(getattr(d, f)), fields))
    return _f


def _limited_bytes(size: int, name: typing.Optional[str] = None):
    return pycomb_combinators.subtype(
        _unlimited_bytes,
        lambda d: len(d) <= size,
        examples.String.encode(),
        name=name
    )


BtcRawTransaction = _limited_bytes(_MAX_TX_BYTES, 'BtcRawTransaction')


ActivityID = pycomb_combinators.subtype(
    pycomb_combinators.String,
    lambda d: any(
        map(
            lambda x: d.startswith(x),
            (
                '__SELL__',
                '__BID__',
                '__BTC__'
            )
        )
    ) and len(d) < 128
)


def _timestamp_function(d: int):
    # noinspection PyBroadException
    try:
        datetime.datetime.fromtimestamp(d / 1000)
        return 0 < d == int(d)
    except Exception:
        return False


_Timestamp = pycomb_combinators.subtype(
    pycomb_combinators.Number,
    _timestamp_function
)

PositiveInteger = pycomb_combinators.subtype(pycomb_combinators.Number, lambda d: 0 <= d, name='PositiveInteger')
Satoshi = pycomb_combinators.subtype(pycomb_combinators.Number, lambda d: 0 <= d < 2100000000000000, name='Satoshi')
UnixTimestamp = pycomb_combinators.subtype(pycomb_combinators.Number, lambda d: 0 <= d, name='UnixTimestamp')
RSASignature = _limited_bytes(_MAX_RSA_SIGNATURE_BYTES, 'RSASignature')
FiatCurrency = pycomb_combinators.union(
    pycomb_combinators.constant('EUR'),
    pycomb_combinators.constant('USD'),
    name='FiatCurrency'
)


def _is_der_encoded_signature(d: bytes):
    d = binascii.hexlify(d)
    # noinspection PyBroadException
    try:
        lr, r, ls, s = bitcoin.der_decode_sig(d)

        return 31 <= lr <= 33 and 31 <= ls <= 33
    except Exception:
        return False


def _proto_maybe(combinator, name: str = None, constant_value=None):
    return pycomb_combinators.union(
        pycomb_combinators.maybe(combinator),
        pycomb_combinators.constant('' if constant_value is None else constant_value),
        name=name
    )


def any_type(name='Any'):
    # noinspection PyUnusedLocal
    def _any(value, ctx=None):
        return value

    _any.is_type = lambda d: True

    _any.meta = {
        'name': name
    }
    _any.example = 'Any type'
    return _any


AnyType = any_type()


def bitcoin_address():
    name = 'BitcoinAddress'

    def _is_bitcoin_address(d):
        if not d or len(d) <= 5:  # 1 byte for network + 4 bytes for checksum are mandatory
            _log('%s is NOT a valid Bitcoin address', d)
            return False

        from etc.settings import BTCD_NETWORK

        if d[:4] == 'bcrt':
            # noinspection PyBroadException
            try:
                data = bitcoin.bech32encode(bitcoin.bech32decode(d), prefix=d[:4])
                assert data == d, (data, d)
                return True
            except Exception:
                _log('%s is NOT a valid Bech32 Bitcoin address', d)
                return False
        elif d[:2] in ('bc', 'tb'):
            # noinspection PyBroadException
            try:
                data = bitcoin.bech32encode(bitcoin.bech32decode(d), prefix=d[:2])
                assert data == d, (data, d)
                return True
            except Exception:
                _log('%s is NOT a valid Bitcoin address', d)
                return False
        elif d[0] not in (('1', '3') if BTCD_NETWORK == 'mainnet' else ('2', 'n', 'm')):
            _log('%s is NOT a valid Bitcoin address', d)
            return False

        bin_format = bitcoin.changebase(d, 58, 256)
        ripemd160 = bin_format[0:-4]
        if len(ripemd160) < 21:
            ripemd160 = b'\x00' * (21 - len(ripemd160)) + ripemd160

        result = hashlib.sha256(hashlib.sha256(ripemd160).digest()).digest()[0:4] == bin_format[-4:]
        if not result:
            _log('%s is NOT a valid Bitcoin address', d)
        return result

    def _bitcoin_address_function(x, ctx=None):
        new_ctx_list = pycomb_ctx.create(ctx)
        if new_ctx_list.empty:
            new_ctx_list.append(name)

        pycomb_combinators.assert_type(
            _is_bitcoin_address(x),
            ctx=new_ctx_list, found_type=type(x)
        )

    _bitcoin_address_function.meta = {
        'name': name
    }

    _bitcoin_address_function.is_type = _is_bitcoin_address

    return _bitcoin_address_function


BitcoinAddress = bitcoin_address()


def _is_hex(d) -> bool:
    if not d:
        return True
    # noinspection PyBroadException
    try:
        if len(d) > 4096:
            return False
        binascii.unhexlify(d)
        return True
    except Exception:
        return False


def _is_uuid(d):
    # noinspection PyBroadException
    try:
        if len(d) > 36:
            return False
        uuid.UUID(d)
        return True
    except Exception:
        return False


def _is_bip32(d):
    return d[:4] in ('xpub', 'tpub') and len(d) < 222


def _lim_bytes(d):
    return len(d) < 1024 and bool(d)


def _is_country(e):
    if not e or not isinstance(e, str):
        return False
    # noinspection PyBroadException
    try:
        pycountry.countries.get(alpha_2=e.upper())
        return True
    except Exception:
        return False


def _is_iban(d):
    def _number_iban(iban):
        return (iban[4:] + iban[:4]).translate(_LETTERS)

    def generate_iban_check_digits(iban):
        number_iban = _number_iban(iban[:2] + '00' + iban[4:])
        try:
            number_iban = int(number_iban)
        except ValueError:
            return -1

        return '{:0>2}'.format(98 - (number_iban % 97))

    def valid_iban(iban):
        return int(_number_iban(iban)) % 97 == 1

    def valid_country(iban):
        return _is_country(iban[:2])

    return valid_country(d) and generate_iban_check_digits(d) == d[2:4] and valid_iban(d)


def _matches(expression: str) -> callable:
    return lambda value: bool(re.fullmatch(expression, value))


# TODO after removing backward compatibility with old clients that do not
# send first name and last name, the `_proto_maybe` will have to be removed.
_SignupFirstName = \
    _proto_maybe(
        pycomb_combinators.subtype(
            pycomb_combinators.String,
            lambda d: len(d) < 300
        )
    )
_SignupLastName = \
    _proto_maybe(
        pycomb_combinators.subtype(
            pycomb_combinators.String,
            lambda d: len(d) < 300
        )
    )


def _is_transaction_bin(t: bytes) -> bool:
    if not isinstance(t, bytes):
        return False
    if len(t) > _MAX_TX_SIZE:
        return False
    # noinspection PyBroadException
    try:
        bitcoin.deserialize(t)
        return True
    except Exception:
        return False


_EMAIL_PATTERN = re.compile(
        r'^([a-zA-Z0-9\\.\-!#$%&\'*+/=?^_`{|}~]{1,256})@'
        r'([a-zA-Z0-9][a-zA-Z0-9\-_]{0,64}(\.[a-zA-Z0-9_][a-zA-Z0-9-_]{0,25})+)$'
)


def _is_email(e):
    result = _EMAIL_PATTERN.match(e) and len(e) <= 255

    if not result:
        _log('%s is NOT an email', e)
    return result


_Bytes = pycomb_combinators.irreducible(lambda d: type(d) == bytes, b'bytez', name='Bytes')
_TransactionBin = pycomb_combinators.subtype(_Bytes, _is_transaction_bin, name='TransactionBin')
_AskID = pycomb_combinators.subtype(pycomb_combinators.String, _matches(r'[A-F0-9]{32}'), name='AskID')
_BidID = pycomb_combinators.subtype(pycomb_combinators.String, _matches(r'[A-F0-9]{32}'), name='BidID')
_FiatAmount = pycomb_combinators.subtype(pycomb_combinators.Number, lambda d: 0 <= d < _MAX_FIAT, name='FiatAmount')
_Bip32PublicKey = pycomb_combinators.subtype(pycomb_combinators.String, _is_bip32, name='BIP32 Public Key')
_CryptoProof = pycomb_combinators.subtype(_unlimited_bytes, _lim_bytes, name='Crypto Proof')
_EncryptedMnemonic = pycomb_combinators.subtype(_unlimited_bytes, _lim_bytes, name='Encrypted Mnemonic')
_EncryptedSeed = pycomb_combinators.subtype(_unlimited_bytes, _lim_bytes, name='Encrypted Seed')
_EncryptedPrivateKey = pycomb_combinators.subtype(_unlimited_bytes, _lim_bytes, name='Encrypted Private Key')
_ProofExpiration = pycomb_combinators.subtype(
    pycomb_combinators.Int, lambda d: d > 1540913768456, name='_ProofExpiration'
)
_UUID = pycomb_combinators.subtype(pycomb_combinators.String, _is_uuid, name='UUID')
_UserLevel = pycomb_combinators.subtype(pycomb_combinators.String, lambda d: len(d) < 128, name='User Level')
_ExternalUserID = pycomb_combinators.subtype(pycomb_combinators.String, lambda d: len(d) < 128, name='External User ID')
_HashedConioPassword = pycomb_combinators.subtype(pycomb_combinators.String, _is_hex, name='Hashed Conio Password')
_MarketInfoValidInterval = pycomb_combinators.subtype(
    pycomb_combinators.Int, lambda d: d >= _MIN_INTERVAL_MARKET_INFO,
    'Market info valid interval'
)
_MarketInfoNoInterval = pycomb_combinators.constant(0, 'No market info inteval')
_Iban = pycomb_combinators.subtype(pycomb_combinators.String, _is_iban)
_Email = pycomb_combinators.subtype(pycomb_combinators.String, _is_email, name="email")

_MarketInfoInterval = pycomb_combinators.union(
    _MarketInfoValidInterval, _MarketInfoNoInterval,
    name='Market info interval'
)
_ExternalPaymentMethodReferenceId = pycomb_combinators.subtype(
    pycomb_combinators.String, lambda d: len(d) <= 128,
    name='External payment method reference ID'
)
_MaskedPAN = pycomb_combinators.subtype(
    pycomb_combinators.String, lambda d: len(d) <= 128,
    name='Masked PAN'
)

_ConioCredentialsCombinator = pycomb_combinators.generic_object(
    {
        'externalUserID': _ExternalUserID,
        'hashedConioPassword': _HashedConioPassword
    }, v1_pb2.ConioCredentials, name='Conio Credentials'
)
_SignupCryptoRequestCombinator = pycomb_combinators.generic_object(
    {
        'proofID': _UUID,
        'cryptoProof': _CryptoProof,
        'proofExpiration': _ProofExpiration,
        'externalUserID': _ExternalUserID,
        'userLevel': _UserLevel,
        'iban': _Iban,
        'email': _Email,
        'firstName': _SignupFirstName,
        'lastName': _SignupLastName,
    }, v1_pb2.SignupCryptoRequest, name='Signup Crypto Request'
)

# noinspection PyUnresolvedReferences
_AcceptanceType = pycomb_combinators.enum.of(ACCEPTANCE_TYPES_PB2_BY_NAME.keys())
_AcceptanceCombinator = \
    pycomb_combinators.generic_object(
        {
            'acceptance_type': _AcceptanceType,
            'acceptance_choice': pycomb_combinators.Int,
        },
        v1_pb2.Acceptance,
        name='Acceptance')
_TermsAndConditionsAcceptancesCombinator = \
    pycomb_combinators.generic_object(
        {
            'acceptances': pycomb_combinators.list(_AcceptanceCombinator)
        },
        v1_pb2.TermsAndConditionsAcceptances,
        name='Terms And Conditions Acceptances')
_EncryptedUserKeyCombinator = \
    pycomb_combinators.generic_object(
        {
            'encryptedMnemonic': _EncryptedMnemonic,
            'encryptedSeed': _EncryptedSeed,
            'encryptedPrivateKey': _EncryptedPrivateKey,
            'bip32PublicKey': _Bip32PublicKey
        },
        v1_pb2.EncryptedUserKey, name='Encrypted User Key'
    )

_LoginCombinator = pycomb_combinators.generic_object(
    {
        'conioCredentials': _ConioCredentialsCombinator
    }, v1_pb2.Login, name='Login'
)
_SignupCombinator = pycomb_combinators.generic_object(
    {
        'conioCredentials': _ConioCredentialsCombinator,
        'tc': _TermsAndConditionsAcceptancesCombinator,
        'cryptoRequest': _SignupCryptoRequestCombinator,
        'encryptedUserKey': _EncryptedUserKeyCombinator
    }, v1_pb2.Signup, name='Signup'
)
_MsgRequestBtcWithdrawalCombinator = pycomb_combinators.generic_object(
    {
        'dest_address': BitcoinAddress,
        'amount': Satoshi,
        'fee_per_byte': Satoshi
    },
    v1_pb2.MsgRequestBtcWithdrawal, name='MsgRequestBtcWithdrawal'
)

_DerEncodedSignature = pycomb_combinators.subtype(
    _limited_bytes(_DER_ENCODED_SIGNATURE_SIZE),
    _is_der_encoded_signature,
    name='DerEncodedSignature'
)

_SignatureCombinator = \
    pycomb_combinators.generic_object(
        {
            'path': AnyType,
            'signature': _DerEncodedSignature
        },
        v1_pb2.Signature,
        name='Signature'
    )


_MsgWithdrawBtcCombinator = pycomb_combinators.generic_object(
    {
        'tx': BtcRawTransaction,
        'date': UnixTimestamp,
        'request_signature': RSASignature,
        'tx_signatures': pycomb_combinators.list(_SignatureCombinator)
    },
    v1_pb2.MsgWithdrawBtc, name='MsgWithdrawBtc'
)

_MsgActivitiesInfoCombinator = pycomb_combinators.generic_object(
    {
        'from_index': PositiveInteger,
        'get_previous': AnyType,
        'activity_types': AnyType,
        'fiat': FiatCurrency
    },
    v1_pb2.MsgActivitiesInfo, name='MsgActivitiesInfo'
)

_MsgActivityDetailsCombinator = pycomb_combinators.generic_object(
    {
        'activity_id': ActivityID,
        'fiat_currency': FiatCurrency
    },
    v1_pb2.MsgActivityDetails, name='MsgActivityDetails'
)


TransactionSpeed = pycomb_combinators.subtype(pycomb_combinators.Int, lambda d: d > 0)
_MsgRequestBtcWithdrawalFeesCombinator = \
    pycomb_combinators.union(
        pycomb_combinators.generic_object(
            {
                'dest_address': bitcoin_address(),
                'amount': Satoshi,
                'speed': _proto_maybe(TransactionSpeed),
                'get_all_fees_info': pycomb_combinators.constant(False)
            },
            v1_pb2.MsgRequestBtcWithdrawalFees),
        pycomb_combinators.generic_object(
            {
                'dest_address': bitcoin_address(),
                'amount': Satoshi,
                'speed': pycomb_combinators.constant(v1_pb2.__TRANSACTION_SPEED_TYPE_DO_NOT_USE),
                'get_all_fees_info': pycomb_combinators.constant(True)
            },
            v1_pb2.MsgRequestBtcWithdrawalFees),
        name='MsgRequestBtcWithdrawalFees'
    )

_MsgGetCurrentPriceCombinator = pycomb_combinators.generic_object(
            {
                'currency': pycomb_combinators.Int,
                'satoshi_amount': _proto_maybe(Satoshi),
            },
            v1_pb2.MsgGetCurrentPrice,
            name='MsgGetCurrentPrice')


def _valid_history_price(d: v1_pb2.MsgHistoryPrices) -> bool:
    if d.end_timestamp <= d.start_timestamp:
        return False
    if d.interval != 0:
        return (d.end_timestamp - d.start_timestamp) / d.interval <= _MAX_NUM_PRICE_POINTS
    return True


_MsgHistoryPricesCombinator = \
    pycomb_combinators.subtype(
        pycomb_combinators.generic_object(
            {
                'currency': AnyType,
                'start_timestamp': _Timestamp,
                'end_timestamp': _Timestamp,
                'interval': _MarketInfoInterval
            },
            v1_pb2.MsgHistoryPrices),
        _valid_history_price,
        name='MsgHistoryPrices'
    )

_MsgCreateOrRefreshAskCombinator = \
    pycomb_combinators.generic_object(
        {
            'ask_id': _proto_maybe(_AskID),
            'currency': AnyType,
            'satoshi': _proto_maybe(Satoshi, constant_value=0),
            'fiat_amount': _proto_maybe(_FiatAmount, constant_value=0.0)
        },
        v1_pb2.MsgCreateOrRefreshAsk,
        name='MsgCreateOrRefreshAsk'
    )

_MsgPayForAskCombinator = \
    pycomb_combinators.generic_object(
        {
            'ask_id': _AskID,
        },
        v1_pb2.MsgPayForAsk,
        name='MsgPayForAsk'
    )


_MsgFinalizePaymentForAskCombinator = \
    pycomb_combinators.generic_object(
        {
            'ask_id': _AskID,
            'tx': _TransactionBin,
            'date': AnyType,
            'request_signature': RSASignature,
            # 'pin_code': _pincode,
            'signatures': pycomb_combinators.list(_SignatureCombinator)
        },
        v1_pb2.MsgFinalizePaymentForAsk,
        name='MsgFinalizePaymentForAsk'
    )


_MsgCreateOrRefreshBidCombinator = \
    pycomb_combinators.subtype(
        pycomb_combinators.generic_object(
            {
                'bid_id': _proto_maybe(_BidID),
                'currency': AnyType,
                'satoshi': _proto_maybe(Satoshi, constant_value=0),
                'fiat_amount': _proto_maybe(_FiatAmount, constant_value=0.0)
            },
            v1_pb2.MsgCreateOrRefreshBid,
        ),
        _either_field('satoshi', 'fiat_amount'),
        name='MsgCreateOrRefreshBid'
    )

_PayForBidCryptoRequestCombinator = \
    pycomb_combinators.generic_object(
        {
            'proofID': _UUID,
            'cryptoProof': _CryptoProof,
            'proofExpiration': _ProofExpiration,
            'external_reference_id': _ExternalPaymentMethodReferenceId,
        }, v1_pb2.PayForBidCryptoRequest, name='Pay for bid Crypto Request'
    )


_PayForBidWTCryptoRequestCombinator = \
    pycomb_combinators.generic_object(
        {
            'proofID': _UUID,
            'cryptoProof': _CryptoProof,
            'proofExpiration': _ProofExpiration
        }, v1_pb2.PayForBidWTCryptoRequest,
        name='Pay for bid using Wiretransfer Crypto Request'
    )

_MsgPayForBidCombinator = \
    pycomb_combinators.generic_object(
        {
            'bid_id': _BidID,
            'crypto_proof': _PayForBidCryptoRequestCombinator
        },
        v1_pb2.MsgPayForBid,
        name='MsgPayForBid'
    )


_MsgPayForBidUsingWiretransferCombinator = \
    pycomb_combinators.generic_object(
        {
            'bid_id': _BidID,
            'crypto_proof': _PayForBidWTCryptoRequestCombinator
        },
        v1_pb2.MsgPayForBidUsingWiretransfer,
        name='MsgPayForBidUsingWiretransfer'
    )

_MsgFinalizePaymentForBidCombinator = \
    pycomb_combinators.generic_object(
        {
            'bid_id': _BidID,
            'maskedpan': _proto_maybe(_MaskedPAN),
            'crypto_proof': _PayForBidCryptoRequestCombinator
        },
        v1_pb2.MsgFinalizePaymentForBid,
        name='Finalize payment for Bid'
    )


_MsgFinalizePaymentForBidUsingWiretransferCombinator = \
    pycomb_combinators.generic_object(
        {
            'bid_id': _BidID
        },
        v1_pb2.MsgFinalizePaymentForBidUsingWiretransfer,
        name='Finalize payment for Bid using wiretransfer'
    )

COMBINATORS = {
    v1_pb2.Login: _LoginCombinator,
    v1_pb2.Signup: _SignupCombinator,
    v1_pb2.MsgRequestBtcWithdrawal: _MsgRequestBtcWithdrawalCombinator,
    v1_pb2.MsgWithdrawBtc: _MsgWithdrawBtcCombinator,
    v1_pb2.MsgActivitiesInfo: _MsgActivitiesInfoCombinator,
    v1_pb2.MsgActivityDetails: _MsgActivityDetailsCombinator,
    v1_pb2.MsgRequestBtcWithdrawalFees: _MsgRequestBtcWithdrawalFeesCombinator,
    v1_pb2.MsgGetCurrentPrice: _MsgGetCurrentPriceCombinator,
    v1_pb2.MsgHistoryPrices: _MsgHistoryPricesCombinator,
    v1_pb2.MsgCreateOrRefreshAsk: _MsgCreateOrRefreshAskCombinator,
    v1_pb2.MsgPayForAsk: _MsgPayForAskCombinator,
    v1_pb2.MsgFinalizePaymentForAsk: _MsgFinalizePaymentForAskCombinator,
    v1_pb2.MsgCreateOrRefreshBid: _MsgCreateOrRefreshBidCombinator,
    v1_pb2.MsgPayForBid: _MsgPayForBidCombinator,
    v1_pb2.MsgFinalizePaymentForBid: _MsgFinalizePaymentForBidCombinator,
    v1_pb2.MsgPayForBidUsingWiretransfer: _MsgPayForBidUsingWiretransferCombinator,
    v1_pb2.MsgFinalizePaymentForBidUsingWiretransfer: _MsgFinalizePaymentForBidUsingWiretransferCombinator
}
