#!/usr/bin/env python
"""CLI Script Runner for Kingpin."""

import argparse
import asyncio
import json
import logging
import os
import sys

from kingpin import utils
from kingpin.actors import exceptions as actor_exceptions
from kingpin.actors import utils as actor_utils
from kingpin.actors.misc import Macro
from kingpin.version import __version__

log = logging.getLogger(__name__)


# We handle all the exceptions ourselves, so additional log statements from
# BOTO are not needed.
logging.getLogger("boto").setLevel(logging.CRITICAL)
logging.getLogger("botocore").setLevel(logging.CRITICAL)

# Initial option handler to set up the basic application environment.
parser = argparse.ArgumentParser(description=f"Kingpin v{__version__}")
parser.set_defaults(verbose=True)

# Job Configuration
parser.add_argument(
    "-j",
    "--json",
    "-s",
    "--script",
    dest="script",
    help="Path to JSON/YAML Deployment Script",
)
parser.add_argument(
    "-a",
    "--actor",
    dest="actor",
    help="Name of an Actor to execute (overrides --script)",
)
parser.add_argument(
    "-E",
    "--explain",
    dest="explain",
    action="store_true",
    help="Explain how an actor works. Requires --actor.",
    default=False,
)
parser.add_argument(
    "-p",
    "--param",
    dest="params",
    action="append",
    help="Actor Parameter to set (ie, warn_on_failure=true)",
    default=[],
)
parser.add_argument(
    "-o",
    "--option",
    dest="options",
    action="append",
    help="Actor Options to set (ie, elb_name=foobar)",
    default=[],
)
parser.add_argument(
    "-d", "--dry", dest="dry", action="store_true", help="Executes a dry run only."
)
parser.add_argument(
    "--build-only",
    dest="build_only",
    action="store_true",
    help="Compile the input JSON without executing any runs",
)
parser.add_argument(
    "--orgchart",
    dest="orgchart",
    help="Save the orgchart into file. Requires --build-only",
)

# Logging Configuration
parser.add_argument(
    "-l",
    "--level",
    dest="level",
    default="info",
    help="Set logging level (INFO|WARN|DEBUG|ERROR)",
)
parser.add_argument(
    "-D",
    "--debug",
    dest="level_debug",
    default=False,
    action="store_true",
    help="Equivalent to --level=DEBUG",
)
parser.add_argument(
    "-c",
    "--color",
    dest="color",
    default=False,
    action="store_true",
    help="Colorize the log output",
)

args = parser.parse_args()


def kingpin_fail(message: str):
    parser.print_help()
    sys.stderr.write(f"\nError: {message}\n")
    sys.exit(1)


def get_main_actor(dry: bool):
    env_tokens = dict(os.environ)

    # Cannot specify a script file an an actor at the same time.
    if args.script and args.actor:
        kingpin_fail("You may only specify --actor or --script, not both!")

    if args.actor:
        ActorClass = actor_utils.get_actor_class(args.actor)
        parameters = dict([i.split("=") for i in args.params])
        options = dict([i.split("=") for i in args.options])

        return ActorClass(
            options=options, dry=dry, init_tokens=env_tokens, **parameters
        )

    # Actor not specified. Process JSON file.
    try:
        script = args.script or sys.argv[1] if sys.argv else None
    except Exception as e:
        kingpin_fail(f"{e} You must specify --script or provide it as first argument.")

    return Macro(
        desc="Kingpin", options={"macro": script, "tokens": env_tokens}, dry=dry
    )


def _write_orgchart(path, data):
    with open(path, "w") as output:
        output.write(json.dumps(data))


async def main():

    if args.actor and args.explain:
        ActorClass = actor_utils.get_actor_class(args.actor)
        print(ActorClass.__doc__)
        sys.exit(0)

    if args.build_only:
        try:
            actor = get_main_actor(dry=False)
        except Exception as e:
            log.critical(e)
            sys.exit(1)

        if args.orgchart:
            log.info(f"Creating organizational chart into {args.orgchart}")
            try:
                orgdata = actor.get_orgchart()
            except Exception as e:
                log.critical(e)
                sys.exit(2)

            _write_orgchart(args.orgchart, orgdata)

        sys.exit(0)

    if not args.dry:
        # Lets maybe do a dry run first anyways...
        skip_dry = False
        try:
            skip_dry = utils.str2bool(os.getenv("SKIP_DRY", "False"), strict=True)
        except ValueError:
            log.warning("SKIP_DRY is not a valid boolean-like. Defaulting to False.")
            pass

        if skip_dry:
            # Okay, the user really does not want to do a dry run... fine
            log.warning("")
            log.warning("*** You have disabled the pre-check dry run.")
            log.warning("*** Execution will begin with no expectation of success.")
            log.warning("")
        else:
            log.info("Rehearsing... Break a leg!")

            try:
                dry_actor = get_main_actor(dry=True)
                await dry_actor.execute()
            except actor_exceptions.ActorException as e:
                log.critical("Dry run failed. Reason:")
                log.critical(e)
                sys.exit(2)

            log.info("Rehearsal OK! Performing!")

    # Begin doing real stuff!
    try:
        runner = get_main_actor(dry=args.dry)

        log.info("")
        log.warning("Lights, camera ... action!")
        log.info("")
        await runner.execute()
    except actor_exceptions.ActorException as e:
        log.error("Kingpin encountered mistakes during the play.")
        log.error(e)
        sys.exit(2)


def begin():
    # Set up logging before we do anything else
    if args.level_debug:
        args.level = "DEBUG"
    utils.setup_root_logger(level=args.level, color=args.color)

    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        log.info("CTRL-C Caught, shutting down")
        sys.exit(130)  # Standard KeyboardInterrupt exit code.
    except Exception:
        import traceback

        traceback.print_exc()
        sys.exit(3)


if __name__ == "__main__":  # pragma: no cover
    begin()
