import logging
import json
import os
import subprocess
from colorama import just_fix_windows_console
from colorama import Fore
from uuid import UUID

just_fix_windows_console()


class Vault:
    PASSWORD_FILE: str = os.path.join(os.path.expanduser("~"), ".config", "bw_secrets", "pass.key")
    ENV: os._Environ

    def __init__(self, password_file: str | None = None):
        if password_file:
            self.PASSWORD_FILE = password_file

    def get_vault_status(self) -> str | None:
        """
        retrieve vault status from the cli program.

        the status can be a string of either `locked`, `unlocked`, or `unauthenticated`

        if `None` is returned then the subprocessed failed to retrieve the status.
        logging is done in this function.
        """

        status_cmd = ["bw", "status", "--raw"]
        try:
            output = subprocess.check_output(status_cmd)
        except subprocess.CalledProcessError as e:
            logging.error(
                f"{Fore.RED}error while getting vault status, exited with code: {e.returncode}{Fore.RESET}")
            error_output = e.output
            if isinstance(error_output, bytes):
                error_output = error_output.decode()
            logging.debug(error_output)
            return None
        json_output = json.loads(output.decode())
        return json_output["status"]

    def unlock_vault(self) -> bool:
        """
        unlocks the vault using the cli program.

        returns `True` if successful, `False` if not.
        """

        unlock_cmd = ["bw", "unlock", "--raw",
                      "--passwordfile", self.PASSWORD_FILE]
        try:
            output = subprocess.check_output(unlock_cmd)
        except subprocess.CalledProcessError as e:
            logging.error(
                f"{Fore.RED}error while unlocking vault, exited with status code: {e.returncode}{Fore.RESET}")
            error_output = e.output
            if isinstance(error_output, bytes):
                error_output = error_output.decode()
            logging.debug(error_output)
            return False

        # get the session id, inject it into a copy of the current environment,
        # and save for later use
        session_key = output.decode()
        self.ENV = os.environ
        self.ENV["BW_SESSION"] = session_key
        return True

    def lock_vault(self) -> bool:
        """
        locks the vault using the cli program.

        returns `True` if successful, `False` if not.
        """

        lock_cmd = ["bw", "lock", "--raw"]
        try:
            subprocess.check_output(lock_cmd)
        except subprocess.CalledProcessError as e:
            logging.error(
                f"{Fore.RED}error while locking vault, exited with status code: {e.returncode}{Fore.RESET}")
            error_output = e.output
            if isinstance(error_output, bytes):
                error_output = error_output.decode()
            logging.debug(error_output)
            return False
        return True

    def sync_vault(self) -> bool:
        """
        calls the cli program to sync the vault from the remote server.

        returns `True` if successful, `False` if not.
        """

        sync_cmd = ["bw", "sync", "--raw"]
        try:
            subprocess.check_output(sync_cmd)
        except subprocess.CalledProcessError as e:
            logging.error(
                f"{Fore.RED}error while syncing vault, exited with status code: {e.returncode}{Fore.RESET}")
            error_output = e.output
            if isinstance(error_output, bytes):
                error_output = error_output.decode()
            logging.debug(error_output)
            return False
        return True

    def get_item_list_from_folder(self, folder_id: str) -> dict | None:
        """
        retrieves a list of items in a given folder. if the folder is not the direct id then
        it will attempt to find the folder id through a search of the passed folder name.

        will return a list of `dict` items if successful, `None` if there was a failure.
        """

        # check if the passed string is a folder id or folder name
        # if it is a name then search for the folder id
        if not self.__is_valid_uuid(folder_id):
            if not (temp_folder_id := self.__search_folder(folder_id)):
                return None
            folder_id = temp_folder_id

        list_cmd = ["bw", "list", "items", "--raw", "--folderid", folder_id]
        try:
            raw_output = subprocess.check_output(list_cmd, env=self.ENV)
        except subprocess.CalledProcessError as e:
            logging.error(
                f"{Fore.RED}error while getting vault items, exited with status code: {e.returncode}{Fore.RESET}")
            error_output = e.output
            if isinstance(error_output, bytes):
                error_output = error_output.decode()
            logging.debug(error_output)
            return None
        output = json.loads(raw_output.decode())
        if len(output) == 0:
            logging.warning(f"{Fore.YELLOW}no items found in folder: {folder_id}{Fore.RESET}")
            return None

        items = {}
        for r in output:
            name = r["name"]
            secret = r["login"]["password"]
            items[name] = secret
        return items

    def __search_folder(self, folder_name: str) -> str | None:
        """
        attemps to search for a folder id using the given folder name.

        returns a single folder id if successful, `None` if there was a problem
        (i.e. no/multiple folders)
        """

        search_cmd = ["bw", "list", "folders",
                      "--raw", "--search", folder_name]
        try:
            raw_output = subprocess.check_output(search_cmd, env=self.ENV)
        except subprocess.CalledProcessError as e:
            logging.error(
                f"{Fore.RED}error while getting vault items, exited with status code: {e.returncode}{Fore.RESET}")
            error_output = e.output
            if isinstance(error_output, bytes):
                error_output = error_output.decode()
            logging.debug(error_output)
            return None

        # check the output of the search and print an error message if not exactly
        # one folder is found
        raw_folders = json.loads(raw_output.decode())
        if len(raw_folders) == 0:
            logging.error(
                f"{Fore.RED}no folders found for search term \"{folder_name}\"{Fore.RESET}")
            return None
        if len(raw_folders) > 1:
            err = "{Fore.RED}multiple folders found for search term \"{folder_name}\"{Fore.RESET}:\n"
            for f in raw_folders:
                err += f"\t{f["name"]}\n"
            logging.error(err)
            return None
        return raw_folders[0]["id"]

    def __is_valid_uuid(self, uuid_to_test: str, version: int = 4) -> bool:
        """
        checks if a passed string is a uuid or not.

        returns `True`/`False`
        """

        try:
            uuid_obj = UUID(uuid_to_test, version=version)
        except ValueError:
            return False
        return str(uuid_obj) == uuid_to_test
