"""
    flask_security.totp
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    Flask-Security TOTP (Timed-One-Time-Passwords) module

    :copyright: (c) 2019-2021 by J. Christopher Wagner (jwag).
    :license: MIT, see LICENSE for more details.
"""
import base64
import io

from passlib.totp import TOTP, TokenError


class Totp:
    """Encapsulate usage of Passlib TOTP functionality.

    Flask-Security doesn't implement any replay-attack protection out of the box
    as suggested by:
    https://passlib.readthedocs.io/en/stable/narr/totp-tutorial.html#match-verify

    Subclass this and implement the get/set last_counter methods. Your subclass can
    be registered at Flask-Security creation/initialization time.

    .. versionadded:: 3.4.0

    """

    def __init__(self, secrets, issuer):
        """Initialize a totp factory.
        secrets are used to encrypt the per-user totp_secret on disk.
        """
        # This should be a dict with at least one entry
        if not isinstance(secrets, dict) or len(secrets) < 1:
            raise ValueError("secrets needs to be a dict with at least one entry")
        self._totp = TOTP.using(issuer=issuer, secrets=secrets)

    def generate_totp_password(self, totp_secret):
        """Get time-based one-time password on the basis of given secret and time
        :param totp_secret: the unique shared secret of the user
        """
        return self._totp.from_source(totp_secret).generate().token

    def generate_totp_secret(self):
        """Create new user-unique totp_secret.

        We return an encrypted json string so that when sent in a cookie or
        sent to DB - it is encrypted.

        """
        return self._totp.new().to_json(encrypt=True)

    def verify_totp(self, token, totp_secret, user, window=0):
        """Verifies token for specific user.

        :param token: token to be check against user's secret
        :param totp_secret: the unique shared secret of the user
        :param user: User model
        :param window: optional. How far backward and forward in time to search
         for a match. Measured in seconds.
        :return: True if match
        """

        # TODO - in old implementation  using onetimepass window was described
        # as 'compensate for clock skew) and 'interval_length' would say how long
        # the token is good for.
        # In passlib - 'window' means how far back and forward to look and 'clock_skew'
        # is specifically for well, clock slew.
        try:
            tmatch = self._totp.verify(
                token,
                totp_secret,
                window=window,
                last_counter=self.get_last_counter(user),
            )
            self.set_last_counter(user, tmatch)
            return True

        except TokenError:
            return False

    def get_totp_uri(self, username, totp_secret):
        """Generate provisioning url for use with the qrcode
                scanner built into the app

        :param username: username/email of the current user
        :param totp_secret: a unique shared secret of the user
        """
        tp = self._totp.from_source(totp_secret)
        return tp.to_uri(username)

    def get_totp_pretty_key(self, totp_secret):
        """Generate pretty key for manual input

        :param totp_secret: a unique shared secret of the user

        .. versionadded:: 4.0.0
        """
        tp = self._totp.from_source(totp_secret)
        return tp.pretty_key()

    def fetch_setup_values(self, totp, user):
        """Generate various values user needs to setup authenticator app.
            Returns dict with keys:
                'key': totp key
                'image': image as string (useful for <img src=xx>)
                'username: qrcode best practice
                'issuer': qrcode best practice

        .. versionadded:: 4.0.0
        """

        r = dict()

        # By convention, the URI should have the username that the user
        # logs in with.
        username = user.calc_username() or "Unknown"
        r["username"] = username
        r["key"] = self.get_totp_pretty_key(totp)
        r["issuer"] = self._totp.issuer
        r["image"] = self.generate_qrcode(username, totp)
        return r

    def generate_qrcode(self, username, totp):
        """Generate QRcode
         Using username, totp, generate the actual QRcode image.
         This method can be overridden to fine-tune how the image is created -
         such as size, color etc.

         It must return a string suitable for use in an <img src=xx> tag.

        .. versionadded:: 4.0.0
        """
        try:
            import pyqrcode

            code = pyqrcode.create(self.get_totp_uri(username, totp))
            with io.BytesIO() as virtual_file:
                code.svg(file=virtual_file, scale=3)
                image_as_str = base64.b64encode(virtual_file.getvalue()).decode("ascii")

            return f"data:image/svg+xml;base64,{image_as_str}"
        except ImportError:  # pragma: no cover
            # This should have been checked at app init.
            raise

    def get_last_counter(self, user):
        """Implement this to fetch stored last_counter from cache.

        :param user: User model
        :return: last_counter as stored in set_last_counter()
        """
        return None

    def set_last_counter(self, user, tmatch):
        """Implement this to cache last_counter.

        :param user: User model
        :param tmatch: a TotpMatch as returned from totp.verify()
        """
        pass
