from subprocess import CalledProcessError
import subprocess as sp
import json

from bitwarden_pyro.util.logger import ProjectLogger
from bitwarden_pyro.controller.cache import Cache


class Vault:
    """Load, get and filter items from bitwarden"""

    def __init__(self, expiry):
        self._items = None
        self._key = None
        self._filter = None

        self._cache = Cache(expiry)
        self._logger = ProjectLogger().get_logger()

    def has_cache(self):
        """Returns true if the cache has any available items"""

        return self._cache.has_items()

    def set_key(self, key):
        """Set the session key needed to access bw"""

        self._logger.debug("Vault key set")
        self._key = key

    def set_filter(self, folder_filter):
        """Set the folder filter used when getting items"""

        self._logger.debug("Vault folder filter set")
        self._filter = folder_filter

    def has_filter(self):
        """Returns true if the folder filter is set"""

        return self._filter is not None

    def get_filter(self):
        """Returns the folder filter, or None if not set"""
        return self._filter

    def sync(self):
        """Forces an items sync from bitwarden"""

        try:
            self._logger.info("Syncing items with bitwarden")

            sync_cmd = f"bw sync --session {self._key}"
            sp.run(sync_cmd.split(), capture_output=True, check=True)
        except CalledProcessError:
            raise SyncException("Failed to force a bitwarden sync")

    def __get_item_property(self, item, field):
        try:
            self._logger.info("Requesting %s from bitwarden", field)

            cmd = ['bw', '--session', self._key, 'get', field, item['id']]
            proc = sp.run(cmd, capture_output=True, check=True)

            output = proc.stdout.decode("utf-8")
            return output
        except CalledProcessError:
            raise LoadException(f"Failed to retrieve {field} from bw")

    def get_item_full(self, item):
        """Get a single item's full data directly from bw"""

        if self._cache.has_items():
            return json.loads(self.__get_item_property(item, 'item'))

        return item

    def get_item_topt(self, item):
        """Get a single item's TOTP data from bitwarden"""
        return self.__get_item_property(item, 'totp')

    def load_items(self, use_cache=True):
        """Load item data from bitwarden or cache"""
        try:
            if use_cache and self.has_cache():
                self._logger.info("Loading items from cache")
                self._items = self._cache.get()
            else:
                self._logger.info("Loading items from bw")
                load_cmd = f"bw list items --session {self._key}"

                proc = sp.run(load_cmd.split(),
                              capture_output=True, check=True)
                items_json = proc.stdout.decode("utf-8")
                self._items = json.loads(items_json)

                if self._cache.should_cache():
                    self._cache.save(self._items)
        except CalledProcessError:
            raise LoadException("Failed to load vault items from bitwarden")

    def get_folders(self):
        """Get all available folders from bw"""
        try:
            self._logger.info("Getting folders from bw")
            cmd = f"bw list folders --session {self._key}"

            proc = sp.run(cmd.split(), capture_output=True, check=True)
            folders = proc.stdout.decode("utf-8")
            return json.loads(folders)
        except CalledProcessError:
            raise LoadException("Failed to load vault items from bitwarden")

    def get_items(self):
        """Get currently loaded items, after applying available filters"""
        if not self._filter:
            return self._items

        return [
            i for i in self._items
            if i.get('folderId') is not None
            and i.get('folderId') == self._filter['id']
        ]

    def get_by_name(self, name):
        """Get items filtered by name"""
        items = [i for i in self._items if i['name'] == name]
        if len(items) == 1:
            return items[0]

        return items


class VaultException(Exception):
    """Base class for items generated by Vault"""


class LoadException(VaultException):
    """Raised when vault fails to load items"""


class SyncException(VaultException):
    """Raised when bitwarden fails to sync"""
