#!/usr/bin/env python
import hashlib
import os
import shutil
import subprocess
import sys
import tarfile

from gzip import GzipFile

from cleo import Application
from cleo import Command


WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt")


class MakeReleaseCommand(Command):
    """
    Makes a self-contained package of Poetry.

    release
        {--P|python=?* : Python version to use}
    """

    PYTHON = {
        "2.7": "python2.7",
        "3.4": "python3.4",
        "3.5": "python3.5",
        "3.6": "python3.6",
        "3.7": "python3.7",
        "3.8": "python3.8",
        "3.9": "python3.9",
    }

    def handle(self):
        pythons = self.PYTHON
        if self.option("python"):
            pythons = {}
            for python in self.option("python"):
                parts = python.split(":", 1)
                if len(parts) == 1:
                    python = self.PYTHON[parts[0]]
                    version = parts[0]
                else:
                    version, python = parts
                pythons[version] = python

        self.check_system(pythons)

        from poetry import __version__
        from poetry.factory import Factory
        from poetry.puzzle import Solver
        from poetry.repositories.pool import Pool
        from poetry.repositories.repository import Repository
        from poetry.utils._compat import Path
        from poetry.utils._compat import decode
        from poetry.utils._compat import encode
        from poetry.utils._compat import subprocess
        from poetry.utils.env import GET_BASE_PREFIX
        from poetry.utils.env import VirtualEnv
        from poetry.utils.helpers import temporary_directory
        from poetry.vcs import get_vcs

        project = Factory().create_poetry(Path.cwd())
        package = project.package
        del package.dev_requires[:]

        # We only use the lock file to resolve the dependencies
        pool = Pool()
        pool.add_repository(project.locker.locked_repository(with_dev_reqs=True))

        vcs = get_vcs(Path(__file__).parent)
        if vcs:
            vcs_excluded = [str(f) for f in vcs.get_ignored_files()]
        else:
            vcs_excluded = []

        created_files = []
        with temporary_directory() as tmp_dir:
            # Copy poetry to tmp dir
            poetry_dir = os.path.join(tmp_dir, "poetry")
            shutil.copytree(
                os.path.join(os.path.dirname(__file__), "poetry"),
                poetry_dir,
                ignore=lambda dir_, names: set(vcs_excluded).intersection(
                    set([os.path.join(dir_, name) for name in names])
                ),
            )
            created_files += [
                p.relative_to(Path(tmp_dir))
                for p in Path(poetry_dir).glob("**/*")
                if p.is_file()
                and p.suffix != ".pyc"
                and str(p.relative_to(Path(tmp_dir))) not in vcs_excluded
            ]
            for version, python in sorted(pythons.items()):
                self.line(
                    "<info>Preparing files for Python <comment>{}</comment></info>".format(
                        version
                    )
                )
                prefix = decode(
                    subprocess.run(
                        [python],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT,
                        input=encode(GET_BASE_PREFIX),
                        check=True,
                        shell=WINDOWS,
                    ).stdout
                )
                env = VirtualEnv(Path(prefix.strip()), base=Path(prefix.strip()))
                with package.with_python_versions("~{}".format(version)):
                    solver = Solver(package, pool, Repository(), Repository(), self.io)
                    ops = solver.solve()
                    for op in ops:
                        if not env.is_valid_for_marker(op.package.marker):
                            op.skip("Not needed for the current environment")

                self.vendorize_for_python(
                    python,
                    [op.package for op in ops if not op.skipped],
                    poetry_dir,
                    version,
                )
                vendor_dir = Path(
                    os.path.join(poetry_dir, "_vendor", "py{}".format(python))
                )
                created_files += [
                    p.relative_to(Path(tmp_dir))
                    for p in vendor_dir.glob("**/*")
                    if p.is_file()
                    and p.suffix != ".pyc"
                    and str(p.relative_to(Path(tmp_dir))) not in vcs_excluded
                ]

                self.line("")

            self.line("<info>Packaging files</info>")
            with temporary_directory() as tmp_dir2:
                base_name = "poetry-{}-{}".format(__version__, sys.platform)
                name = "{}.tar.gz".format(base_name)
                gz = GzipFile(os.path.join(tmp_dir2, name), mode="wb")
                try:
                    with tarfile.TarFile(
                        os.path.join(tmp_dir2, name),
                        mode="w",
                        fileobj=gz,
                        format=tarfile.PAX_FORMAT,
                    ) as tar:
                        for root, dirs, files in os.walk(tmp_dir):
                            for f in files:
                                if f.endswith(".pyc"):
                                    continue

                                path = os.path.join(os.path.realpath(root), f)

                                relpath = os.path.relpath(
                                    path, os.path.realpath(tmp_dir)
                                )

                                if relpath in vcs_excluded:
                                    continue

                                tar_info = tar.gettarinfo(str(path), arcname=relpath)

                                if tar_info.isreg():
                                    with open(path, "rb") as f:
                                        tar.addfile(tar_info, f)
                                else:
                                    tar.addfile(tar_info)  # Symlinks & ?
                finally:
                    gz.close()

                self.line("<info>Checking release file</info>")
                missing_files = []
                with tarfile.open(os.path.join(tmp_dir2, name), "r") as tar:
                    names = tar.getnames()

                    for created_file in created_files:
                        if created_file.as_posix() not in names:
                            missing_files.append(created_file.as_posix())

                if missing_files:
                    self.line("<error>Some files are missing:</error>")
                    for missing_file in missing_files:
                        self.line("<error>  - {}</error>".format(missing_file))

                    return 1

                releases_dir = os.path.join(os.path.dirname(__file__), "releases")
                if not os.path.exists(releases_dir):
                    os.mkdir(releases_dir)

                shutil.copyfile(
                    os.path.join(tmp_dir2, name), os.path.join(releases_dir, name)
                )

                # Compute hash
                sha = hashlib.sha256()
                with open(os.path.join(releases_dir, name), "rb") as f:
                    while True:
                        buffer = f.read(8192)
                        if not buffer:
                            break

                        sha.update(buffer)

                with open(
                    os.path.join(releases_dir, "{}.sha256sum".format(base_name)), "w"
                ) as f:
                    f.write(sha.hexdigest())

                self.line("<info>Built <comment>{}</comment></info>".format(name))

    def check_system(self, pythons):
        for version, python in sorted(pythons.items()):
            try:
                subprocess.check_output(
                    [python, "-V"], stderr=subprocess.STDOUT, shell=WINDOWS
                )
                if version == "3.4" and WINDOWS:
                    continue

                subprocess.check_output([python, "-m", "pip", "install", "pip", "-U"])
            except subprocess.CalledProcessError:
                raise RuntimeError("Python {} is not available".format(version))

    def vendorize_for_python(self, python, packages, dest, python_version):
        vendor_dir = os.path.join(dest, "_vendor", "py{}".format(python_version))

        bar = self.progress_bar(max=len(packages))
        bar.set_format("%message% %current%/%max%")
        bar.set_message(
            "<info>Vendorizing dependencies for Python <comment>{}</comment></info>".format(
                python_version
            )
        )
        bar.start()
        for package in packages:
            subprocess.check_output(
                [
                    python,
                    "-m",
                    "pip",
                    "install",
                    "{}=={}".format(package.name, package.version),
                    "--no-deps",
                    "--target",
                    vendor_dir,
                ],
                stderr=subprocess.STDOUT,
                shell=WINDOWS,
            )
            bar.advance()

        bar.finish()

        self.line("")


class MakeCommand(Command):
    """
    Build poetry releases.

    make
    """

    commands = [MakeReleaseCommand()]

    def handle(self):
        return self.call("help", self.config.name)


app = Application("sonnet")

app.add(MakeCommand())

if __name__ == "__main__":
    app.run()
