"""
Converge the set of installed virtualenvs.

"""
from datetime import datetime
import subprocess
import sys

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

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


_SENTINEL = "# Auto-generated by venvs"
_MODULE_WRAPPER = """\
#!{python}
""" + _SENTINEL + """ v{version} on {now}.
# Don't modify this file, it may be replaced when re-converging.
import os
import sys
# No runpy.run_module because of https://bugs.python.org/issue37941 ...
argv = [sys.executable, "-m", "{module}"] + sys.argv[1:]
os.execvp(argv[0], argv)
"""


def _fail(virtualenv, name):
    raise


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


@_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.argument("venvs", nargs=-1)
def main(filesystem, locator, link_dir, handle_error, venvs):
    """
    Converge the configured set of tracked virtualenvs.
    """
    for config, virtualenv in _loop(
        filesystem=filesystem,
        locator=locator,
        handle_error=handle_error,
    ):
        if venvs and config.name not in venvs:
            continue

        try:
            virtualenv.recreate_on(filesystem=filesystem, python=config.python)
            virtualenv.install(
                packages=config.install,
                requirements=config.requirements,
            )
        except Exception:
            handle_error(virtualenv=virtualenv, name=config.name)
            continue

        filesystem.create_directory(
            link_dir,
            with_parents=True,
            allow_existing=True,
        )

        for name, to in config.link.items():
            _link(
                source=virtualenv.binary(name=name),
                to=link_dir.descendant(to),
                filesystem=filesystem,
            )

        for name, to in config.link_module.items():
            _write_module_wrapper(
                to=link_dir.descendant(to),
                python=virtualenv.binary(name="python"),
                module=name,
                filesystem=filesystem,
            )

        config.save(filesystem=filesystem, virtualenv=virtualenv)

        try:
            for command in config.post_commands:
                subprocess.run(command, check=True)
        except Exception:
            handle_error(virtualenv=virtualenv, name=config.name)
            continue


def _loop(filesystem, locator, handle_error):
    config = Config.from_locator(filesystem=filesystem, locator=locator)
    progress = tqdm(iterable=config, total=len(config), unit="venv")
    iterable = iter(progress)
    while True:
        try:
            venv_config = next(iterable)
        except StopIteration:
            return
        except Exception:
            handle_error(virtualenv=None, name=None)
        else:
            progress.set_description(venv_config.name)
            venv = locator.for_name(name=venv_config.name)
            if venv_config.matches_existing(venv, filesystem=filesystem):
                continue
            yield venv_config, venv


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)


def _write_module_wrapper(to, python, module, filesystem):
    """
    Write a module wrapper for the given module.

    A module wrapper converts `python -m foo` into a bare `foo` executable.
    """
    wrapper = _MODULE_WRAPPER.format(
        python=python,
        module=module,
        version=__version__,
        now=datetime.now(),
    )

    try:
        with filesystem.open(to) as file:
            file.readline()  # shebang
            second_line = file.readline()
    except FileNotFound:
        pass
    else:
        if not second_line.startswith(_SENTINEL):
            raise FileExists(to)

    filesystem.set_contents(to, wrapper)
