#!/usr/bin/env python3
################################################################################
# Retrieves one or more values from a YAML file at a specified YAML Path.
# Output is printed to STDOUT, one line per match.  When a result is a complex
# data-type (Array or Hash), a Python-compatible dump is produced to represent
# the entire complex result.  EYAML can be employed to decrypt the values.
#
# Requirements:
# 1. Python >= 3.6
#    * CentOS:  yum -y install epel-release \
#        && yum -y install python36 python36-pip
# 2. The ruamel.yaml module, version >= 0.15
#    * CentOS:  pip3 install ruamel.yaml
#
# Copyright 2018, 2019 William W. Kimball, Jr. MBA MSIS
################################################################################
import sys
import argparse
import pprint
from os import access, R_OK
from os.path import isfile

from ruamel.yaml import YAML
from ruamel.yaml.parser import ParserError

from yamlpath.exceptions import YAMLPathException, EYAMLCommandException
from yamlpath.parser import Parser
from yamlpath.eyaml import EYAMLPath

from yamlpath.wrappers import ConsolePrinter

# Implied Constants
MY_VERSION = "1.0.0"

def processcli():
    """Process command-line arguments."""
    parser = argparse.ArgumentParser(
        description="Gets one or more values from a YAML file at a specified\
            YAML Path.  Can employ EYAML to decrypt values.",
        epilog="For more information about YAML Paths, please visit\
            https://github.com/wwkimball/yamlpath."
    )
    parser.add_argument("-V", "--version", action="version",
                        version="%(prog)s " + MY_VERSION)

    required_group = parser.add_argument_group("required settings")
    required_group.add_argument(
        "-p", "--query",
        required=True,
        metavar="YAML_PATH",
        help="YAML Path to query"
    )

    eyaml_group = parser.add_argument_group(
        "EYAML options", "Left unset, the EYAML keys will default to your\
         system or user defaults.  Both keys must be set when using EYAML.")
    eyaml_group.add_argument("-x", "--eyaml", default="eyaml",
        help="the eyaml binary to use when it isn't on the PATH")
    eyaml_group.add_argument("-r", "--privatekey", help="EYAML private key")
    eyaml_group.add_argument("-u", "--publickey", help="EYAML public key")

    noise_group = parser.add_mutually_exclusive_group()
    noise_group.add_argument("-d", "--debug", action="store_true",
        help="output debugging details")
    noise_group.add_argument("-v", "--verbose", action="store_true",
        help="increase output verbosity")
    noise_group.add_argument("-q", "--quiet", action="store_true",
        help="suppress all output except errors")

    parser.add_argument("yaml_file", metavar="YAML_FILE",
        help="the YAML file to query")
    return parser.parse_args()

def validateargs(args, log):
    """Validate command-line arguments."""
    has_errors = False

    # Enforce sanity
    # * When set, --privatekey must be a readable file
    if args.privatekey and not (
        isfile(args.privatekey) and access(args.privatekey, R_OK)
    ):
        log.error(
            "EYAML private key is not a readable file:  " + args.privatekey
        )

    # * When set, --publickey must be a readable file
    if args.publickey and not (
        isfile(args.publickey) and access(args.publickey, R_OK)
    ):
        log.error(
            "EYAML public key is not a readable file:  " + args.publickey
        )

    # * When either --publickey or --privatekey are set, the other must also be
    if (
        (args.publickey and not args.privatekey)
        or (args.privatekey and not args.publickey)
    ):
        log.error("Both private and public EYAML keys must be set.")

    if has_errors:
        exit(1)

def main():
    """Main code."""
    args = processcli()
    log = ConsolePrinter(args)
    validateargs(args, log)
    parser = Parser(log)
    processor = EYAMLPath(
        log,
        eyaml=args.eyaml,
        publickey=args.publickey,
        privatekey=args.privatekey,
        parser=parser
    )

    # Prep the YAML parser
    yaml = YAML()
    yaml.indent(mapping=2, sequence=4, offset=2)
    yaml.explicit_start = True
    yaml.preserve_quotes = True
    yaml.width = sys.maxsize

    # Attempt to open the YAML file; check for parsing errors
    try:
        with open(args.yaml_file, 'r') as f:
            yaml_data = yaml.load(f)
    except ParserError as ex:
        log.critical(
            "YAML parsing error {}:  {}"
            .format(str(ex.problem_mark).lstrip(), ex.problem)
            , 1
        )

    # Seek the queried value(s)
    discovered_nodes = []
    try:
        yaml_path = parser.str_path(args.query)
    except YAMLPathException as ex:
        log.critical(ex, 1)

    try:
        for node in processor.get_eyaml_values(
            yaml_data, yaml_path, mustexist=True
        ):
            if node is not None:
                log.debug("Got {} from {}.".format(node, yaml_path))
                discovered_nodes.append(node)
    except YAMLPathException as ex:
        log.critical(ex, 1)
    except EYAMLCommandException as ex:
        log.critical(ex, 2)

    if not discovered_nodes:
        log.critical("No matches for {}!".format(yaml_path), 3)

    pprinter = pprint.PrettyPrinter(indent=4)
    for node in discovered_nodes:
        pprinter.pprint(node)

if __name__ == "__main__":
    main()
