"""
Converge the set of installed virtualenvs.

"""
from datetime import datetime
import os
import subprocess
import sys

from filesystems.exceptions import FileExists, FileNotFound
from tqdm import tqdm
import click
import toml

from venvs import __version__, _config
from venvs.common import _FILESYSTEM, _LINK_DIR, _ROOT


_MODULE_WRAPPER = """\
#!{python}
# Auto-generated by venvs v{version} on {now}.
import os
import sys
# No runpy.run_module because of https://bugs.python.org/issue37941 ...
argv = [sys.executable, "-m", "{name}"] + sys.argv[1:]
os.execvp(argv[0], argv)
"""


def _fail(virtualenv):
    raise


def _do_not_fail(virtualenv):
    sys.stderr.write("Converging {!r} failed!\n".format(virtualenv))


@click.command(context_settings=dict(help_option_names=["-h", "--help"]))
@_FILESYSTEM
@_LINK_DIR
@_ROOT
@click.option(
    "--fail-fast", "handle_error",
    flag_value=_fail,
    help="Fail if any virtualenv cannot be converged.",
)
@click.option(
    "--no-fail-fast", "handle_error",
    default=True,
    flag_value=_do_not_fail,
    help="Do not fail if a virtualenv cannot be converged.",
)
@click.version_option(version=__version__)
def main(filesystem, locator, link_dir, handle_error):
    contents = _config.load(filesystem=filesystem, locator=locator)
    versions = {}

    progress = tqdm(contents["virtualenv"].items())
    for name, config in progress:
        progress.set_description(name)

        python = config.pop("python", sys.executable)
        if python in versions:
            config["sys.version"] = versions[python]
        else:
            config["sys.version"] = versions[python] = subprocess.check_output(
                [python, "--version"],
                stderr=subprocess.STDOUT,
            ).decode("ascii")

        virtualenv = locator.for_name(name=name)
        existing_config_path = virtualenv.path / "installed.toml"

        try:
            with filesystem.open(existing_config_path) as existing_config:
                if toml.loads(existing_config.read()) == config:
                    continue
        except FileNotFound:
            virtualenv.create(python=python)
        else:
            virtualenv.recreate_on(filesystem=filesystem, python=python)

        packages = _interpolated(config.get("install", []))
        requirements = _interpolated(config.get("requirements", []))
        try:
            virtualenv.install(packages=packages, requirements=requirements)
        except Exception:
            handle_error(virtualenv)
            continue

        for link in config.get("link", []):
            name, _, to = link.partition(":")
            _link(
                source=virtualenv.binary(name=name),
                to=link_dir.descendant(to or name),
                filesystem=filesystem,
            )

        for each in config.get("link-module", []):
            name, _, to = each.partition(":")
            filesystem.create_with_contents(
                link_dir.descendant(to or name),
                _MODULE_WRAPPER.format(
                    python=virtualenv.binary(name="python"),
                    name=name,
                    version=__version__,
                    now=datetime.now(),
                ),
            )

        with filesystem.open(existing_config_path, "wt") as existing_config:
            existing_config.write(toml.dumps(config))


def _interpolated(iterable):
    return [os.path.expandvars(os.path.expanduser(each)) for each in iterable]


def _link(source, to, filesystem):
    """
    Link the given binary, replacing broken symlinks and erroring if existing.
    """

    try:
        filesystem.link(source=source, to=to)
    except FileExists as error:
        if filesystem.realpath(error.value) == filesystem.realpath(source):
            return
        if filesystem.exists(to):
            raise
        filesystem.remove(to)
        filesystem.link(source=source, to=to)
