"""
Subclass of the BaseCredContainer used for reading secrets from bitwarden password manager.
This class wraps the bitwarden CLI.  See:  https://bitwarden.com/help/article/cli/#using-an-api-key
Note that only the Enterprise version of bitwarden can (supported) hit the REST API.  
In contrast, the API key that can be found under the "My Account" page can be used to log into the cli tool

"""

from ctypes import resize
from flat_file import FlatFileCredContainer
from base_cred_container import CredContainerBase
import json
#TODO:  Drop this:  import requests
#TODO:  Drop this:  import hashlib
#TODO:  Drop this:  import base64
import getpass
import os
import subprocess
import uuid
from shutil import which

API_KEY_FLAT_FILE='/.credentials/bw_api.json'

def make_bitwarden_container(api_key_flat_file:str = None):
    """
    Factory function to return a BitwardeCredContainer object, instantiated using data
    read from a flat file.See 'View API Key' button at https://vault.bitwarden.com/#/settings/account

    Args:
        api_key_flat_file (str, optional): The flat file that contains the API details. If not provided, defaults to API_KEY_FLAT_FILE

    Returns:
        BitwardenCredContainer
    """

    # Read the contents of the flat file
    if api_key_flat_file is None:
        api_key_flat_file = API_KEY_FLAT_FILE
    
    file_cred_obj = FlatFileCredContainer(file_path=api_key_flat_file, allow_broad_permissions=False) # This ss very stubborn about reading a file that isn't locked down properly
    file_contents = file_cred_obj.read()
    j = json.loads(file_contents)

    o = BitwardenCredContainer(**j)
    return o

#TODO:  Drop this
# def prompt_for_credentials(email:str = None, password: str = None) -> str:
#     """
#     A wrapper function around hash_password that will interactively prompt the use for the (non-hashed) password in order to come up with the right digest.
#     This behavior allows a user to not specify the hashed password in the config flat file and be prompted for it instead
#     Args:
#         email (string, optional): The email address. If not passed, the user will be prompted
#         password (string, optional): The password. If not passed (intended behavior for this function), the user will be prompted

#     Returns:
#         string: The hashed password 
#     """

#     if not email:
#         email = input("Bitwarden account email address: ")

#     if not password:
#         password = getpass.getpass(f"{email} account password: ")

#     ret_val = hash_password(email=email, password=password)

#     return ret_val

class BitwardenCredContainer(CredContainerBase):
    """
    A credential container for interacting with bitwarden

    Args:
        CredContainerBase ([type]): [description]
    """

    def __init__(self, client_id:str = None, client_secret:str = None, session_key:str = None, **kwargs) -> None:
        """
        Init method for the BitwardenCredContainer

        Args:
            client_id (string): Username (email address)
            client_secret (string): Password (Hashed, as would be treutnred by the has_password function)
            
            session_key (string):  If passed, should correspond with a currently valid sesison key that corresponds with the '--session' 
                for any command and/or the BW_SESSION environment variable.  Ultimately, this is the value we're after for any subsequent
                interactions with the cli.  Thus, if supplied (and valid) this is really the only arg we need
        """

        # We won't get far at all if the bw tool isn't installed.
        which_bw = which('bw')
        if which_bw is None:
            raise FileNotFoundError(f"This program wraps the bitwarden cli tool, 'bw', but it doesn't seem to be installed (or is not on PATH).  Please fix that and try again.  See:  https://bitwarden.com/help/article/cli/")

        # Pin client id and client secret to self
        self.client_id = client_id
        self.client_secret = client_secret
        self.session_key = session_key

        # Just for a stab in the dark , see if BW_SESSION is set and if so, set the value to self.session_key
        # If it's invalid, it's not a big deal because get_auth_status (which wraps get_bitwarden_status) will return 'locked'
        if 'BW_SESSION' in os.environ:
            self.session_key = os.getenv('BW_SESSION')

        # Do validations
        if session_key is None:
            # Then we've got to have the client id and secret
            if self.client_id is None or self.client_secret is None:
                raise ValueError(f"If not instantiating with a session key, client_id and client_secret arguments must be supplied")
        
        # Pin other arbitrary stuff to self
        for k in kwargs.keys():
            if not hasattr(self, k):
                setattr(self, k, kwargs[k])

        # Set environment variables that the BW CLI looks for to skip prompt for credentials
        os.environ['BW_CLIENTID'] = self.client_id
        os.environ['BW_CLIENTSECRET'] = self.client_secret

        # Get context about email address
        if not hasattr(self, 'email_address'):
            self.email_address = input("Bitwarden account email: ")
            print("If you instantiated via a JSON config file, you can avoid this message in the future by adding the key 'email_address'")

        # Do the login flow.  This will ultimately pin the value for self.session_key if we didn't have a valid one already
        if self.get_auth_status() != 'unlocked':
            self.do_auth_and_unlock_flow()

        # At this point we should be unlocked for sure.  If not, we've failed miserably
        if self.get_auth_status() != 'unlocked':
            raise ValueError(f"The bitwarden vault should be unlocked now using the session key but it still isn't.  Something bad happened.  There might be a bug in this program.  Please troubleshoot.\nSession Key: {self.session_key}")


    def do_auth_and_unlock_flow(self):
        """
        Gently guides us through the necessary steps to get the vault into an unlocked state
        We need to go from 'unauthenticated' --> 'locked' --> 'unlocked'
        """

        auth_status = self.get_auth_status()

        # Bail out if we're already unlocked
        if auth_status == 'unlocked':
            return
        
        # We've got some auth and/or unlocking to do. Put the password into a randomly named environment variable
        rand_variable_name = str(uuid.uuid1()).upper()
        os.environ[rand_variable_name] = getpass.getpass("Bitwarden Master Password: ")

        try:
            while auth_status != 'unlocked':
                
                auth_status = self.get_auth_status()
                
                if auth_status == 'unauthenticated':
                    # Let's get authenticated
                    self.log_in(password_environemt_variable=rand_variable_name)

                elif auth_status == 'locked':
                    # We are authenticated (That is, bitwarden is pointing to our account), but the vault is locked
                    self.unlock(password_environemt_variable=rand_variable_name) # This method pins session_key to self
                elif auth_status == 'unlocked':
                    # We are authenticated and the vault is unlocked.  We can interact with it now
                    print("The vault is now unlocked.")
                    break
                else:
                    raise ValueError(f"There is no handling for the status '{auth_status}'")
        finally:
            del os.environ[rand_variable_name] # Implicitly calls unsetenv


    def log_in(self, password_environemt_variable:str):
        """
        Walks is through the login process.  For deets, see 'bw login --help'

        Args:
            password_environemt_variable (string): The name of an environment variable which contains our master password
        """

        client_secret = self.client_secret
        email = self.email_address

        # Now log in and point to the environment variable
        print ("Logging into Bitwarden...")
        cmd = f"bw login {self.email_address} --passwordenv {password_environemt_variable} --apikey {self.client_secret}"
        result = subprocess.run(cmd, shell=True, capture_output=True)

    def unlock(self, password_environemt_variable:str):
        """
        Unlocks the vault after having previously logged in.  This action returns a session key

        Args:
            password_environemt_variable (string): The name of an environment variable which contains our master password
        """
        
        print ("Unlocking Bitwarden Vault...")
        cmd = f"bw unlock --passwordenv {password_environemt_variable} --raw"  #The raw flag simply prints the session key that we should use for subsequent requests
        result = subprocess.run(cmd, shell=True, capture_output=True)

        if result.returncode == 0:
            session_key = result.stdout.decode('utf-8')
            self.session_key = session_key  #This can be set in the env var BW_SESSION or passed with a '--session' argument with any bw command    



    def get_bitwarden_status(self):
        """
        Issues the 'bitwarden status' command, which returns a JSON object we can use to tell if we're logged in or not
        """

        # Do we already have a session key?
        if self.session_key is not None and self.session_key != '':
            session_key_part = f" --session '{self.session_key}'"
        else:
            session_key_part = ""

        cmd = f"bw status{session_key_part}"
        result = subprocess.run(cmd, shell=True, capture_output=True)

        if result.returncode != 0:
            raise OSError(f"The command '{cmd}' resulted in return code of {result.returncode}:\n{result.stderr.decode('utf-8')}")
        else:
            ret_val = result.stdout.decode('utf-8')
    
        return json.loads(ret_val)

    def get_auth_status(self):
        """
        Returns the authentication status which according to 'bw status --help' should be one of these:
            "unauthenticated", "locked", "unlocked"
        """

        return self.get_bitwarden_status()['status']
        

    def get_session_key(self):
        """
        Issues the command 'bw login --raw' which causes authentication to happen and returns a session key to be used for subsequent requests
        """

        # Get the status
        self.get_bitwarden_status()

        command = "bw login --raw"
        result = subprocess.run(command, shell=True, capture_output=True)

        print(f"Instantiated {type(self)} for username (email address) {self.username}")
        

    def get_cred(self):
        return super().get_cred()

    def set_cred(self):
        return super().set_cred()
    
    def delete_cred(self):
        return super().delete_cred()


if __name__ == '__main__':
    #TODO:  Really need to unit test this entire module thoroughly.  Write test cases


    o = make_bitwarden_container()


    # test = hash_password(password="p4ssw0rd", email="nobody@example.com")
    # print(test)

    # print(prompt_for_credentials())