# -*- coding: utf-8 -*-

""" conda_content_trust.cli
This module provides the CLI interface for conda-content-trust.
This is intended to provide a command-line signing and metadata update
interface.
"""

# Python2 Compatibility
from __future__ import absolute_import, division, print_function, unicode_literals

import json
from argparse import ArgumentParser
import copy
import json

from conda_content_trust.common import (
        canonserialize, load_metadata_from_file, write_metadata_to_file,
        CCT_Error, PrivateKey, is_gpg_fingerprint, is_hex_key)

from conda_content_trust import __version__
import conda_content_trust.root_signing as cct_root_signing
import conda_content_trust.signing as cct_signing
import conda_content_trust.authentication as cct_authentication
import conda_content_trust.metadata_construction as cct_metadata_construction

# In Python2, input() performs evaluation and raw_input() does not.  In
# Python3, input() does not perform evaluation and there is no raw_input().
# So... use raw_input in Python2, and input in Python3.
try:                     # pragma: no cover
    _input_func = raw_input
except NameError:        # pragma: no cover
    _input_func = input


def cli(args=None):
    p = ArgumentParser(
        description="Signing and verification tools for Conda",
        conflict_handler='resolve'
    )
    p.add_argument(
        '-V', '--version',
        action='version',
        help='Show the conda-content-trust version number and exit.',
        version="conda-content-trust %s" % __version__,
    )

    # Create separate parsers for the subcommands.
    sp = p.add_subparsers(title='subcommands', dest='subcommand_name')


    # subcommand: sign-artifacts

    p_signrepo = sp.add_parser(
            'sign-artifacts', help=('Given a repodata.json '
            'file, produce signatures over the metadata for each artifact listed, '
            'and update the repodata.json file with their individual signatures.'))
    p_signrepo.add_argument(
            'repodata_fname', help=('the filename of a repodata.json file from '
            'which to retrieve metadata for individual artifacts.'))
    p_signrepo.add_argument(
            'private_key_hex', help=('the ed25519 private key to be used to '
            'sign each artifact\'s metadata'))


    # subcommand: verify-metadata

    p_verifymd = sp.add_parser(
            'verify-metadata', help=('Uses the first (trusted) metadata file '
            'to verify the second (not yet trusted) metadata file.  For '
            'example, '
            '"conda-content-trust verify-metadata 4.root.json 5.root.json"'
            ' to verify version 5 of root based on version 4 of root, or '
            '"conda-content-trust verify-metadata 4.root.json key_mgr.json" '
            'to verify key manager metadata based on version 4 of root.'))
    p_verifymd.add_argument(
            'trusted_metadata_filename', help=('the filename of the '
            'already-trusted metadata file that sets the rules for verifying '
            'the untrusted metadata file'))
    p_verifymd.add_argument(
            'untrusted_metadata_filename', help=('the filename of the '
            '(untrusted) metadata file to verify'))


    # subcommand: modify-metadata

    p_modifymd = sp.add_parser(
            'modify-metadata', help=('Interactive metadata modification.  Use '
            'this to produce a new version of a metadata file (like root.json '
            'or key_mgr.json), or correct an error in an unpublished metadata '
            'file, or review and sign a metadata file.  This increments '
            'version number / timestamp, reports changes on console, etc. For '
            'example, "conda-content-trust modify-metadata 8.root.json" '
            'for assistance in '
            'producing a new version of root (version 9) using version 8.'))
    p_modifymd.add_argument(
            'metadata_filename', help=('the filename of the existing metadata '
            'file to modify'))


    # If we're missing optional requirements for the next few options, note
    # that in their help strings.
    opt_reqs_str = ''
    if not cct_root_signing.SSLIB_AVAILABLE:
        opt_reqs_str = ('[Unavailable]: Requires optional '
                'dependencies: securesystemslib and gpg.  ')


    # subcommand: gpg-key-lookup
    p_gpglookup = sp.add_parser('gpg-key-lookup', help=(opt_reqs_str +
        'Given the OpenPGP fingerprint of an ed25519-type OpenPGP key, fetch '
        'the actual ed25519 public key value of the underlying key.'))
    p_gpglookup.add_argument(
        'gpg_key_fingerprint',
        help=('the 40-hex-character key fingerprint (long keyid) for the '
        'OpenPGP/GPG key that you want to sign something with.  Do not '
        'add prefix "0x".'))


    # subcommand: gpg-sign

    p_gpgsign = sp.add_parser('gpg-sign', help=(opt_reqs_str + 'Sign a given '
        'piece of metadata using GPG instead of the usual signing '
        'mechanisms.  Takes an OpenPGP key fingerprint and a filename.'))
    p_gpgsign.add_argument(
        'gpg_key_fingerprint',
        help=('the 40-hex-character key fingerprint (long keyid) for the '
        'OpenPGP/GPG key that you want to sign something with.  Do not '
        'add prefix "0x".'))
    p_gpgsign.add_argument(
        'filename',
        help=('the filename of the file that will be signed'))



    args = p.parse_args(args)

    if args.subcommand_name == 'gpg-sign':

        # TODO: Validate arguments.

        # Strip any whitespace from the key fingerprint and lowercase it.
        # GPG pops out keys in a variety of whitespace arrangements and cases,
        # so this is necessary for convenience.
        gpg_key_fingerprint = ''.join(args.gpg_key_fingerprint.split()).lower()

        cct_root_signing.sign_root_metadata_via_gpg(
                args.filename, gpg_key_fingerprint)



    elif args.subcommand_name == 'sign-artifacts':

        cct_signing.sign_all_in_repodata(
                args.repodata_fname, args.private_key_hex)



    elif args.subcommand_name == 'gpg-key-lookup':
        gpg_key_fingerprint = ''.join(args.gpg_key_fingerprint.split()).lower()
        keyval = cct_root_signing.fetch_keyval_from_gpg(gpg_key_fingerprint)
        print('Underlying ed25519 key value: ' + str(keyval))


    elif args.subcommand_name == 'modify-metadata':

        # `conda-content-trust update-metadata <metadata file to produce new version of>`

        # underlying functions: build_delegating_metadata,
        # load_metadata_from_file

        # given a metadata file, increment the version number and timestamps,
        # reporting the changes on the console

        # strip signatures

        # indicate what signatures are required

        # ask if the user wants to sign; query for the key hex or fname;
        # ideally, offer this functionality for both root and non-root keys.
        # For root metadata, we can (and should) also report which keys are
        # expected / still needed in order for the metadata to be verifiable
        # according to the old metadata and the new metadata

        old_metadata = load_metadata_from_file(args.metadata_filename)

        # new_metadata = cct_metadata_construction.interactive_modify_metadata(old_metadata)
        # if new_metadata is not None and new_metadata:
        #     write_metadata_to_file(new_metadata, args.metadata_filename)

        interactive_modify_metadata(old_metadata)



    elif args.subcommand_name == 'verify-metadata':


        # `conda-content-trust verify-metadata <trusted delegating metadata> <untrusted
        # metadata> <(optional) role name>`

        # underlying functions: cct_authentication.verify_delegation,
        # load_metadata_from_file

        # takes two metadata files, the first being a trusted file that should
        # provide the verification criteria (expected keys and expected number
        # of keys) for the second file.  This should support root-root
        # verification (root chaining as currently implemented in
        # conda-content-trust) and delegation from one metadata type to another
        # (e.g. root to key_mgr)

        # conveys to the user whether or not the file is trusted, and for what
        # role.  e.g., would convey that the first file is (e.g.) a root
        # metadata file, that it provides a delegation to <role name>, and that
        # the <untrusted metadata> file provides <role name> and is signed
        # appropriately based on what the root metadata file requires of that
        # delegation.


        untrusted_metadata = load_metadata_from_file(
                args.untrusted_metadata_filename)

        trusted_metadata = load_metadata_from_file(
                args.trusted_metadata_filename)

        # TODO✅: Argument validation via the check_format_* calls.

        metadata_type = untrusted_metadata['signed']['type']

        if metadata_type == 'root':
            # Verifying root has additional steps beyond verify_delegation.
            try:
                cct_authentication.verify_root(trusted_metadata, untrusted_metadata)
                print('Root metadata verification successful.')
                return 0 # success

            except CCT_Error as e:
                errorcode = 10
                errorstring = str(e)

        else:
            # Verifying anything other than root just uses verify_delegation
            # directly.
            try:
                cct_authentication.verify_delegation(
                        delegation_name=metadata_type,
                        untrusted_delegated_metadata=untrusted_metadata,
                        trusted_delegating_metadata=trusted_metadata)
                print('Metadata verification successful.')
                return 0 # success

            except CCT_Error as e:
                errorcode = 20
                errorstring = str(e)


        # We should only get here if verification failed.
        print(
                'Verification of untrusted metadata failed.  Metadata '
                'type was "' + metadata_type + '".  Error reads:\n  "'
                + errorstring + '"')
        return errorcode   # failure; exit code





    else:
        print('No command provided.  Please use  "conda-content-trust -h" for help.')




def interactive_modify_metadata(metadata):
    """
    """

    # Update version if there is a version.
    # Update timestamp if there is a timestamp.
    #
    # Show metadata contents ('signed') -- pprint?
    #    indicate updated version/timestamp
    #
    # Changes phase:
    #    Prompt to
    #       (m) modify a value, (a) add a new entry, (d) delete an entry,
    #       (r) revert to original, (f) finish and sign ((move on to signing
    #       prompts))
    #
    # Signing phase:
    #   Show metadata again, ask if metadata looks right
    #   Show what keys the original was signed by and ask if those should be
    #     the keys used for the new version.
    #        ((Later: if root, vet against contents of new and old root versions))
    #   Prompt for key (raw key file, raw key data, or gpg key fingerprint)
    #   Sign using the given key (gpg if gpg, else normal signing mechanism).
    #   Write (making sure not to overwrite, and -- if root -- making sure to
    #     prepend "<version>." to root.json file.

    initial_metadata = metadata
    metadata = copy.deepcopy(initial_metadata)

    try:
        import pygments
        import pygments.lexers
        import pygments.formatters
    except ImportError:
        print(
                'interactive modify-metadata mode employs pygments for syntax '
                'highlighting, if pygments is available.  pygments was not '
                'found, so the JSON contents will be... uglier than they '
                'would otherwise be.  If you would like syntax highlighting '
                'and prettier printing of JSON, you may install pygments.')
        pygments = None
        from pprint import pprint

    # Build the modification options and prompt.
    def promptfor(s):
        return _input_func(F_INSTRUCT + '\n----- Please provide ' + s + ENDC + ': ')

    def fn_write():
        fname = promptfor('a filename to save this metadata as')
        print('Writing to file....')
        write_metadata_to_file(metadata, fname)
        print('Modified metadata written!')
        return 1

    def fn_abort():
        # TODO✅: Ask to confirm.
        print(RED + BOLD + '\nAborting!\n' + ENDC)
        return 1

    def fn_addsig():
        if not cct_root_signing.SSLIB_AVAILABLE:
            print(F_OPTS + 'Signing.  ' + RED + 'Please ABORT (control-c) if '
                    'the metadata above is not EXACTLY what you want to sign!'
                    + ENDC)
        key = promptfor(
                'a key: either:\n     - a 40-character-hex-string GPG PUBLIC '
                'key fingerprint\n'
                '       for GPG keys (e.g. root YubiKeys), or \n     - a '
                '64-character-hex-string PRIVATE key value for normal '
                'keys.\n\n     Whitespace will be removed and characters will '
                'be lowercased.\n     Key')
        key = ''.join(key.split()).lower()

        if is_hex_key(key):
            private_key = PrivateKey.from_hex(key)
            cct_signing.sign_signable(metadata, private_key)
            print(F_OPTS + '\n\n--- Successfully signed!  Please save.' + ENDC)

        elif is_gpg_fingerprint(key):
            try:
                cct_root_signing.sign_root_metadata_dict_via_gpg(metadata, key)
            except:
                print(F_OPTS + '\n\n--- ' + RED + 'Signing FAILED.'
                        + F_OPTS + '  Do you have this key loaded in GPG on '
                        'this system?')
            else:
                print(F_OPTS + '\n\n--- Successfully signed!  Please save.' + ENDC)

        else:
            print(F_OPTS + RED + 'Unable to recognize key.  Please try again.'
                + ENDC)
        return 0
    def fn_remsig():
        return 0
    def fn_update():
        return 0
    def fn_adddel():
        return 0
    def fn_remdel():
        return 0
    def fn_thresh():
        delegation = promptfor('a delegation name (one of the entries in the'
                '\n     "delegations" dictionary in the metadata above).  '
                'This will\n     be the delegation whose threshold number of '
                'required keys we\n     will change.')
        if delegation not in metadata['signed']['delegations']:
            print(F_OPTS + '\n\n--- ' + RED + 'Unable to find that delegation.'
                    '  Please try again.' + ENDC)
            return 0

        new_thresh = promptfor('a new threshold value.  The current value is '
                 + str(metadata['signed']['delegations'][delegation]['threshold']))

        try:
            new_thresh = int(new_thresh)
            assert new_thresh >= 1
        except:
            print(F_OPTS + '\n--- ' + RED + 'Invalid value.  Expecting integer '
                    'greater than or equal to 1.  Please try again.' + ENDC)
            return 0

        metadata['signed']['delegations'][delegation]['threshold'] = new_thresh

        print(F_OPTS + '\n--- Threshold successfully updated.' + ENDC)

        return 0
    def fn_addkey():
        return 0
    def fn_remkey():
        return 0

    options = {
        0: [fn_write, 'Done: write and save metadata'],
        1: [fn_abort, 'Abort: discard changes -- abort without writing'],
        2: [fn_addsig, 'Add a signature (sign with a key you have)'],
        3: [fn_remsig, 'Remove a signature'],
        4: [fn_update, 'Update any top-level dictionary entry'],
        5: [fn_adddel, 'Add a delegation'],
        6: [fn_remdel, 'Remove a delegation'],
        7: [fn_thresh, 'Change the threshold number of keys for a delegation'],
        8: [fn_addkey, 'Add an authorized key to a delegation'],
        9: [fn_remkey, 'Remove an authorized key from a delegation']}


    option_text = (
            F_INSTRUCT + '\n--- Please choose an operation by entering its '
            'number\n' + ENDC)
    for index in options:
        option_text += ('    ' + F_LABEL + str(index) + ENDC + ': '
                + options[index][1] + ENDC + '\n')


    done = False
    while not done:

        print(F_OPTS + BOLD + '\n\n---------------------\n--- Current metadata:\n---------------------\n' + ENDC)

        if pygments is not None:
            formatted_metadata = json.dumps(metadata, sort_keys=True, indent=4)
            print(pygments.highlight(
                    formatted_metadata.encode('utf-8'),
                    pygments.lexers.JsonLexer(),
                    pygments.formatters.TerminalFormatter()))
        else:
            pprint(metadata)

        print(option_text)
        selected = _input_func(F_OPTS + 'Choice: ' + ENDC)
        try:
            selected = int(selected)
        except:
            print(RED + BOLD + '\nInvalid entry.  Try again.\n' + ENDC)
            continue
        if selected not in options:
            print(RED + BOLD + '\nInvalid entry.  Try again.\n' + ENDC)
            continue

        print(F_OPTS + '\nChose "' + options[selected][1] + '"' + ENDC)

        done = options[selected][0]() # Run the func associated with the option.


    ### Pull modified from debugging script
    ### Pull modified from debugging script
    ### Pull modified from debugging script



# Basic text formatting string constants
PINK = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'

# Complete formats
F_LABEL = ENDC + UNDERLINE + BOLD + PINK
F_INSTRUCT = ENDC + BOLD + PINK
F_OPTS = ENDC + GREEN





if __name__ == '__main__':
    import sys
    exit_status = cli(sys.argv[1:])
    sys.exit(exit_status)
