"""Provide methods to manipulate Bitwarden vault using the Bitwarden CLI"""

from copy import deepcopy
import json
import logging
from subprocess import run


def status(session=b""):
    """Check status of vault

    Returns: Dict -
             {serverUrl: <url>,
             lastSync: date/time,
             userEmail: <email>,
             userId: userId,
             status: <'locked', 'unlocked' or 'unauthenticated'>}
        Empty dict {} on error

    """
    res = run(
        ["bw", "--session", session, "status"], capture_output=True, check=False
    )
    if not res.stdout:
        logging.error(res)
        return {}
    return dict(json.loads(res.stdout.split(b"\n")[-1]))


def set_server(url="https://vault.bitwarden.com"):
    """Set vault URL

    Returns: True if successful or False on error

    """
    res = run(["bw", "config", "server", url], capture_output=True, check=False)
    if not res.stdout:
        logging.error(res)
        return False
    return True


def login(email, password, method=None, code=""):
    """Initial login to Bitwarden Vault. May require BW_CLIENTSECRET to be set.

    Args: email - string
          password - string
          method - int (0: Authenticator, 1: Email, 3: Yubikey)
          code - OTP code

    Returns: session (bytes) or False on error, Error message

    """
    cmd = ["bw", "login", "--raw", email, password]
    if method and code:
        cmd = [
            "bw",
            "login",
            "--raw",
            email,
            password,
            "--method",
            method,
            "--code",
            code,
        ]
    res = run(cmd, capture_output=True, check=False)
    if not res.stdout or res.stderr:
        logging.error(res)
        return (False, res.stderr)
    return res.stdout, None


def unlock(password):
    """Unlock vault

    Returns: session (bytes) or False on error, Error message

    """
    if not password:
        logging.error("No password provided")
        return (False, "No password provided")
    res = run(
        ["bw", "unlock", "--raw", password], capture_output=True, check=False
    )
    if not res.stdout:
        logging.error(res)
        return (False, res.stderr)
    return res.stdout, None


def lock():
    """Lock vault

    Return: True on success, False with any errors

    """
    res = run(["bw", "lock"], capture_output=True, check=False)
    if not res.stdout:
        logging.error(res)
        return False
    return True


def logout():
    """Logout of vault

    Return: True on success, False with any errors

    """
    res = run(["bw", "logout"], capture_output=True, check=False)
    if not res.stderr:
        logging.error(res)
        return False
    return True


def get_orgs(session):
    """Return all organizations for the logged in user

    Return: Dict of org dicts {id: dict('object':'organization','id':id,'name':<name>...)}
            False on error

    """
    res = run(
        ["bw", "--session", session, "list", "organizations"],
        capture_output=True,
        check=False,
    )
    if not res.stdout:
        logging.error(res)
        return False
    return {i["id"]: i for i in json.loads(res.stdout)}


class Item(dict):
    """Set some default attributes to all items"""

    def __init__(self, /, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setdefault("fields", [])
        if not any(i["name"] == "autotype" for i in self.get("fields")):
            self["fields"].append({"name": "autotype", "value": "", "type": 0})


def get_entries(session, org_name=""):
    """Get all entries, folders, collections and orgs from vault

    Args: session: bytes
          org_name: name of organization. If given, only return items for that org
    1. the URL is buried in:
        'login'->'uris'->[{match: xxx, uri: http...}, {match2: xxx, uri2: httpxxx}]
    2. Also adjust 'path' to be just the dirname, not including the 'name'
    3. Add the 'autotype' field so it can be edited if necessary

        Return: items (list of Items), folders, collections, orgs
                False on error

    """
    logging.debug(
        f"get_entries: session type={type(session)}, value (first 20 chars)={str(session)[:20]}"
    )

    res = run(
        ["bw", "--session", session, "list", "items"],
        capture_output=True,
        check=False,
    )

    logging.debug(f"get_entries: returncode={res.returncode}")
    logging.debug(f"get_entries: stdout (first 200 chars)={res.stdout[:200]}")
    logging.debug(
        f"get_entries: stderr={res.stderr.decode('utf-8') if res.stderr else 'None'}"
    )

    if not res.stdout:
        logging.error(res)
        return False

    if res.returncode != 0:
        logging.error(f"get_entries failed with return code {res.returncode}")
        logging.error(f"stdout: {res.stdout.decode('utf-8')}")
        logging.error(
            f"stderr: {res.stderr.decode('utf-8') if res.stderr else 'None'}"
        )
        return False

    items = [Item(i) for i in json.loads(res.stdout)]
    folders = get_folders(session)
    collections = get_collections(session, org_name)
    orgs = get_orgs(session)
    return items, folders, collections, orgs


def sync(session):
    """Sync web vault changes to local vault

    Return: True on success, False with any errors

    """
    res = run(
        ["bw", "--session", session, "sync"], capture_output=True, check=False
    )
    if not res.stdout:
        logging.error(res)
        return False
    return True


def get_folders(session):
    """Return all folder names.

    Return: Dict of folder dicts {id: dict('object':folder,'id':id,'name':<name>)}
            False on error

    """
    res = run(
        ["bw", "--session", session, "list", "folders"],
        capture_output=True,
        check=False,
    )
    if not res.stdout:
        logging.error(res)
        return False
    return {i["id"]: i for i in json.loads(res.stdout)}


def get_collections(session, org_id=""):
    """Return all collection names for user.

    Args: session - session id bytes
          org_id - organization id string.

    Return: Dict of collection dicts {id:
        dict('object':collection,'id':id,'organizationId:<org
             id>,'externalId':<ext id>,'name':<name>)}

    """
    cmd = ["bw", "--session", session, "list", "collections"]
    if org_id:
        cmd.extend(["--organizationid", org_id])
    res = run(cmd, capture_output=True, check=False)
    if not res.stdout:
        logging.error(res)
        return False
    return {i["id"]: i for i in json.loads(res.stdout)}


def add_entry(entry, session):
    """Add new entry to vault

    Args: entry - dict with at least these fields
    Returns: new item dict or False on error

    New item template:
    {
        "organizationId":null,
        "folderId":null,
        "type":1,
        "name":"Item name",
        "notes":null,
        "favorite":false,
        "fields":[],
        "login":null,
        "secureNote":null,
        "card":null,
        "identity":null}'

    """
    enc = run(
        ["bw", "encode"],
        input=json.dumps(entry).encode(),
        capture_output=True,
        check=False,
    )
    if not enc.stdout:
        logging.error(enc)
        return False
    res = run(
        ["bw", "create", "--session", session, "item", enc.stdout],
        capture_output=True,
        check=False,
    )
    if not res.stdout:
        logging.error(res)
        return False
    return json.loads(res.stdout)


def edit_entry(entry, session, update_coll="NO"):
    # pylint: disable=too-many-return-statements
    """Modify existing vault entry

    Args: entry - entry dict object
          session - session id
          update_coll - 'YES' if item collections have been modified.
                        'MOVE' if collections are added so item needs to be
                            move to an org.  Returns: updated entry object
                            (dict) on success, False on failure
                        'REMOVE' if collections are removed so item needs to
                            be moved from the org to personal vault.

    """
    item = deepcopy(entry)
    if update_coll == "YES":
        # bw edit item-collections is unreliable, use delete+add instead
        res = delete_entry(entry, session)
        if res is False:
            return False
        item["id"] = None
        # organizationId and collectionIds are already set on item
        res = add_entry(item, session)
        if res is False:
            return False
        return res
    elif update_coll == "MOVE":
        # bw move command is unreliable, use delete+add instead
        res = delete_entry(entry, session)
        if res is False:
            return False
        item["id"] = None
        # organizationId and collectionIds are already set on item
        res = add_entry(item, session)
        if res is False:
            return False
        return res
    elif update_coll == "REMOVE":
        res = delete_entry(entry, session)
        if res is False:
            return False
        item["id"] = None
        item["collectionIds"] = []
        item["organizationId"] = None
        res = add_entry(item, session)
        if res is False:
            return False
        return res
    enc = run(
        ["bw", "encode"],
        input=json.dumps(item).encode(),
        capture_output=True,
        check=False,
    )
    if not enc.stdout:
        logging.error(enc)
        return False
    res = run(
        ["bw", "edit", "--session", session, "item", item["id"], enc.stdout],
        capture_output=True,
        check=False,
    )
    if not res.stdout:
        logging.error(res)
        return False
    return json.loads(res.stdout)


def delete_entry(entry, session):
    """Delete existing vault entry

    Args: entry - entry dict object
    Returns: entry object (dict) on success, False on failure

    """
    res = run(
        ["bw", "delete", "--session", session, "item", entry["id"]],
        capture_output=True,
        check=False,
    )
    if res.returncode != 0:
        logging.error(res)
        return False
    return entry


def add_folder(folder, session):
    """Add folder

    Args: folder - string (name of folder)
          session - bytes

    Returns: Folder object or False on error

    """
    folder = {"name": folder}
    enc = run(
        ["bw", "encode"],
        input=json.dumps(folder).encode(),
        capture_output=True,
        check=False,
    )
    if not enc.stdout:
        logging.error(enc)
        return False
    res = run(
        ["bw", "create", "--session", session, "folder", enc.stdout],
        capture_output=True,
        check=False,
    )
    if not res.stdout:
        logging.error(res)
        return False
    return json.loads(res.stdout)


def delete_folder(folder, session):
    """Delete folder

    Args: folder - folder object (dict)
          session - bytes
    Returns: folder object (dict) on success, False on failure


    """
    res = run(
        ["bw", "delete", "--session", session, "folder", folder["id"]],
        capture_output=True,
        check=False,
    )
    if res.returncode != 0:
        logging.error(res)
        return False
    return folder


def move_folder(folder, newpath, session):
    """Move or rename folder

    Args: folder - folder dict object
          newpath - string (new name/path)
          session - bytes
    Returns: Folder object on success, False on failure

    """
    fold = deepcopy(folder)
    fold["name"] = newpath
    enc = run(
        ["bw", "encode"],
        input=json.dumps(fold).encode(),
        capture_output=True,
        check=False,
    )
    if not enc.stdout:
        logging.error(enc)
        return False
    res = run(
        ["bw", "edit", "--session", session, "folder", fold["id"], enc.stdout],
        capture_output=True,
        check=False,
    )
    if not res.stdout:
        logging.error(res)
        return False
    return json.loads(res.stdout)


def add_collection(collection, org_id, session):
    """Add collection

    Args: collection - string
          org - organization id string
          session - bytes

    Returns: collection object or False on error

    """
    collection = {"name": collection, "organizationId": org_id}
    enc = run(
        ["bw", "encode"],
        input=json.dumps(collection).encode(),
        capture_output=True,
        check=False,
    )
    if not enc.stdout:
        logging.error(enc)
        return False
    res = run(
        [
            "bw",
            "create",
            "--session",
            session,
            "--organizationid",
            org_id.encode(),
            "org-collection".encode(),
            enc.stdout,
        ],
        capture_output=True,
        check=False,
    )
    if not res.stdout:
        logging.error(res)
        return False
    return json.loads(res.stdout)


def delete_collection(collection, session):
    """Delete collection

    Args: collection - collection object (dict)
          session - bytes
    Returns: collection object (dict) on success, False on failure

    """
    res = run(
        [
            "bw",
            "delete",
            "--session",
            session,
            "--organizationid",
            collection["organizationId"].encode(),
            "org-collection",
            collection["id"],
        ],
        capture_output=True,
        check=False,
    )
    if res.returncode != 0:
        logging.error(res)
        return False
    return collection


def move_collection(collection, newpath, session):
    """Move or rename collection

    Args: collection - collection object (dict)
          newpath - string (new name/path)
          session - bytes
    Returns: collection object on success, False on failure

    """
    coll = deepcopy(collection)
    coll["name"] = newpath
    enc = run(
        ["bw", "encode"],
        input=json.dumps(coll).encode(),
        capture_output=True,
        check=False,
    )
    if not enc.stdout:
        logging.error(enc)
        return False
    res = run(
        [
            "bw",
            "edit",
            "--session",
            session,
            "--organizationid",
            coll["organizationId"].encode(),
            "org-collection",
            coll["id"],
            enc.stdout,
        ],
        capture_output=True,
        check=False,
    )
    if not res.stdout:
        logging.error(res)
        return False
    return json.loads(res.stdout)


# vim: set et ts=4 sw=4 :
