"""
Converge the set of installed virtualenvs.

"""
from datetime import datetime
from importlib import metadata
import subprocess
import sys
import warnings

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

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(f"Converging {name!r} failed!\n")


@_FILESYSTEM
@_LINK_DIR
@_ROOT
@click.option(
    "--fail-fast",
    "handle_error",
    flag_value=_fail,
    type=lambda value: value,
    help="Fail if any virtualenv cannot be converged.",
)
@click.option(
    "--no-fail-fast",
    "handle_error",
    default=True,
    flag_value=_do_not_fail,
    type=lambda value: value,
    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 to, name in config.link.items():
            _link(
                source=virtualenv.binary(name=name),
                to=link_dir.descendant(to),
                filesystem=filesystem,
            )

        for to, name 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)

    # REMOVEME: When tqdm/tqdm#1517 is closed.
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        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=metadata.version("venvs"),
        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)
