# Copyright(C) 2012-2020  Budget Insight
#
# This file is part of a woob module.
#
# This woob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This woob module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this woob module. If not, see <http://www.gnu.org/licenses/>.

# flake8: compatible

from woob.browser import URL, need_login
from woob.browser.exceptions import ClientError
from woob.exceptions import (
    BrowserIncorrectPassword, BrowserPasswordExpired, BrowserUnavailable,
    BrowserUserBanned, OfflineOTPQuestion, OTPSentType, SentOTPQuestion,
)
from woob.browser.mfa import TwoFactorBrowser
from woob.tools.capabilities.bank.transactions import sorted_transactions
from woob.tools.decorators import retry

from .pages import (
    AccountsPage, HistoryPage, HomePage, InvestPage, IsinPage,
    JsRedirectPage, LoginPage, MarketOrderDetailsPage,
    MarketOrdersPage, PasswordRenewalPage, PortfolioPage,
    SendOTPSMSPage, TwofaStatePage, ValidateOTPSMSPage, ValidateTOTPPage,
)


class BoursedirectBrowser(TwoFactorBrowser):
    BASEURL = 'https://www.boursedirect.fr'
    HAS_CREDENTIALS_ONLY = True

    login = URL(r'/hub/auth/login', LoginPage)
    login_twofa = URL(r'/hub/auth/login-2fa', LoginPage)
    twofa_state = URL(
        r'/hub/auth/two-factor/login/active-systems/boursedirect',
        TwofaStatePage,
    )
    validate_totp = URL(r'/hub/auth/two-factor/validate-totp', ValidateTOTPPage)
    send_otp_sms = URL(r'/hub/auth/two-factor/generate-and-send-otp', SendOTPSMSPage)
    validate_otp_sms = URL(r'/hub/auth/two-factor/validate-otp', ValidateOTPSMSPage)
    password_renewal = URL(r'/fr/changer-mon-mot-de-passe', PasswordRenewalPage)
    home = URL(r'/fr/page/inventaire', HomePage)
    accounts = URL(
        r'/priv/new/compte.php$',
        r'/priv/new/compte.php\?nc=(?P<nc>\d+)',
        r'/priv/listeContrats.php\?nc=(?P<nc>\d+)',
        AccountsPage
    )
    history = URL(r'/priv/new/historique-de-compte.php\?ong=3&nc=(?P<nc>\d+)', HistoryPage)
    portfolio = URL(r'/fr/page/portefeuille', PortfolioPage)
    pre_invests = URL(r'/priv/new/portefeuille-TR.php\?nc=(?P<nc>\d+)')
    invests = URL(r'/streaming/compteTempsReelCK.php\?stream=0', InvestPage)
    market_orders = URL(r'/priv/new/ordres-en-carnet.php\?ong=7&nc=(?P<nc>\d+)', MarketOrdersPage)
    market_orders_details = URL(r'/priv/new/detailOrdre.php', MarketOrderDetailsPage)
    isin_page = URL(r'/fr/marche/', IsinPage)
    js_redirect = URL(r'/priv/fiche-valeur.php', JsRedirectPage)

    __states__ = ('trusted_device', 'device_id')

    def __init__(self, config, *args, **kwargs):
        super().__init__(config, *args, **kwargs)

        self.device_id = None
        self.trusted_device = None
        # TOTP is generated by the user on the app
        # he choses, like Google Authenticator.
        self.totp = self.config['totp'].get()
        self.otp_sms = self.config['otp_sms'].get()

        # The website saves our browser information
        # so that 2FA is not requested on future connections.
        # Hardcode headers here to anticipate changes woob's default headers.
        self.session.headers.update({
            ('Accept-Language', 'en-US,en;q=0.5'),
            ('Accept-Encoding', 'gzip, deflate'),
            ('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
            ('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0'),
        })

        self.AUTHENTICATION_METHODS = {
            'totp': self.handle_totp,
            'otp_sms': self.handle_otp_sms,
        }

    def get_login_data(self):
        return {
            'username': self.username,
            'password': self.password,
            'branding': 'boursedirect',
            'redirect': None,
        }

    @retry(BrowserUnavailable, tries=2)
    def init_login(self):
        try:
            # This request will work only in two cases:
            # - the user hasn't activated his 2FA.
            # - the user has done it's 2FA and added the device as trusted.
            # 2FA is systematic if the deivce is not trusted.
            if self.device_id and not self.session.cookies.get('device_id'):
                # If we already did the 2FA but have to login again,
                # and if the cookies can't be loaded for any reason
                # (expired for example), then recreate the device_id
                # cookie from the device_id kept in the state to avoid
                # triggering the 2FA again.
                self.session.cookies.set(
                    'device_id',
                    self.device_id,
                    domain='www.boursedirect.fr',
                )
            self.login.go(
                json=self.get_login_data(),
                # If the origin header is not set, website will answer with a server error 500.
                headers={'Origin': 'https://www.boursedirect.fr'},
            )
        except ClientError as e:
            if e.response.status_code == 401:
                login_page = LoginPage(self, e.response)
                error_message = login_page.get_error_401_message()
                if error_message in ('bad_credentials_error', 'error_bad_credentials'):
                    raise BrowserIncorrectPassword(
                        message='Couple login mot de passe incorrect',
                        bad_fields=['login'],
                    )
                elif error_message == 'error_password_not_found':
                    raise BrowserIncorrectPassword(
                        message='Couple login mot de passe incorrect',
                        bad_fields=['password'],
                    )
                elif error_message == 'error_locked_user':
                    raise BrowserUserBanned()
                elif error_message == 'error_expired_password':
                    raise BrowserPasswordExpired(
                        locale='fr-FR', message='Vous devez changer votre mot de passe depuis le site web.'
                    )
                raise AssertionError(f'Unhandled error during login: {error_message}')

            elif e.response.status_code == 403:
                self.page = LoginPage(self, e.response)
                error_message = self.page.get_error_403_message()
                if error_message == '2FA required':
                    self.check_interactive()
                    self.twofa_state.go()
                    # If the 2FA is validated, this device_id will be valid for about 13 months.
                    # Keep it in the state because, if for any reason cookies can't be loaded, this
                    # device_id will allow us to directly login without 2FA. When it won't be valid
                    # anymore in ~13 months, trying to login with it will trigger a response 403
                    # and ask the 2FA again.
                    self.device_id = self.session.cookies['device_id']
                    self.trusted_device = self.page.is_device_trusted()

                    if self.page.is_totp_twofa():
                        raise OfflineOTPQuestion(
                            field_name='totp',
                            message="Saisissez le code généré par l'application d'authentification que vous avez paramétrée.",
                        )
                    else:
                        medium_label = self.page.get_mobile_number()
                        self.send_otp_sms.go(json={'branding': 'boursedirect'})  # OTP sent here
                        raise SentOTPQuestion(
                            field_name='otp_sms',
                            medium_type=OTPSentType.SMS,
                            medium_label=medium_label,
                            message='Entrez le code reçu par SMS.',
                        )
                raise AssertionError(f'Unhandled error during login: {error_message}')
            raise

    def do_twofa_validation(self, twofa_code, twofa_validation_url):
        if not self.trusted_device:
            self.trusted_device = True
        data = {
            'code': twofa_code,
            'branding': 'boursedirect',
            'trusted_device': self.trusted_device,
        }
        try:
            twofa_validation_url.go(json=data)
        except ClientError as e:
            if e.response.status_code == 403:
                self.page = twofa_validation_url.klass(self, e.response)
                error_message = self.page.get_error_message()
                if 'provided code is not valid' in error_message:
                    raise BrowserIncorrectPassword(message='Code invalide, merci de réessayer.')
                raise AssertionError(f'Unhandled error during 2FA validation: {error_message}')
            raise

        self.login_twofa.go(json=self.get_login_data())

    def handle_totp(self):
        self.do_twofa_validation(self.totp, self.validate_totp)

    def handle_otp_sms(self):
        self.do_twofa_validation(self.otp_sms, self.validate_otp_sms)

    @need_login
    def iter_accounts(self):
        self.accounts.go()
        for account in self.page.iter_accounts():
            self.accounts.go(nc=account._select)
            self.page.fill_account(obj=account)
            yield account

    @need_login
    def iter_investment(self, account):
        self.pre_invests.go(nc=account._select)
        self.invests.go()

        for inv in self.page.iter_investment():
            yield inv
        yield self.page.get_liquidity()

    @need_login
    def iter_market_orders(self, account):
        if account.type not in (account.TYPE_PEA, account.TYPE_MARKET):
            return

        self.market_orders.go(nc=account._select)
        for order in self.page.iter_market_orders():
            if order.url:
                self.location(order.url)
                if self.market_orders_details.is_here():
                    self.page.fill_market_order(obj=order)
                else:
                    self.logger.warning('Unknown details URL for market order %s.', order.label)
            else:
                self.logger.warning('Market order %s has no details URL.', order.label)
            yield order

    @need_login
    def iter_history(self, account):
        if account.type in (account.TYPE_MARKET, account.TYPE_PEA):
            self.history.go(nc=account._select)
        else:
            raise NotImplementedError()
        return sorted_transactions(self.page.iter_history())
