#!/usr/bin/env python
# We're using optparse because we need to support 2.6
# which doesn't have argparse.  Given that argparse is
# a dependency that eventually gets installed, we could
# try to bootstrap, but using optparse is just easier.
import optparse
import os
import platform
import shutil
import subprocess
import sys
import tarfile
import tempfile
from contextlib import contextmanager

PACKAGES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "packages")
INSTALL_DIR = os.path.expanduser(os.path.join("~", ".local", "lib", "cfn-lint"))
GTE_PY37 = sys.version_info[:2] >= (3, 7)
UNSUPPORTED_PYTHON = (sys.version_info[0] == 2 and sys.version_info[:2] <= (2, 6)) or (
    sys.version_info[0] == 3 and sys.version_info[:2] <= (3, 3)
)
INSTALL_ARGS = "--no-binary :all: --no-build-isolation --no-cache-dir --no-index "


class BadRCError(Exception):
    pass


class MultipleBundlesError(Exception):
    pass


@contextmanager
def cd(dirname):
    original = os.getcwd()
    os.chdir(dirname)
    try:
        yield
    finally:
        os.chdir(original)


def run(cmd):
    sys.stdout.write("Running cmd: %s\n" % cmd)
    p = subprocess.Popen(
        cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
    stdout, stderr = p.communicate()
    if p.returncode != 0:
        output = (stdout + stderr).decode("utf-8")
        raise BadRCError("Bad rc (%s) for cmd '%s': %s" % (p.returncode, cmd, output))
    return stdout


def bin_path():
    """
    Get the system's binary path, either `bin` on reasonable
    systems or `Scripts` on Windows.
    """
    path = "bin"

    if platform.system() == "Windows":
        path = "Scripts"

    return path


def create_install_structure(working_dir, install_dir):
    if not os.path.isdir(install_dir):
        os.makedirs(install_dir)
    create_virtualenv(location=install_dir, working_dir=working_dir)


def _create_virtualenv_internal(location, working_dir):
    # On py3 we use the built in venv to create our virtualenv.
    # There's a bug with sys.executable on external virtualenv
    # that causes installation failures.
    run("%s -m venv %s" % (sys.executable, location))


def _create_virtualenv_external(location, working_dir):
    # working_dir is used (generally somewhere in /tmp) so that we
    # don't modify the install/packages directories.
    with cd(PACKAGES_DIR):
        venv = _get_venv_package_tarball(".")
        compressed = tarfile.open(venv)
        compressed.extractall(path=working_dir)
        compressed.close()
    with cd(working_dir):
        # We know that virtualenv is the only dir in this directory
        # so we can listdir()[0] it.
        with cd(os.listdir(".")[0]):
            run(
                ("%s virtualenv.py --no-download --python %s %s")
                % (sys.executable, sys.executable, location)
            )


def _get_package_tarball(package_dir, package_prefix):
    package_filenames = sorted(
        [p for p in os.listdir(package_dir) if p.startswith(package_prefix)]
    )
    return package_filenames[-1]


def _get_venv_package_tarball(package_dir):
    return _get_package_tarball(package_dir, "virtualenv")


def create_working_dir():
    d = tempfile.mkdtemp()
    return d


def pip_install_packages(install_dir):
    cli_tarball = [p for p in os.listdir(PACKAGES_DIR) if p.startswith("cfn-lint")]
    if len(cli_tarball) != 1:
        message = (
            "Multiple versions of the CLI were found in %s. Please clear "
            "out this directory before proceeding."
        )
        raise MultipleBundlesError(message % PACKAGES_DIR)
    cli_tarball = cli_tarball[0]
    pip_script = os.path.join(install_dir, bin_path(), "pip")

    setup_requires_dir = os.path.join(PACKAGES_DIR, "setup")
    with cd(setup_requires_dir):
        _install_setup_deps(pip_script, ".")

    with cd(PACKAGES_DIR):
        run(
            "%s install %s --find-links file://%s %s"
            % (pip_script, INSTALL_ARGS, PACKAGES_DIR, cli_tarball)
        )


def _install_setup_deps(pip_script, setup_package_dir):
    # Some packages declare `setup_requires`, which is a list of dependencies
    # to be used at setup time. These need to be installed before anything
    # else, and pip doesn't manage them.  We have to manage this ourselves
    # so for now we're explicitly installing the one setup_requires package
    # we need.  This comes from python-dateutils.
    setuptools_scm_tarball = _get_package_tarball(setup_package_dir, "setuptools_scm")
    run(
        "%s install --no-binary :all: --no-cache-dir --no-index "
        "--find-links file://%s %s"
        % (pip_script, setup_package_dir, setuptools_scm_tarball)
    )
    wheel_tarball = _get_package_tarball(setup_package_dir, "wheel")
    run(
        "%s install --no-binary :all: --no-cache-dir --no-index "
        "--find-links file://%s %s" % (pip_script, setup_package_dir, wheel_tarball)
    )


def create_symlink(real_location, symlink_name):
    if os.path.isfile(symlink_name):
        print("Symlink already exists: %s" % symlink_name)
        print("Removing symlink.")
        os.remove(symlink_name)
    symlink_dir_name = os.path.dirname(symlink_name)
    if not os.path.isdir(symlink_dir_name):
        os.makedirs(symlink_dir_name)
    os.symlink(real_location, symlink_name)
    return True


def main():
    parser = optparse.OptionParser()
    parser.add_option(
        "-i",
        "--install-dir",
        help="The location to install "
        "the CloudFormation Linter. "
        "The default value is ~/.local/lib/cfn-lint",
        default=INSTALL_DIR,
    )
    parser.add_option(
        "-b",
        "--bin-location",
        help="If this argument is "
        "provided, then a symlink will be created at this "
        "location that points to the cfn-lint executable. "
        "This argument is useful if you want to put the cfn-lint "
        "executable somewhere already on your path, e.g. "
        "-b /usr/local/bin/cfn-lint.  This is an optional argument. "
        "If you do not provide this argument you will have to "
        "add INSTALL_DIR/bin to your PATH.",
    )
    if UNSUPPORTED_PYTHON:
        unsupported_python_msg = (
            "Unsupported Python version detected: Python %s.%s\n"
            "To continue using this installer you must use Python 2.7 "
            "and greater or Python 3.5 and greater.\n"
            "For more information see the following blog post:\n"
            "https://aws.amazon.com/blogs/developer/deprecation-of-python-2-6"
            "-and-python-3-3-in-botocore-boto3-and-the-aws-cli/"
        )
        print(unsupported_python_msg % sys.version_info[:2])
        sys.exit(1)

    opts = parser.parse_args()[0]
    working_dir = create_working_dir()
    try:
        create_install_structure(working_dir, opts.install_dir)
        pip_install_packages(opts.install_dir)
        real_location = os.path.join(opts.install_dir, bin_path(), "cfn-lint")
        if opts.bin_location and create_symlink(real_location, opts.bin_location):
            print("You can now run: %s --version" % opts.bin_location)
        else:
            print("You can now run: %s --version" % real_location)
    finally:
        shutil.rmtree(working_dir)


if GTE_PY37:
    create_virtualenv = _create_virtualenv_internal
else:
    create_virtualenv = _create_virtualenv_external


if __name__ == "__main__":
    main()
