import typing
from distutils.version import StrictVersion

import requests
from bithustler_client.client.client import BithustlerClient
from cards_client.client.client import CardsClient
from cards_client.generated_protobuf import base_pb2
from requests import Response, HTTPError
from requests.auth import HTTPBasicAuth
from vault_client.client.auth_client import VaultAuthClient
from vault_client.client.client_definition import VaultClient
from vault_client.client.conio_user import ConioUser
from vault_client.client.exceptions import DuplicateEntryException
from vault_client.client.support import userproto_to_dict
from vault_client.client.types import AcceptanceType, AcceptanceChoiceType, UserStatus, \
    TwoFactorAuthenticationMethodType

from conio_sdk.common import exceptions
from conio_sdk.common.decorators import xray_this
from conio_sdk.logging.factory import LOGGING_FACTORY
from conio_sdk.services.conio.user.user_service import UserService, UserCredentials, TermsAndConditionsAcceptances, \
    AuthenticationData, AccessToken, RefreshToken, TokenScope, AuthenticatedUser, ExternalUserReference, UserLanguage
from etc.settings import VaultAuthCredentials


class VaultService(UserService):
    def __init__(
            self, vault_client: VaultClient,
            vault_auth_client: VaultAuthClient,
            vault_auth_credentials: VaultAuthCredentials,
            cards_client: CardsClient, bithustler_client: BithustlerClient):
        self._bithustler_client = bithustler_client
        self._cards_client = cards_client
        self._vault_client = vault_client
        self._vault_auth_client = vault_auth_client
        self._vault_auth_credentials = vault_auth_credentials

    @xray_this
    def create_user_if_does_not_exist(
            self, credentials: UserCredentials, email: str,
            first_name: typing.Optional[str],
            last_name: typing.Optional[str]) -> ConioUser:
        user = self._vault_client.get_user_info_by_attributes(external_reference=credentials.external_user_reference)
        if user:
            LOGGING_FACTORY.user.warning(
                'User for external reference %s already exists. Replacing email %s with %s',
                credentials.external_user_reference,
                user.email, email
            )
            if email != user.email or \
                    not self._equal(user.first_name, first_name) or \
                    not self._equal(user.last_name, last_name):
                try:
                    self._vault_client.save_user_info_v2(
                        {
                            'first_name': first_name or '',
                            'last_name': last_name or '',
                            'email': email
                        },
                        user.reference_key_id)
                except DuplicateEntryException:
                    LOGGING_FACTORY.security.warning(
                        'Existing user %s attempted to signup using a different existing email %s',
                        credentials.external_user_reference,
                        user.email, email
                    )
                    raise exceptions.DuplicateEmailAddressException
            return ConioUser(data=userproto_to_dict(user))
        try:
            return self._create_new_user(
                credentials.external_user_reference,
                credentials.password,
                email,
                first_name, last_name
            )
        except DuplicateEntryException:
            LOGGING_FACTORY.security.warning(
                'New user %s attempted to signup using an existing email %s',
                credentials.external_user_reference, email
            )
            raise exceptions.DuplicateEmailAddressException

    @xray_this
    def create_user(
            self, email: str, password: str,
            device_id: str, app_version: StrictVersion, user_agent: str, device_language: str,
            acceptances: typing.Dict[AcceptanceType, AcceptanceChoiceType],
            explicit_fees_ids: typing.Sequence[str]
    ) -> ConioUser:
        safe_data = self._vault_client.create_user_v2(
            {
                'request_data': {
                    'app_version': str(app_version),
                    'user_agent': user_agent
                },
                'email': email,
                'password': password,
                'status': UserStatus.LOCKED.value,
                'lang': device_language,
                'device_data': {
                    'physical_device': device_id
                },  # purpose of device_data is to recovery an existing stashed_user in status RECOVERY
                'acceptances': acceptances,
                'explicit_fees_ids': explicit_fees_ids
            }
        )
        safe_data.pop('recovered')
        return ConioUser(data=safe_data)

    @xray_this
    def activate_user_if_not_yet_active(
            self, credentials: UserCredentials,
            terms_and_conditions: TermsAndConditionsAcceptances,
            user_language: typing.Optional[UserLanguage]) -> AuthenticatedUser:
        user = ConioUser(
            data=userproto_to_dict(self._vault_client.get_user_info_by_attributes(
                external_reference=credentials.external_user_reference)
            )
        )
        if not user.has_mfa_enabled:
            self._vault_auth_client.enable_2fa(
                user.reference_key_id, TwoFactorAuthenticationMethodType.MFA_EMAIL,
                force_activation=True
            )
        self._activate_user(user)
        # Before proceeding I want to be sure that, if the user already exists, he can authenticate
        authentication_data = self.external_user_login(credentials, user_language)
        self._ensure_trading_metadata(user)
        self._refresh_terms_and_conditions(terms_and_conditions, user)
        return AuthenticatedUser(authentication_data, user)

    @xray_this
    def external_user_login(self, credentials: UserCredentials, user_language: typing.Optional[UserLanguage]) \
            -> AuthenticationData:
        params = {
            'external_user_reference_id': credentials.external_user_reference,
            'password': credentials.password,
            'grant_type': 'external_user_password',
            'scope': 'profile'
        }

        auth = HTTPBasicAuth(
            self._vault_auth_credentials.username,
            self._vault_auth_credentials.password)
        headers = {}
        if user_language:
            headers['User-Language'] = user_language
        return self._process_token_request(auth, headers, params)

    @xray_this
    def user_login(
            self, username: str, password: str, hw_id: str,
            user_language: typing.Optional[UserLanguage]) -> AuthenticationData:
        params = {
            'username': username,
            'password': password,
            'hw_id': hw_id,
            'grant_type': 'password',
            'scope': 'profile'
        }

        auth = HTTPBasicAuth(
            self._vault_auth_credentials.username,
            self._vault_auth_credentials.password)
        headers = {}
        if user_language:
            headers['User-Language'] = user_language
        return self._process_token_request(auth, headers, params)

    @xray_this
    def escalate_token(self, access_token: str, hw_id: str) -> AuthenticationData:
        params = {
            'access_token': access_token,
            'hw_id': hw_id,
            'grant_type': 'exchange',
            'scope': 'profile'
        }

        auth = HTTPBasicAuth(
            self._vault_auth_credentials.username,
            self._vault_auth_credentials.password)
        headers = {}

        return self._process_token_request(auth, headers, params)

    def _process_token_request(self, auth, headers, params):
        res = requests.post(
            self._vault_auth_credentials.url + '/token',
            params=params,
            auth=auth,
            headers=headers)
        print("login output:", res.content)
        self._manage_vault_auth_errors(res)
        res = res.json()
        return AuthenticationData(
            AccessToken(res['access_token']),
            RefreshToken(res.get('refresh_token')),
            TokenScope(res['scope'])
        )

    @xray_this
    def get_user(self, external_user_reference: ExternalUserReference) -> ConioUser:
        user = self._vault_client.get_user_info_by_attributes(external_reference=external_user_reference)
        return ConioUser(data=userproto_to_dict(user)) if user else None

    @xray_this
    def refresh_session(self, refresh_token: str) -> AuthenticationData:
        params = {
            'grant_type': 'refresh_token',
            'scope': 'profile',
            'refresh_token': refresh_token
        }

        auth = HTTPBasicAuth(
            self._vault_auth_credentials.username,
            self._vault_auth_credentials.password)
        res = requests.post(self._vault_auth_credentials.url + '/token', params=params, auth=auth)
        self._manage_vault_auth_errors(res)
        res = res.json()

        return AuthenticationData(
            AccessToken(res['access_token']),
            RefreshToken(res.get('refresh_token')),
            TokenScope(res['scope']),
        )

    @xray_this
    def logout(self, access_token: str) -> None:
        res = requests.post(
            self._vault_auth_credentials.url + '/logout',
            headers={'Authorization': 'Bearer {}'.format(access_token)}
        )
        self._manage_vault_auth_errors(res)

    @classmethod
    def _equal(cls, s1: typing.Optional[str], s2: typing.Optional[str]) -> bool:
        if bool(s1) != bool(s2):
            return False
        if not bool(s1):
            return True
        return s1 == s2

    @classmethod
    def _bool2choice_type(cls, value: bool) -> AcceptanceChoiceType:
        return value and AcceptanceChoiceType.ACCEPTED or AcceptanceChoiceType.REJECTED

    def _create_terms_and_conditions(self, tc: TermsAndConditionsAcceptances) \
            -> typing.Dict[AcceptanceType, AcceptanceChoiceType]:
        return {
                AcceptanceType.MARKETING: self._bool2choice_type(tc.accepted_marketing),
                AcceptanceType.APP_IMPROVEMENT: self._bool2choice_type(tc.accepted_app_improvement),
                AcceptanceType.CLIENT_SUPPORT: self._bool2choice_type(tc.accepted_client_support),
            }

    def _refresh_terms_and_conditions(
            self, tc: TermsAndConditionsAcceptances, user: ConioUser):
        LOGGING_FACTORY.user.info('Refreshing terms and conditions for user %s: %s', user.reference_key_id, tc)
        self._vault_client.save_user_info_v2(
            {'acceptances': self._create_terms_and_conditions(tc)}, user.reference_key_id
        )

    def _create_new_user(
            self, external_reference: str, password: str, email: str,
            first_name: typing.Optional[str], last_name: typing.Optional[str]) -> ConioUser:
        LOGGING_FACTORY.user.info('Creating user for external reference %s', external_reference)
        self._vault_client.create_user_v2(
            {
                'external_references': [external_reference],
                'password': password,
                'email': email,
                'first_name': first_name or '',
                'last_name': last_name or ''
            }
        )
        data = userproto_to_dict(self._vault_client.get_user_info_by_attributes(external_reference=external_reference))
        user = ConioUser(data=data)
        LOGGING_FACTORY.user.info(
            'Created user for external reference %s, user id %s',
            external_reference, user.reference_key_id)
        return user

    def _activate_user(self, user: ConioUser):
        LOGGING_FACTORY.user.info('Activating user %s', user.reference_key_id)
        if not user.is_active and not user.is_disabled:
            self._vault_client.save_user_info_v2(
                {'status': UserStatus.ACTIVE.value}, reference_user_id=user.reference_key_id
            )
        else:
            LOGGING_FACTORY.user.info('User %s not locked or recovery, cannot activate', user.reference_key_id)

    def _ensure_trading_metadata(self, user: ConioUser):
        data_to_update = {}
        if not user.cards_integration_id:
            LOGGING_FACTORY.user.info('Creating payer for user %s', user.reference_key_id)
            req = base_pb2.MsgCreatePayer()
            payer_descr = user.reference_key_id
            req.payer_descr = payer_descr
            try:
                resp = self._cards_client.create_payer(req)
            except HTTPError as e:
                LOGGING_FACTORY.user.error(
                    'Could not interact with cards microservice for user %s',
                    user.reference_key_id)
                raise exceptions.CardsServiceCouldNotCreatePayerException(str(e.response.status_code))
            if resp.HasField('error'):
                LOGGING_FACTORY.user.error(
                    'Could not create payer for user %s: %s',
                    user.reference_key_id, str(resp))
                raise exceptions.CardsServiceCouldNotCreatePayerException(str(resp.error))
            data_to_update['cards_integration_id'] = resp.data.payer_id
        if not user.seller_id:
            LOGGING_FACTORY.user.info('Creating seller for user %s', user.reference_key_id)
            try:
                seller = self._bithustler_client.create_seller(user.reference_key_id)
            except HTTPError as e:
                LOGGING_FACTORY.user.error(
                    'Could not interact with bithustler microservice for user %s',
                    user.reference_key_id)
                raise exceptions.BithustlerServiceCouldNotCreateSellerException(str(e.response.status_code))
            data_to_update['seller_id'] = seller.id

        if data_to_update:
            LOGGING_FACTORY.user.info('Updating trading metadata for user %s', user.reference_key_id)
            self._vault_client.save_user_info_v2(data_to_update, user.reference_key_id)

    @classmethod
    def _manage_vault_auth_errors(cls, resp: Response) -> None:
        try:
            resp.raise_for_status()
        except HTTPError as e:
            if e.response.status_code in (401, 403):
                raise exceptions.NotAuthenticatedException
            if e.response.status_code == 400:
                raise exceptions.InvalidTokenException
            raise e
