from logging import DEBUG, ERROR
from typing import Dict, List, Optional, Tuple
import click
from collections import defaultdict
import json
from pathlib import Path
from logdecorator import log_on_end, log_on_error, log_on_start
from taf.api.api_workflow import manage_repo_and_signers, transactional_execution
from taf.tuf.keys import get_sslib_key_from_value
from taf.api.utils._git import check_if_clean
from taf.exceptions import KeystoreError, TAFError
from taf.models.converter import from_dict
from taf.models.types import TargetsRole, compare_roles_data
from taf.keys import (
    find_keystore,
    get_key_name,
    get_metadata_key_info,
    load_sorted_keys_of_new_roles,
)
from taf.auth_repo import AuthenticationRepository
from taf.constants import (
    DEFAULT_ROLE_SETUP_PARAMS,
    DEFAULT_RSA_SIGNATURE_SCHEME,
)
from taf.keystore import new_public_key_cmd_prompt
from taf.tuf.repository import MAIN_ROLES, METADATA_DIRECTORY_NAME
from taf.utils import get_key_size, read_input_dict, resolve_keystore_path
from taf.log import NOTICE, taf_logger
from taf.models.types import RolesKeysData
from taf.messages import git_commit_message

from securesystemslib.signer._key import SSlibKey


@log_on_start(DEBUG, "Adding a new role {role:s}", logger=taf_logger)
@log_on_end(DEBUG, "Finished adding a new role", logger=taf_logger)
@log_on_error(
    ERROR,
    "An error occurred while adding a new role {role:s}: {e}",
    logger=taf_logger,
    on_exceptions=TAFError,
    reraise=True,
)
@check_if_clean
def add_role(
    path: str,
    role: str,
    parent_role: str,
    paths: list,
    keys_number: int,
    threshold: int,
    yubikey: bool,
    keystore: str,
    scheme: Optional[str] = DEFAULT_RSA_SIGNATURE_SCHEME,
    auth_repo: Optional[AuthenticationRepository] = None,
    commit: Optional[bool] = True,
    prompt_for_keys: Optional[bool] = False,
    push: Optional[bool] = True,
    skip_prompt: Optional[bool] = False,
) -> None:
    """
    Add a new delegated target role and update and sign metadata files.
    Automatically commit the changes if commit is set to True.

    Arguments:
        path: Path to the authentication repository.
        role: Name of the role which is to be added.
        parent_role: Name of the target role that is the new role's parent. Can be targets or another delegated role.
        paths: A list of target paths that are delegated to the new role.
        keys_number: Total number of signing keys
        threshold: Signature's threshold.
        yubikey: Specifies if the metadata file should be signed using a YubiKey.
        keystore: Location of the keystore files.
        scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default.
        auth_repo (optional): Instance of the authentication repository. Will be created if not passed into the function.
        commit (optional): Indicates if the changes should be committed and pushed automatically.
        prompt_for_keys (optional): Whether to ask the user to enter their key if it is not located inside the keystore directory.
        push (optional): Flag specifying whether to push to remote.
        skip_prompt (optional): A flag defining if the user will be asked if they want to generate new keys or reuse existing
            ones in case keystore files should be used. New keys will be generated by default.

    Side Effects:
        Initializes a new delegated targets role, signs metadata files, write changes to the disk and optionally commits.

    Returns:
        None
    """

    if auth_repo is None:
        auth_repo = AuthenticationRepository(path=path)

    if not parent_role:
        parent_role = "targets"

    existing_roles = auth_repo.get_all_targets_roles()
    existing_roles.extend(MAIN_ROLES)
    if role in existing_roles:
        taf_logger.log("NOTICE", "All roles already set up")
        return

    keystore_path = Path(keystore) if keystore else find_keystore(Path(path))
    commit_msg = git_commit_message("add-role", role=role)
    metadata_path = Path(METADATA_DIRECTORY_NAME, f"{role}.json")

    with manage_repo_and_signers(
        auth_repo,
        roles=[parent_role],
        keystore=keystore_path,
        scheme=scheme,
        prompt_for_keys=prompt_for_keys,
        load_roles=True,
        load_snapshot_and_timestamp=True,
        commit=commit,
        push=push,
        commit_msg=commit_msg,
        paths_to_reset_on_error=[metadata_path],
    ):
        targets_parent_role = TargetsRole()
        if parent_role != "targets":
            targets_parent_role.name = parent_role
            targets_parent_role.paths = []

        new_role = TargetsRole()
        new_role.name = role
        new_role.parent = targets_parent_role
        new_role.paths = paths
        new_role.number = keys_number
        new_role.threshold = threshold
        new_role.yubikey = yubikey

        signers, _ = load_sorted_keys_of_new_roles(
            roles=new_role,
            yubikeys_data=None,
            keystore=keystore_path,
            skip_prompt=skip_prompt,
            certs_dir=auth_repo.certs_dir,
        )
        auth_repo.create_delegated_roles([new_role], signers)
        auth_repo.add_new_roles_to_snapshot([new_role.name])
        auth_repo.do_timestamp()


@log_on_start(DEBUG, "Adding new paths to role {delegated_role:s}", logger=taf_logger)
@log_on_end(DEBUG, "Finished adding new paths to role", logger=taf_logger)
@log_on_error(
    ERROR,
    "An error occurred while adding new paths to role {delegated_role:s}: {e}",
    logger=taf_logger,
    on_exceptions=TAFError,
    reraise=True,
)
def add_role_paths(
    paths: List[str],
    delegated_role: str,
    keystore: str,
    commit: Optional[bool] = True,
    auth_repo: Optional[AuthenticationRepository] = None,
    auth_path: Optional[str] = None,
    prompt_for_keys: Optional[bool] = False,
    push: Optional[bool] = True,
) -> None:
    """
    Adds additional delegated target paths to the specified role. That means that
    the role will be responsible for sining target files at those location going forward.

    Arguments:
        paths: A list of additional target paths that should be delegated to the role.
        delegated_role: Name of the targets role to which the new paths should be delegated.
        auth_path: Path to the authentication repository.
        keystore: Location of the keystore files.
        auth_repo (optional): Instance of the authentication repository. Will be created if not passed into the function.
        prompt_for_keys (optional): Whether to ask the user to enter their key if it is not located inside the keystore directory.
        commit (optional): Indicates if the changes should be committed and pushed automatically.
        push (optional): Flag specifying whether to push to remote.
    Side Effects:
        Updates the specified target role's parent and other metadata files (snapshot and timestamp),
        signs them, writes changes to disk and optionally commits everything.

    Returns:
        None
    """

    if auth_repo is None:
        auth_repo = AuthenticationRepository(path=auth_path)

    parent_role = auth_repo.find_delegated_roles_parent(delegated_role)
    if all(
        path in auth_repo.get_delegations_of_role(parent_role)[delegated_role].paths
        for path in paths
    ):
        taf_logger.log("NOTICE", "Paths already added")
        return

    commit_msg = git_commit_message(
        "add-role-paths", role=delegated_role, paths=", ".join(paths)
    )

    with manage_repo_and_signers(
        auth_repo,
        [delegated_role],
        keystore=keystore,
        prompt_for_keys=prompt_for_keys,
        load_roles=False,
        load_parents=True,
        load_snapshot_and_timestamp=True,
        commit=commit,
        push=push,
        commit_msg=commit_msg,
    ):
        auth_repo.add_path_to_delegated_role(role=delegated_role, paths=paths)
        auth_repo.update_snapshot_and_timestamp()


@log_on_start(DEBUG, "Adding new roles", logger=taf_logger)
@log_on_end(DEBUG, "Finished adding new roles", logger=taf_logger)
@log_on_error(
    ERROR,
    "An error occurred while adding new roles: {e}",
    logger=taf_logger,
    on_exceptions=TAFError,
    reraise=True,
)
@check_if_clean
def add_multiple_roles(
    path: str,
    keystore: Optional[str] = None,
    roles_key_infos: Optional[str] = None,
    scheme: Optional[str] = DEFAULT_RSA_SIGNATURE_SCHEME,
    prompt_for_keys: Optional[bool] = False,
    commit: Optional[bool] = True,
    push: Optional[bool] = True,
) -> None:
    """
    Add new target roles and sign all metadata files given information stored in roles_key_infos
    dictionary or .json file.

    Arguments:
        path: Path to the authentication repository.
        keystore (optional): Location of the keystore files.
        roles_key_infos: Path to a json file which contains information about repository's roles and keys.
        scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default.
        prompt_for_keys (optional): Whether to ask the user to enter their key if it is not located inside the keystore directory.
        commit (optional): Indicates if the changes should be committed and pushed automatically.
        push (optional): Flag specifying whether to push to remote.
    Side Effects:
        Updates metadata files (parent of new roles, snapshot and timestamp) and creates new targets metadata files.
        Writes changes to disk.

    Returns:
        None
    """

    roles_keys_data_new = _initialize_roles_and_keystore_for_existing_repo(
        path, roles_key_infos, keystore
    )

    auth_repo = AuthenticationRepository(path=path)
    roles_data = auth_repo.generate_roles_description()
    roles_keys_data_current = from_dict(roles_data, RolesKeysData)
    new_roles_data, _ = compare_roles_data(roles_keys_data_current, roles_keys_data_new)
    existing_roles = auth_repo.get_all_targets_roles()
    existing_roles.extend(MAIN_ROLES)
    roles_to_add_data = [
        role_data
        for role_data in new_roles_data
        if role_data.name not in existing_roles
    ]
    if not len(roles_to_add_data):
        taf_logger.log("NOTICE", "All roles already set up")
        return

    roles_to_add = [role_data.name for role_data in new_roles_data]
    commit_msg = git_commit_message("add-roles", roles=", ".join(roles_to_add))
    roles_to_load = [
        role_data.parent.name
        for role_data in new_roles_data
        if role_data.parent.name not in roles_to_add
    ]
    keystore_path = roles_keys_data_new.keystore

    with manage_repo_and_signers(
        auth_repo,
        roles=roles_to_load,
        keystore=keystore_path,
        scheme=scheme,
        prompt_for_keys=prompt_for_keys,
        load_snapshot_and_timestamp=True,
        commit_msg=commit_msg,
        commit=commit,
        push=push,
    ):
        all_signers = {}
        for role_to_add_data in roles_to_add_data:
            signers, _ = load_sorted_keys_of_new_roles(
                roles=role_to_add_data,
                yubikeys_data=None,
                keystore=keystore_path,
                skip_prompt=not prompt_for_keys,
                certs_dir=auth_repo.certs_dir,
            )
            all_signers.update(signers)

        auth_repo.create_delegated_roles(roles_to_add_data, all_signers)
        auth_repo.add_new_roles_to_snapshot(roles_to_add)
        auth_repo.do_timestamp()


@log_on_start(NOTICE, "Adding a new signing key", logger=taf_logger)
@log_on_end(DEBUG, "Finished adding a new signing key", logger=taf_logger)
@log_on_error(
    ERROR,
    "An error occurred while adding a new signing key: {e}",
    logger=taf_logger,
    on_exceptions=TAFError,
    reraise=True,
)
@check_if_clean
def add_signing_key(
    path: str,
    roles: List[str],
    pub_key_path: Optional[str] = None,
    pub_key: Optional[SSlibKey] = None,
    keystore: Optional[str] = None,
    scheme: Optional[str] = DEFAULT_RSA_SIGNATURE_SCHEME,
    commit: Optional[bool] = True,
    prompt_for_keys: Optional[bool] = False,
    push: Optional[bool] = True,
    commit_msg: Optional[str] = None,
) -> None:
    """
    Add a new signing key to the listed roles. Update root metadata if one or more roles is one of the main TUF roles,
    parent target role if one of the roles is a delegated target role and timestamp and snapshot in any case.

    Arguments:
        path: Path to the authentication repository.
        roles: A list of roles whose signing keys need to be extended.
        pub_key_path (optional): path to the file containing the public component of the new key. If not provided,
            it will be necessary to ender the key when prompted.
        keystore (optional): Location of the keystore files.
        scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default.
        prompt_for_keys (optional): Whether to ask the user to enter their key if it is not located inside the keystore directory.
        commit (optional): Indicates if the changes should be committed and pushed automatically.
        push (optional): Flag specifying whether to push to remote.
        commit_msg(optional): Commit message. Will be necessary to enter it if not provided.
    Side Effects:
        Updates metadata files (parents of the affected roles, snapshot and timestamp).
        Writes changes to disk.

    Returns:
        None
    """

    pub_key = pub_key or _load_pub_key_from_file(
        pub_key_path, prompt_for_keys=prompt_for_keys, scheme=scheme
    )

    roles_keys = {role: [pub_key] for role in roles}

    auth_repo = AuthenticationRepository(path=path)

    with manage_repo_and_signers(
        auth_repo,
        roles,
        keystore,
        scheme,
        prompt_for_keys,
        load_snapshot_and_timestamp=True,
        load_parents=True,
        load_roles=False,
        commit=commit,
        push=push,
        commit_msg=commit_msg,
    ):
        added_keys, already_added_keys, invalid_keys = auth_repo.add_metadata_keys(
            roles_keys
        )
        if already_added_keys:
            taf_logger.log(
                "NOTICE", f"Key(s) {', '.join(already_added_keys)} already added"
            )
        if invalid_keys:
            taf_logger.warning(f"Key(s) {', '.join(invalid_keys)} invalid")

        if len(added_keys):
            auth_repo.update_snapshot_and_timestamp()


@log_on_start(NOTICE, "Revoking signing key", logger=taf_logger)
@log_on_end(DEBUG, "Finished revoking signing key", logger=taf_logger)
@log_on_error(
    ERROR,
    "An error occurred while revoking signing key: {e}",
    logger=taf_logger,
    on_exceptions=TAFError,
    reraise=True,
)
@check_if_clean
def revoke_signing_key(
    path: str,
    key_id: str,
    roles: Optional[List[str]] = None,
    keystore: Optional[str] = None,
    scheme: Optional[str] = DEFAULT_RSA_SIGNATURE_SCHEME,
    commit: Optional[bool] = True,
    prompt_for_keys: Optional[bool] = False,
    push: Optional[bool] = True,
    commit_msg: Optional[str] = None,
) -> None:
    """
    Revoke signing key. Update root metadata if one or more roles is one of the main TUF roles,
    parent target role if one of the roles is a delegated target role and timestamp and snapshot in any case.

    Arguments:
        path: Path to the authentication repository.
        roles: A list of roles whose signing keys need to be extended.
        key_id: id of the key to be removed
        keystore (optional): Location of the keystore files.
        scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default.
        prompt_for_keys (optional): Whether to ask the user to enter their key if it is not located inside the keystore directory.
        commit (optional): Indicates if the changes should be committed and pushed automatically.
        push (optional): Flag specifying whether to push to remote.
        commit_msg(optional): Commit message. Will be necessary to enter it if not provided.
    Side Effects:
        Updates metadata files (parents of the affected roles, snapshot and timestamp).
        Writes changes to disk.

    Returns:
        None
    """

    auth_repo = AuthenticationRepository(path=path)

    roles_to_update = roles or auth_repo.find_keysid_roles([key_id])

    with manage_repo_and_signers(
        auth_repo,
        roles_to_update,
        keystore,
        scheme,
        prompt_for_keys,
        load_snapshot_and_timestamp=True,
        load_parents=True,
        load_roles=False,
        commit=commit,
        push=push,
        commit_msg=commit_msg,
    ):

        (
            removed_from_roles,
            not_added_roles,
            less_than_threshold_roles,
        ) = auth_repo.revoke_metadata_key(key_id=key_id, roles=roles)
        if not_added_roles:
            taf_logger.log(
                "NOTICE",
                f"Key is not a signing key of role(s) {', '.join(not_added_roles)}",
            )
        if less_than_threshold_roles:
            taf_logger.warning(
                f"Cannot remove key from {', '.join(less_than_threshold_roles)}. Number of keys must be greater or equal to thresholds"
            )

        if len(removed_from_roles):
            auth_repo.update_snapshot_and_timestamp()


@check_if_clean
def rotate_signing_key(
    path: str,
    key_id: str,
    pub_key_path: Optional[str] = None,
    roles: Optional[List[str]] = None,
    keystore: Optional[str] = None,
    scheme: Optional[str] = DEFAULT_RSA_SIGNATURE_SCHEME,
    prompt_for_keys: Optional[bool] = False,
    push: Optional[bool] = True,
    revoke_commit_msg: Optional[str] = None,
    add_commit_msg: Optional[str] = None,
) -> None:
    """
    Rotate signing key. Remove it from one or more roles and add a new signing key.
    Update root metadata if one or more roles is one of the main TUF roles,
    parent target role if one of the roles is a delegated target role and timestamp and snapshot in any case.

    Arguments:
        path: Path to the authentication repository.
        roles: A list of roles whose signing keys need to be extended.
        key_id: id of the key to be removed
        pub_key_path (optional): path to the file containing the public component of the new key. If not provided,
            it will be necessary to ender the key when prompted.
        keystore (optional): Location of the keystore files.
        scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default.
        prompt_for_keys (optional): Whether to ask the user to enter their key if it is not located inside the keystore directory.
        commit (optional): Indicates if the changes should be committed and pushed automatically.
        push (optional): Flag specifying whether to push to remote.
        revoke_commit_msg(optional): First commit message, when revokig the specified key. Will be necessary to enter it if not provided.
        add_commit_msg(optional): Second commit message, when addug a new signing key. Will be necessary to enter it if not provided.
    Side Effects:
        Updates metadata files (parents of the affected roles, snapshot and timestamp).
        Writes changes to disk.

    Returns:
        None
    """

    pub_key = _load_pub_key_from_file(
        pub_key_path, prompt_for_keys=prompt_for_keys, scheme=scheme
    )
    auth_repo = AuthenticationRepository(path=path)
    roles = roles or auth_repo.find_keysid_roles([key_id])

    with transactional_execution(auth_repo):
        revoke_signing_key(
            path=path,
            key_id=key_id,
            roles=roles,
            keystore=keystore,
            scheme=scheme,
            commit=True,
            prompt_for_keys=prompt_for_keys,
            push=False,
            commit_msg=revoke_commit_msg,
        )

        add_signing_key(
            path=path,
            roles=roles,
            pub_key=pub_key,
            keystore=keystore,
            scheme=scheme,
            commit=True,
            prompt_for_keys=prompt_for_keys,
            push=push,
            commit_msg=add_commit_msg,
        )


# TODO this is probably outdated, the format of the outputted roles_key_infos
def _enter_roles_infos(keystore: Optional[str], roles_key_infos: Optional[str]) -> Dict:
    """
    Ask the user to enter information taf roles and keys, including the location
    of keystore directory if not entered through an input parameter

    Arguments:
        keystore: Location of the keystore files.
        roles_key_infos: Path to a json file which contains information about repository's roles and keys.

    Side Effects:
        None

    Returns:
        A dictionary containing entered information about taf roles and keys (total number of keys per role,
        parent roles of roles, threshold of signatures per role, indicator if metadata should be signed using
        a yubikey for each role, key length and signing scheme for each role)
    """
    mandatory_roles = ["root", "targets", "snapshot", "timestamp"]
    role_key_infos: Dict = defaultdict(dict)
    infos_json: Dict = {}

    for role in mandatory_roles:
        role_key_infos[role] = _enter_role_info(role, role == "targets", keystore)
    infos_json["roles"] = role_key_infos

    if keystore:
        infos_json["keystore"] = keystore

    def _print_roles_key_infos(infos_json_str):
        print("------------------")
        print(
            "Configuration json - save it in order to make creation of repositories quicker"
        )
        print(json.dumps(infos_json, indent=4))
        print("------------------")

    infos_json_str = json.dumps(infos_json, indent=4)
    if roles_key_infos is not None:
        try:
            path = Path(roles_key_infos)
            path.parent.mkdir(parents=True, exist_ok=True)
            Path(roles_key_infos).write_text(infos_json_str)
            taf_logger.log(
                "NOTICE",
                f"Configuration json written to {Path(roles_key_infos).absolute()}",
            )
        except Exception as e:
            taf_logger.error(e)
            _print_roles_key_infos(infos_json_str)
    else:
        print(infos_json_str)
    return infos_json


def _enter_role_info(
    role: str, is_targets_role: bool, keystore: Optional[str] = None
) -> Dict:
    role_info = {}
    keys_num = _read_val(int, f"number of {role} keys", "number")
    if keys_num is not None:
        role_info["number"] = keys_num

    role_info["yubikey"] = click.confirm(f"Store {role} keys on Yubikeys?")
    if role_info["yubikey"]:
        # in case of yubikeys, length and scheme have to have specific values
        role_info["length"] = 2048
        role_info["scheme"] = DEFAULT_RSA_SIGNATURE_SCHEME
    else:
        # if keystore is specified and contains keys corresponding to this role
        # get key size based on the public key
        keystore_length = 0
        if keystore is not None:
            keystore_length = _get_roles_key_size(role, keystore, keys_num)
        if keystore_length == 0:
            key_length = _read_val(int, f"{role} key length", "length")
            if key_length is not None:
                role_info["length"] = key_length
        else:
            role_info["length"] = keystore_length
        scheme = _read_val(str, f"{role} signature scheme", "scheme")
        if scheme is not None:
            role_info["scheme"] = scheme

    threshold = _read_val(int, f"{role} signature threshold", "threshold")
    if threshold is not None:
        role_info["threshold"] = threshold

    if is_targets_role:
        delegated_roles: Dict = defaultdict(dict)
        while click.confirm(
            f"Add {'another' if len(delegated_roles) else 'a'} delegated targets role of role {role}?"
        ):
            role_name = _read_val(str, "role name", "role_name", True)
            delegated_paths: List[str] = []
            while not len(delegated_paths) or click.confirm("Enter another path?"):
                delegated_paths.append(
                    _read_val(
                        str,
                        f"path or glob pattern delegated to {role_name}",
                        "delegated_paths",
                        True,
                    )
                )
            delegated_roles[role_name]["paths"] = delegated_paths
            is_terminating = click.confirm(f"Is {role_name} terminating?")
            delegated_roles[role_name]["terminating"] = is_terminating
            delegated_roles[role_name].update(
                _enter_role_info(role_name, True, keystore)
            )
        role_info["delegations"] = delegated_roles

    return role_info


def _load_pub_key_from_file(pub_key_path, prompt_for_keys, scheme) -> SSlibKey:
    pub_key_pem = None
    if pub_key_path is not None:
        pub_key_pem_path = Path(pub_key_path)
        if pub_key_pem_path.is_file():
            pub_key_pem = Path(pub_key_path).read_text()

    if pub_key_pem is None and prompt_for_keys:
        pub_key_pem = new_public_key_cmd_prompt(scheme)["keyval"]["public"]

    if pub_key_pem is None:
        raise TAFError("Public key not provided or invalid")

    return get_sslib_key_from_value(pub_key_pem)


def _read_val(input_type, name, param=None, required=False):
    default_value_msg = ""
    default_value = None
    if param is not None:
        default_value = DEFAULT_ROLE_SETUP_PARAMS[param]
        if default_value is not None:
            default_value_msg = f"(default {default_value}) "

    while True:
        try:
            val = input(f"Enter {name} and press ENTER {default_value_msg}")
            if not val:
                if not required:
                    return default_value
                else:
                    continue
            return input_type(val)
        except ValueError:
            pass


def _initialize_roles_and_keystore_for_existing_repo(
    path: str,
    roles_key_infos: Optional[str],
    keystore: Optional[str],
    enter_info: Optional[bool] = True,
) -> RolesKeysData:
    roles_key_infos_dict = read_input_dict(roles_key_infos)

    if not roles_key_infos_dict and enter_info:
        roles_key_infos_dict = _enter_roles_infos(None, roles_key_infos)
    roles_keys_data = from_dict(roles_key_infos_dict, RolesKeysData)
    keystore = keystore or roles_keys_data.keystore
    if keystore is None and path is not None:
        keystore_path = find_keystore(Path(path))
        if keystore_path:
            roles_keys_data.keystore = str(keystore_path)
    else:
        roles_keys_data.keystore = keystore
    return roles_keys_data


def _initialize_roles_and_keystore(
    roles_key_infos: Optional[str],
    keystore: Optional[str],
    enter_info: Optional[bool] = True,
) -> Tuple[Dict, Optional[str], bool]:
    """
    Read information about roles and keys from a json file or ask the user to enter
    this information if not specified through a json file and enter_info is True.

    Arguments:
        roles_key_infos: A dictionary containing information about the roles:
            - total number of keys per role
            - their parent roles
            - threshold of signatures per role
            - should keys of a role be on Yubikeys or should a keystore files be used
            - scheme (the default scheme is rsa-pkcs1v15-sha256)
            - keystore path, if not specified via keystore option
        keystore: Location of the keystore files.
        enter_info (optional): Indicates if the user should be asked to enter information about the
        roles and keys if not specified. Set to True by default.


    Side Effects:
        None

    Returns:
        A dictionary containing entered information about taf roles and keys (total number of keys per role,
        parent roles of roles, threshold of signatures per role, indicator if metadata should be signed using
        a yubikey for each role, key length and signing scheme for each role) and keystore file path.
    """

    skip_prompt = False

    roles_key_infos_dict = read_input_dict(roles_key_infos)

    if not roles_key_infos_dict and enter_info:
        roles_key_infos_dict = _enter_roles_infos(None, roles_key_infos)

    # Check if all keys should be loaded from/stored to Yubikeys
    use_yubikeys = all(
        role_info.get("yubikey", False) for role_info in roles_key_infos_dict.values()
    )

    if not use_yubikeys and not keystore:
        keystore = roles_key_infos_dict.get("keystore")
        if not keystore:
            while True:
                use_keystore = (
                    input("Do you want to save/load keys from keystore files? [y/N]: ")
                    .strip()
                    .lower()
                )
                if use_keystore in ["y", "n"]:
                    break
            if use_keystore == "y":
                keystore = (
                    input("Enter keystore path (default ./keystore): ").strip()
                    or "./keystore"
                )
            else:
                taf_logger.log(
                    "NOTICE",
                    "Keys will be entered and then printed from the command line...",
                )

    if keystore is not None:
        keystore = resolve_keystore_path(keystore, roles_key_infos)
        roles_key_infos_dict["keystore"] = keystore

        while True:
            keystore_path = Path(keystore)
            if keystore_path.exists():
                break
            create_keystore = (
                input(
                    f"Keystore directory {keystore_path} does not exist. Do you want to create it? [y/N]: "
                )
                .strip()
                .lower()
            )
            if create_keystore == "y":
                keystore_path.mkdir(parents=True, exist_ok=True)
                roles_key_infos_dict["keystore"] = keystore
                print(f"Created keystore directory at {keystore}")
                skip_prompt = True
                break
            else:
                enter_new_path = (
                    input(
                        "Do you want to enter a different path to the keystore? [y/N]: "
                    )
                    .strip()
                    .lower()
                )
                if enter_new_path == "y":
                    keystore = input("New keystore path: ").strip()
                    keystore = resolve_keystore_path(keystore, roles_key_infos)
                    roles_key_infos_dict["keystore"] = keystore
                else:
                    raise KeystoreError("Keystore not found")

    return roles_key_infos_dict, keystore, skip_prompt


def _get_roles_key_size(role: str, keystore: str, keys_num: int) -> int:
    pub_key_name = f"{get_key_name(role, 1, keys_num)}.pub"
    key_path = str(Path(keystore, pub_key_name))
    return get_key_size(key_path)


@log_on_error(
    ERROR,
    "Could not list keys of {role:s}: {e}",
    logger=taf_logger,
    on_exceptions=TAFError,
    reraise=True,
)
def list_keys_of_role(
    path: str,
    role: str,
) -> List[str]:
    """
     Print information about signing keys of role. If a certificate whose name matches
     a key's id exists, include information contained by that certificate (like name and valid to/from dates)

     Arguments:
         path: Path to the authentication repository.
         role: Name of the role which is to be removed.

    Side Effects:
         None

     Returns:
         None
    """
    auth_repo = AuthenticationRepository(path=path)
    key_ids = auth_repo.get_role_keys(role=role)
    if key_ids is None:
        raise TAFError(f"Role {role} does not exist")

    return [
        str(get_metadata_key_info(auth_repo.certs_dir, key_id)) for key_id in key_ids
    ]


@log_on_start(DEBUG, "Removing role {role:s}", logger=taf_logger)
@log_on_end(DEBUG, "Finished removing the role", logger=taf_logger)
@log_on_error(
    ERROR,
    "An error occurred while removing role {role:s}: {e}",
    logger=taf_logger,
    on_exceptions=TAFError,
    reraise=True,
)
@check_if_clean
def remove_role(
    path: str,
    role: str,
    keystore: str,
    scheme: Optional[str] = DEFAULT_RSA_SIGNATURE_SCHEME,
    commit: Optional[bool] = True,
    remove_targets: Optional[bool] = False,
    auth_repo: Optional[AuthenticationRepository] = None,
    prompt_for_keys: Optional[bool] = False,
    push: Optional[bool] = True,
) -> None:
    """
    Remove a delegated target role and update and sign metadata files.
    Automatically commit the changes if commit is set to True.
    It is not possible to remove any of the main TUF roles

    Arguments:
        path: Path to the authentication repository.
        role: Name of the role which is to be removed.
        keystore: Location of the keystore files.
        scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default.
        commit (optional): Indicates if the changes should be committed and pushed automatically.
        remove_targets (optional): Indicates if target files should be removed to, or signed by the parent role.
            Set to False by default.
        auth_repo (optional): Instance of the authentication repository. Will be created if not passed into the function.
        prompt_for_keys (optional): Whether to ask the user to enter their key if it is not located inside the keystore directory.
        push (optional): Flag specifying whether to push to remote.
    Side Effects:
        Updates metadata files, optionally deletes target files, writes changes to disk and optionally commits.

    Returns:
        None
    """

    # TODO This didn't fully work and was not being used
    # can be done later

    # if role in MAIN_ROLES:
    #     taf_logger.error(
    #         f"Cannot remove role {role}. It is one of the roles required by the TUF specification"
    #     )
    #     return

    # if auth_repo is None:
    #     auth_repo = AuthenticationRepository(path=path)

    # parent_role = auth_repo.find_delegated_roles_parent(role)
    # if parent_role is None:
    #     taf_logger.error("Role is not among delegated roles")
    #     return
    # parent_role_obj = auth_repo._role_obj(parent_role, auth_repo)
    # if not isinstance(parent_role_obj, Targets):
    #     taf_logger.error(f"Could not find parent targets role of role {role}.")
    #     return

    # roleinfo = tuf.roledb.get_roleinfo(parent_role, auth_repo.name)
    # added_targets_data: Dict = {}
    # removed_targets = []
    # for delegations_data in roleinfo["delegations"]["roles"]:
    #     if delegations_data["name"] == role:
    #         paths = delegations_data["paths"]
    #         for target_path in paths:
    #             target_file_path = Path(path, TARGETS_DIRECTORY_NAME, target_path)
    #             if target_file_path.is_file():
    #                 if remove_targets:
    #                     os.unlink(str(target_file_path))
    #                     removed_targets.append(str(target_file_path))
    #                 else:
    #                     added_targets_data[target_file_path] = {}
    #             else:
    #                 # try glob pattern traversal
    #                 full_pattern = str(Path(path, TARGETS_DIRECTORY_NAME, target_path))
    #                 matching_files = glob.glob(full_pattern)
    #                 for file_path in matching_files:
    #                     if remove_targets:
    #                         os.unlink(str(file_path))
    #                         removed_targets.append(file_path)
    #                     else:
    #                         added_targets_data[file_path] = {}
    #         break

    # parent_role_obj.revoke(role)

    # _update_role(
    #     auth_repo, role=parent_role, keystore=keystore, prompt_for_keys=prompt_for_keys
    # )
    # if len(added_targets_data):
    #     removed_targets_data: Dict = {}
    #     update_target_metadata(
    #         auth_repo,
    #         added_targets_data,
    #         removed_targets_data,
    #         keystore,
    #         write=False,
    #         scheme=DEFAULT_RSA_SIGNATURE_SCHEME,
    #         prompt_for_keys=prompt_for_keys,
    #     )

    # # if targets should be deleted, also removed them from repositories.json
    # if len(removed_targets):
    #     repositories_json = repositoriesdb.load_repositories_json(auth_repo)
    #     if repositories_json is not None:
    #         repositories = repositories_json["repositories"]
    #         for removed_target in removed_targets:
    #             if removed_target in repositories:
    #                 repositories.pop(removed_target)

    #             # update content of repositories.json before updating targets metadata
    #             Path(auth_repo.path, REPOSITORIES_JSON_PATH).write_text(
    #                 json.dumps(repositories_json, indent=4)
    #             )

    # update_snapshot_and_timestamp(
    #     auth_repo, keystore, scheme=scheme, prompt_for_keys=prompt_for_keys
    # )
    # if commit:
    #     commit_msg = git_commit_message("remove-role", role=role)
    #     auth_repo.commit_and_push(commit_msg=commit_msg, push=push)
    # else:
    #     taf_logger.log("NOTICE", "Please commit manually")


@log_on_start(DEBUG, "Removing delegated paths", logger=taf_logger)
@log_on_end(DEBUG, "Finished removing delegated paths", logger=taf_logger)
@log_on_error(
    ERROR,
    "An error occurred while removing roles: {e}",
    logger=taf_logger,
    on_exceptions=TAFError,
    reraise=True,
)
def remove_paths(
    path: str,
    paths: List[str],
    keystore: str,
    scheme: Optional[str] = DEFAULT_RSA_SIGNATURE_SCHEME,
    commit: Optional[bool] = True,
    commit_msg: Optional[str] = None,
    prompt_for_keys: Optional[bool] = False,
    push: Optional[bool] = True,
) -> bool:
    """
    Remove delegated paths. Update parent roles of the roles associated with the removed paths,
    as well as snapshot and timestamp. Optionally commit the changes.

    Arguments:
        path:  Path to the authentication repository.
        paths: Paths to be removed.
        keystore: Location of the keystore files.
        commit (optional): Indicates if the changes should be committed and pushed automatically.
        prompt_for_keys (optional): Whether to ask the user to enter their key if it is not located inside the keystore directory.
        push (optional): Flag specifying whether to push to remote.
    Side Effects:
        Updates metadata files, writes changes to disk and optionally commits them.

    Returns:
        True if the delegation existed, False otherwise
    """
    auth_repo = AuthenticationRepository(path=path)
    paths_to_remove_from_roles = defaultdict(list)
    for path_to_remove in paths:
        delegated_role = auth_repo.get_role_from_target_paths([path_to_remove])

        if delegated_role != "targets":
            paths_to_remove_from_roles[delegated_role].append(path_to_remove)
        else:
            taf_logger.log("NOTICE", f"Path {path_to_remove} not delegated to any role")
    if not len(paths_to_remove_from_roles):
        taf_logger.log("NOTICE", "No paths delegated")
        return False

    if commit_msg is None:
        commit_msg = git_commit_message(
            "remove-role-paths", paths=", ".join(paths), role=delegated_role
        )
    with manage_repo_and_signers(
        auth_repo,
        roles=list(paths_to_remove_from_roles.keys()),
        keystore=keystore,
        scheme=scheme,
        prompt_for_keys=prompt_for_keys,
        load_roles=False,
        load_parents=True,
        load_snapshot_and_timestamp=True,
        commit=commit,
        push=push,
        commit_msg=commit_msg,
    ):
        auth_repo.remove_delegated_paths(paths_to_remove_from_roles)
        auth_repo.update_snapshot_and_timestamp()

    return True


def list_roles(auth_repo: AuthenticationRepository) -> None:
    """
    Print a list of all defined roles with their thresholds and parent roles.
    """

    def print_role_and_children(role: str, indent: int = 0) -> None:
        threshold = auth_repo.get_role_threshold(role)
        indent_str = " " * indent
        print(f"{indent_str}{role} (threshold: {threshold})")

        # Retrieve children (delegated roles)
        delegations = auth_repo.get_delegations_info(role)
        if delegations:
            children = [role_info["name"] for role_info in delegations.get("roles", [])]
            for child in children:
                print_role_and_children(child, indent + 2)

    for role in MAIN_ROLES:
        print_role_and_children(role)
