import base64
import os
import re

import bcrypt
import onetimepass
from jupyterhub.orm import Base
from sqlalchemy import Boolean
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import LargeBinary
from sqlalchemy import String
from sqlalchemy import UnicodeText 
from sqlalchemy.orm import validates


class UserInfo(Base):
    """
    This class represents the information that CognitoAuthenticator persists in
    JupyterHub's database.
    """

    __tablename__ = "users_info"
    id = Column(Integer, primary_key=True, autoincrement=True)

    # username should be a JupyterHub username, normalized by the Authenticator
    # class normalize_username function.
    username = Column(String(128), nullable=False)

    # password should be a bcrypt generated string that not only contains a
    # hashed password, but also the salt and cost that was used to hash the
    # password. Since bcrypt can extract the salt from this concatenation, this
    # can be used again during validation as salt.
    password = Column(LargeBinary, nullable=False)

    # is_authorized is a boolean to indicate if the user has been authorized,
    # either by an admin, or by validating via an email for example.
    is_authorized = Column(Boolean, default=False)

    # login_email_sent is boolean to indicate if a self approval email has been
    # sent out, as enabled by having a allow_self_approval_for configuration
    # set.
    login_email_sent = Column(Boolean, default=False)

    # email is a un-encrypted string representing the email
    email = Column(String(128))

    # has_2fa is a boolean that is being set to true if the user declares they
    # want to setup 2fa during sign-up.
    has_2fa = Column(Boolean, default=False)

    # otp_secret (one-time password secret) is given to a user during setup of
    # 2fa. With a shared secret like this, both the user and nativeauthenticator
    # are enabled to generate the same one-time password's, which enables them
    # to be matched against each other.
    otp_secret = Column(String(16))

    # Cognito Access Token
    access_token = Column(UnicodeText())

    # Cognito IdToken
    id_token = Column(UnicodeText())

    # Cognito Reflesh Token
    refresh_token = Column(UnicodeText())

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if not self.otp_secret:
            self.otp_secret = base64.b32encode(os.urandom(10)).decode("utf-8")

    @classmethod
    def find(cls, db, username):
        """
        Find a user info record by username.

        Returns None if no user was found.
        """
        return db.query(cls).filter(cls.username == username).first()

    @classmethod
    def all_users(cls, db):
        """
        Returns all available user info records.
        """
        return db.query(cls).all()

    @classmethod
    def change_authorization(cls, db, username):
        """
        Toggles the authorization status of a user info record.

        Returns the user info record.
        """
        user = db.query(cls).filter(cls.username == username).first()
        user.is_authorized = not user.is_authorized
        db.commit()
        return user

    def is_valid_password(self, password):
        """
        Checks if a provided password hashes to the hash we have stored in
        self.password.

        Note that self.password has been set to the return value of calling
        bcrypt.hashpw(...) before, that returns a concatenation of the random
        salt used and the hashed salt+password combination. So, when we are
        passing self.password back to bcrypt.hashpw(...) as a salt, it is smart
        enough to extract and use only the salt that was originally used.
        """
        return self.password == bcrypt.hashpw(password.encode(), self.password)

    @validates("email")
    def validate_email(self, key, address):
        """
        Validates any attempt to set the email field of a user info record.
        """
        if not address:
            return
        assert re.match(r"^[A-Za-z0-9\.\+_-]+@[A-Za-z0-9\._-]+\.[a-zA-Z]*$", address)
        return address

    def is_valid_token(self, token):
        """
        Validates a time-based one-time password (TOTP) as generated by a user's
        2fa application against the TOTP generated locally by the onetimepass
        module. Assuming the user generated a TOTP with a common shared one-time
        password secret (otp_secret), these passwords should match.
        """
        return onetimepass.valid_totp(token, self.otp_secret)
