import base64
import traceback
from distutils.version import StrictVersion

from conio_sdk.common import exceptions
from conio_sdk.services.conio.sdk.versions import IBAN_BIDS
from etc import settings
# noinspection PyUnresolvedReferences
from conio_sdk.ioc.ioc_vault import vault_client
from conio_sdk.combinators.v1 import COMBINATORS

try:
    if settings.AWS_TRACING:
        from aws_xray_sdk.core import patch
        libraries = ('requests',)
        patch(libraries)

    import typing

    from aws_lambda_tools.common.decorators import handle_session, aws_encode, protobuf_encode, \
    populate_user_from_event, wrap_in_response, mfa_protected, EXTRACT_BODY_TOKEN_PAYLOAD, verify_signature, \
    extract_url_parameter
    from aws_lambda_tools.common.event import get_authorization_token
    from aws_lambda_tools.common.response import Response, ResponseMimeType

    from conio_sdk.common.decorators import manage_errors, log_this
    from conio_sdk.context import AWS_CONTEXT
    from conio_sdk.generated_protobuf import v1_pb2
    from conio_sdk.services.conio.sdk.signup_vo_service import AuthenticationData
    from conio_sdk.logging.factory import LOGGING_FACTORY
except Exception:
    traceback.print_exc()

    # Intentionally imported locally
    from conio_sdk.logging.factory import LOGGING_FACTORY
    LOGGING_FACTORY.entry_point.exception('Could not instantiate entry v1 point')
    raise


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@aws_encode()
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.Login)
@manage_errors
def login(msg: v1_pb2.Login, event: typing.Dict, context=None) -> Response:
    from conio_sdk.ioc.vo.login_ioc import login_vo_services
    response, authentication_data = login_vo_services.external_user_login(msg, _user_language(context))
    return _auth2resp(authentication_data, message=response)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@aws_encode()
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.Signup)
@manage_errors
def signup(msg: v1_pb2.Signup, event: typing.Dict, context=None) -> Response:
    from conio_sdk.ioc.vo.signup_ioc import signup_vo_services
    response, authentication_data = signup_vo_services.signup(msg, _user_language(context))
    return _auth2resp(authentication_data, message=response)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@manage_errors
def refresh_token(event: typing.Dict, context=None) -> Response:
    from conio_sdk.ioc.vo.refresh_token_ioc import refresh_token_vo_services
    token = get_authorization_token(AWS_CONTEXT)
    return _auth2resp(refresh_token_vo_services.refresh_token(token))


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@manage_errors
def logout(event: typing.Dict, context=None):
    from conio_sdk.ioc.vo.logout_ioc import logout_vo_services
    token = get_authorization_token(AWS_CONTEXT)
    token and logout_vo_services.logout(token)
    return ''


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS)
@manage_errors
def get_wallet_info(event: typing.Dict, context=None):
    from conio_sdk.ioc.vo import wallet_info_ioc
    return wallet_info_ioc.wallet_info_vo_services.get_wallet_info(context.session_strategy.user)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS)
@manage_errors
def get_current_btc_address(event: typing.Dict, context=None):
    from conio_sdk.ioc.vo import wallet_info_ioc
    return wallet_info_ioc.wallet_info_vo_services.get_current_btc_address(context.session_strategy.user)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgWithdrawBtc)
@manage_errors
def withdraw_btc(msg: v1_pb2.MsgWithdrawBtc, event: typing.Dict, context=None)\
        -> v1_pb2.MsgWithdrawBtcResponse:
    from conio_sdk.ioc.vo import btc_transactions_ioc
    return btc_transactions_ioc.btc_transactions_service.withdraw_btc(
        msg, context.session_strategy.user
    )


@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@mfa_protected(AWS_CONTEXT, extract_token_payload_fn=EXTRACT_BODY_TOKEN_PAYLOAD)
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgRequestBtcWithdrawal)
@manage_errors
def request_btc_withdrawal(msg: v1_pb2.MsgRequestBtcWithdrawal, event: typing.Dict, context=None):
    # noinspection PyBroadException
    try:
        msg_before_token = v1_pb2.MsgRequestBtcWithdrawal()
        msg_before_token.ParseFromString(base64.b64decode(AWS_CONTEXT.session_strategy.mfa_payload))
        valid_mfa = msg_before_token == msg
    except Exception:
        LOGGING_FACTORY.security.exception('Could not parse MFA payload %s', AWS_CONTEXT.session_strategy.mfa_payload)
        valid_mfa = False

    if not valid_mfa:
        LOGGING_FACTORY.security.warning('Invalid Token Payload %s', AWS_CONTEXT.session_strategy.mfa_payload)
        raise exceptions.InvalidTokenPayloadException

    from conio_sdk.ioc.vo import btc_transactions_ioc
    return btc_transactions_ioc.btc_transactions_service.request_btc_withdrawal(
        msg, context.session_strategy.user
    )
# noinspection PyUnusedLocal


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgActivitiesInfo)
@manage_errors
def get_activities(msg: v1_pb2.MsgActivitiesInfo, event: typing.Dict, context=None)\
        -> v1_pb2.MsgActivitiesInfoResponse:
    from conio_sdk.ioc.vo import activity_list_ioc
    return activity_list_ioc.activity_list_service.get_activities(
        msg, context.session_strategy.user
    )


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgActivityDetails)
@manage_errors
def get_activity(msg: v1_pb2.MsgActivityDetails, event: typing.Dict, context=None)\
        -> v1_pb2.Activity:
    from conio_sdk.ioc.vo import activity_details_ioc
    return activity_details_ioc.activity_details_service.get_activity_details(
        msg, context.session_strategy.user
    )


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgRequestBtcWithdrawalFees)
@manage_errors
def get_btc_withdrawal_fees(msg: v1_pb2.MsgRequestBtcWithdrawalFees, event: typing.Dict, context=None)\
        -> v1_pb2.MsgRequestBtcWithdrawalFeesResponse:
    from conio_sdk.ioc.vo import btc_transactions_ioc
    result = btc_transactions_ioc.btc_transactions_service.get_btc_withdrawal_fees(
        msg, context.session_strategy.user
    )
    return result


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgGetCurrentPrice)
@manage_errors
def get_current_price(msg: v1_pb2.MsgGetCurrentPrice, event: typing.Dict, context=None)\
        -> v1_pb2.PricePoint:
    from conio_sdk.ioc.vo import price_ioc
    return price_ioc.price_vo_service.get_current_price(msg)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgHistoryPrices)
@manage_errors
def get_historical_prices(msg: v1_pb2.MsgHistoryPrices, event: typing.Dict, context=None)\
        -> v1_pb2.MsgHistoryPricesResponse:
    from conio_sdk.ioc.vo import price_ioc
    return price_ioc.price_vo_service.get_historical_prices(msg, )


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgCreateOrRefreshAsk)
@manage_errors
def create_or_refresh_ask(msg: v1_pb2.MsgCreateOrRefreshAsk, event: typing.Dict, context=None)\
        -> v1_pb2.MsgCreateOrRefreshAskResponse:
    from conio_sdk.ioc.vo import trading_ioc
    return trading_ioc.trading_vo_services.create_or_refresh_ask(msg, context.session_strategy.user)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgPayForAsk)
@manage_errors
def pay_for_ask(msg: v1_pb2.MsgPayForAsk, event: typing.Dict, context=None)\
        -> v1_pb2.MsgPayForAskResponse:
    from conio_sdk.ioc.vo import trading_ioc
    return trading_ioc.trading_vo_services.pay_for_ask(msg, context.session_strategy.user)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgFinalizePaymentForAsk)
@manage_errors
def finalize_payment_for_ask(msg: v1_pb2.MsgFinalizePaymentForAsk, event: typing.Dict, context=None)\
        -> v1_pb2.MsgFinalizePaymentForAskResponse:
    from conio_sdk.ioc.vo import trading_ioc
    return trading_ioc.trading_vo_services.finalize_payment_for_ask(msg)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgCreateOrRefreshBid)
@manage_errors
def create_or_refresh_bid(msg: v1_pb2.MsgCreateOrRefreshBid, event: typing.Dict, context=None)\
        -> v1_pb2.MsgCreateOrRefreshBidResponse:
    from conio_sdk.ioc.vo import trading_ioc
    return trading_ioc.trading_vo_services.create_or_refresh_bid(
        msg, context.session_strategy.user,
        use_iban=_use_iban_as_payment_method(context)
    )


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgPayForBid)
@manage_errors
def pay_for_bid(msg: v1_pb2.MsgPayForBid, event: typing.Dict, context=None) -> v1_pb2.MsgPayForBidResponse:
    if _use_iban_as_payment_method(context):
        raise exceptions.SDKUpgradeRequestException
    from conio_sdk.ioc.vo import trading_ioc
    return trading_ioc.trading_vo_services.pay_for_bid(msg, context.session_strategy.user)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgPayForBidUsingWiretransfer)
@manage_errors
def pay_for_bid_using_wiretransfers(msg: v1_pb2.MsgPayForBidUsingWiretransfer, event: typing.Dict, context=None) \
        -> v1_pb2.MsgPayForBidUsingWiretransferResponse:
    from conio_sdk.ioc.vo import trading_ioc
    return trading_ioc.trading_vo_services.pay_for_bid_using_wiretransfers(msg, context.session_strategy.user)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgFinalizePaymentForBid)
@manage_errors
def finalize_bid(msg: v1_pb2.MsgFinalizePaymentForBid, event: typing.Dict, context=None) \
        -> v1_pb2.MsgFinalizePaymentForBidResponse:
    if _use_iban_as_payment_method(context):
        raise exceptions.SDKUpgradeRequestException
    from conio_sdk.ioc.vo import trading_ioc
    return trading_ioc.trading_vo_services.finalize_bid(msg, context.session_strategy.user)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS, expected_request_type=v1_pb2.MsgFinalizePaymentForBidUsingWiretransfer)
@manage_errors
def finalize_bid_using_wiretransfer(
        msg: v1_pb2.MsgFinalizePaymentForBidUsingWiretransfer,
        event: typing.Dict, context=None) -> v1_pb2.MsgFinalizePaymentForBidUsingWiretransferResponse:
    from conio_sdk.ioc.vo import trading_ioc
    return trading_ioc.trading_vo_services.finalize_bid_using_wiretransfer(msg)


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(COMBINATORS)
@manage_errors
def get_trading_limits(event: typing.Dict, context=None) \
        -> v1_pb2.MsgGetTradingLimitsResponse:
    from conio_sdk.ioc.vo import trading_limits_ioc
    currency = event['queryStringParameters']
    if currency:
        currency = currency.get('currency')
    return trading_limits_ioc.trading_limits_vo_service.get_trading_limits(
        currency or 'EUR',
        context.session_strategy.user,
        _use_iban_as_payment_method(context)
    )


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@populate_user_from_event(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@manage_errors
def test(event: typing.Dict, context=None):
    return ''


# noinspection PyUnusedLocal
@log_this
@handle_session(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@manage_errors
def test_no_auth(event: typing.Dict, context=None):
    return ''


@log_this
@handle_session(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@manage_errors
def run_python_code(event: typing.Dict, context, data):
    eval(compile(data, filename='/tmp/run_python_code.py', mode='exec'))
    return b''


def _auth2resp(authentication_data: AuthenticationData, message=None) -> Response:
    response = Response(
        content=message.SerializeToString() if message else b'',
        content_type=ResponseMimeType.PROTOBUF if message else ResponseMimeType.TEXT
    )
    response.add_headers(
        {
            'X-ConioAccessToken': authentication_data.access_token,
            'X-ConioRefreshToken': authentication_data.refresh_token,
            'X-ConioTokenScope': authentication_data.current_scope
        }
    )
    return response


@log_this
@handle_session(AWS_CONTEXT)
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
@protobuf_encode(None)
@manage_errors
def get_terms_and_conditions_and_privacy_policy(*_) -> v1_pb2.TermsAndConditionsAcceptances:
    from conio_sdk.ioc.vo.tc_ioc import terms_conditions_privacy_policies_vo_services
    return terms_conditions_privacy_policies_vo_services.get_terms_and_conditions_and_privacy_policy()


@extract_url_parameter('wallet_reference_id')
@aws_encode()
@wrap_in_response(ResponseMimeType.TEXT)
def notify_wallet_info(_, __, wallet_reference_id: str):
    return ''


def _user_language(context) -> typing.Optional[str]:
    return context.headers_wrapper.get('User-Language')


def _use_iban_as_payment_method(context) -> bool:
    protocol_version = context.headers_wrapper.get('versioncode')
    if protocol_version:
        try:
            current_version = StrictVersion(protocol_version)
        except ValueError:
            # Here we are for both of the following versions:
            # iOS: "1", which is not compliant with StrictVersion
            # Android: "application/octet-stream, text/plain, application/json", which
            #          is not compliant with any known versioning schemas
            return False
        return current_version >= IBAN_BIDS
    return False
