import argparse
import functools
import os

from ..constants import *  # NOQA
from ..crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2CHPORepoKey
from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey
from ..crypto.keymanager import KeyManager
from ..manifest import Manifest

from ._common import with_repository

from ..logger import create_logger

logger = create_logger(__name__)


class KeysMixIn:
    @with_repository(compatibility=(Manifest.Operation.CHECK,))
    def do_change_passphrase(self, args, repository, manifest):
        """Change repository key file passphrase"""
        key = manifest.key
        if not hasattr(key, "change_passphrase"):
            print("This repository is not encrypted, cannot change the passphrase.")
            return EXIT_ERROR
        key.change_passphrase()
        logger.info("Key updated")
        if hasattr(key, "find_key"):
            # print key location to make backing it up easier
            logger.info("Key location: %s", key.find_key())
        return EXIT_SUCCESS

    @with_repository(exclusive=True, manifest=True, cache=True, compatibility=(Manifest.Operation.CHECK,))
    def do_change_location(self, args, repository, manifest, cache):
        """Change repository key location"""
        key = manifest.key
        if not hasattr(key, "change_passphrase"):
            print("This repository is not encrypted, cannot change the key location.")
            return EXIT_ERROR

        if args.key_mode == "keyfile":
            if isinstance(key, AESOCBRepoKey):
                key_new = AESOCBKeyfileKey(repository)
            elif isinstance(key, CHPORepoKey):
                key_new = CHPOKeyfileKey(repository)
            elif isinstance(key, Blake2AESOCBRepoKey):
                key_new = Blake2AESOCBKeyfileKey(repository)
            elif isinstance(key, Blake2CHPORepoKey):
                key_new = Blake2CHPOKeyfileKey(repository)
            else:
                print("Change not needed or not supported.")
                return EXIT_WARNING
        if args.key_mode == "repokey":
            if isinstance(key, AESOCBKeyfileKey):
                key_new = AESOCBRepoKey(repository)
            elif isinstance(key, CHPOKeyfileKey):
                key_new = CHPORepoKey(repository)
            elif isinstance(key, Blake2AESOCBKeyfileKey):
                key_new = Blake2AESOCBRepoKey(repository)
            elif isinstance(key, Blake2CHPOKeyfileKey):
                key_new = Blake2CHPORepoKey(repository)
            else:
                print("Change not needed or not supported.")
                return EXIT_WARNING

        for name in ("repository_id", "crypt_key", "id_key", "chunk_seed", "tam_required", "sessionid", "cipher"):
            value = getattr(key, name)
            setattr(key_new, name, value)

        key_new.target = key_new.get_new_target(args)
        # save with same passphrase and algorithm
        key_new.save(key_new.target, key._passphrase, create=True, algorithm=key._encrypted_key_algorithm)

        # rewrite the manifest with the new key, so that the key-type byte of the manifest changes
        manifest.key = key_new
        manifest.repo_objs.key = key_new
        manifest.write()
        repository.commit(compact=False)

        # we need to rewrite cache config and security key-type info,
        # so that the cached key-type will match the repo key-type.
        cache.begin_txn()  # need to start a cache transaction, otherwise commit() does nothing.
        cache.key = key_new
        cache.commit()

        loc = key_new.find_key() if hasattr(key_new, "find_key") else None
        if args.keep:
            logger.info(f"Key copied to {loc}")
        else:
            key.remove(key.target)  # remove key from current location
            logger.info(f"Key moved to {loc}")

        return EXIT_SUCCESS

    @with_repository(lock=False, exclusive=False, manifest=False, cache=False)
    def do_key_export(self, args, repository):
        """Export the repository key for backup"""
        manager = KeyManager(repository)
        manager.load_keyblob()
        if args.paper:
            manager.export_paperkey(args.path)
        else:
            try:
                if os.path.isdir(args.path):
                    raise IsADirectoryError
                if args.qr:
                    manager.export_qr(args.path)
                else:
                    manager.export(args.path)
            except IsADirectoryError:
                self.print_error(f"'{args.path}' must be a file, not a directory")
                return EXIT_ERROR
        return EXIT_SUCCESS

    @with_repository(lock=False, exclusive=False, manifest=False, cache=False)
    def do_key_import(self, args, repository):
        """Import the repository key from backup"""
        manager = KeyManager(repository)
        if args.paper:
            if args.path:
                self.print_error("with --paper import from file is not supported")
                return EXIT_ERROR
            manager.import_paperkey(args)
        else:
            if not args.path:
                self.print_error("input file to import key from expected")
                return EXIT_ERROR
            if args.path != "-" and not os.path.exists(args.path):
                self.print_error("input file does not exist: " + args.path)
                return EXIT_ERROR
            manager.import_keyfile(args)
        return EXIT_SUCCESS

    def build_parser_keys(self, subparsers, common_parser, mid_common_parser):
        from ._common import process_epilog

        subparser = subparsers.add_parser(
            "key",
            parents=[mid_common_parser],
            add_help=False,
            description="Manage a keyfile or repokey of a repository",
            epilog="",
            formatter_class=argparse.RawDescriptionHelpFormatter,
            help="manage repository key",
        )

        key_parsers = subparser.add_subparsers(title="required arguments", metavar="<command>")
        subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))

        key_export_epilog = process_epilog(
            """
        If repository encryption is used, the repository is inaccessible
        without the key. This command allows one to back up this essential key.
        Note that the backup produced does not include the passphrase itself
        (i.e. the exported key stays encrypted). In order to regain access to a
        repository, one needs both the exported key and the original passphrase.

        There are three backup formats. The normal backup format is suitable for
        digital storage as a file. The ``--paper`` backup format is optimized
        for printing and typing in while importing, with per line checks to
        reduce problems with manual input. The ``--qr-html`` creates a printable
        HTML template with a QR code and a copy of the ``--paper``-formatted key.

        For repositories using keyfile encryption the key is saved locally
        on the system that is capable of doing backups. To guard against loss
        of this key, the key needs to be backed up independently of the main
        data backup.

        For repositories using the repokey encryption the key is saved in the
        repository in the config file. A backup is thus not strictly needed,
        but guards against the repository becoming inaccessible if the file
        is damaged for some reason.

        Examples::

            borg key export /path/to/repo > encrypted-key-backup
            borg key export --paper /path/to/repo > encrypted-key-backup.txt
            borg key export --qr-html /path/to/repo > encrypted-key-backup.html
            # Or pass the output file as an argument instead of redirecting stdout:
            borg key export /path/to/repo encrypted-key-backup
            borg key export --paper /path/to/repo encrypted-key-backup.txt
            borg key export --qr-html /path/to/repo encrypted-key-backup.html


        """
        )
        subparser = key_parsers.add_parser(
            "export",
            parents=[common_parser],
            add_help=False,
            description=self.do_key_export.__doc__,
            epilog=key_export_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter,
            help="export repository key for backup",
        )
        subparser.set_defaults(func=self.do_key_export)
        subparser.add_argument("path", metavar="PATH", nargs="?", type=str, help="where to store the backup")
        subparser.add_argument(
            "--paper",
            dest="paper",
            action="store_true",
            help="Create an export suitable for printing and later type-in",
        )
        subparser.add_argument(
            "--qr-html",
            dest="qr",
            action="store_true",
            help="Create an html file suitable for printing and later type-in or qr scan",
        )

        key_import_epilog = process_epilog(
            """
        This command restores a key previously backed up with the export command.

        If the ``--paper`` option is given, the import will be an interactive
        process in which each line is checked for plausibility before
        proceeding to the next line. For this format PATH must not be given.

        For repositories using keyfile encryption, the key file which ``borg key
        import`` writes to depends on several factors. If the ``BORG_KEY_FILE``
        environment variable is set and non-empty, ``borg key import`` creates
        or overwrites that file named by ``$BORG_KEY_FILE``. Otherwise, ``borg
        key import`` searches in the ``$BORG_KEYS_DIR`` directory for a key file
        associated with the repository. If a key file is found in
        ``$BORG_KEYS_DIR``, ``borg key import`` overwrites it; otherwise, ``borg
        key import`` creates a new key file in ``$BORG_KEYS_DIR``.
        """
        )
        subparser = key_parsers.add_parser(
            "import",
            parents=[common_parser],
            add_help=False,
            description=self.do_key_import.__doc__,
            epilog=key_import_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter,
            help="import repository key from backup",
        )
        subparser.set_defaults(func=self.do_key_import)
        subparser.add_argument(
            "path", metavar="PATH", nargs="?", type=str, help="path to the backup ('-' to read from stdin)"
        )
        subparser.add_argument(
            "--paper",
            dest="paper",
            action="store_true",
            help="interactively import from a backup done with ``--paper``",
        )

        change_passphrase_epilog = process_epilog(
            """
        The key files used for repository encryption are optionally passphrase
        protected. This command can be used to change this passphrase.

        Please note that this command only changes the passphrase, but not any
        secret protected by it (like e.g. encryption/MAC keys or chunker seed).
        Thus, changing the passphrase after passphrase and borg key got compromised
        does not protect future (nor past) backups to the same repository.
        """
        )
        subparser = key_parsers.add_parser(
            "change-passphrase",
            parents=[common_parser],
            add_help=False,
            description=self.do_change_passphrase.__doc__,
            epilog=change_passphrase_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter,
            help="change repository passphrase",
        )
        subparser.set_defaults(func=self.do_change_passphrase)

        change_location_epilog = process_epilog(
            """
        Change the location of a borg key. The key can be stored at different locations:

        - keyfile: locally, usually in the home directory
        - repokey: inside the repo (in the repo config)

        Please note:

        This command does NOT change the crypto algorithms, just the key location,
        thus you must ONLY give the key location (keyfile or repokey).
        """
        )
        subparser = key_parsers.add_parser(
            "change-location",
            parents=[common_parser],
            add_help=False,
            description=self.do_change_location.__doc__,
            epilog=change_location_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter,
            help="change key location",
        )
        subparser.set_defaults(func=self.do_change_location)
        subparser.add_argument(
            "key_mode", metavar="KEY_LOCATION", choices=("repokey", "keyfile"), help="select key location"
        )
        subparser.add_argument(
            "--keep",
            dest="keep",
            action="store_true",
            help="keep the key also at the current location (default: remove it)",
        )
