#!/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.4 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()
