"""
Converge the set of installed virtualenvs.

"""
from datetime import datetime
import json
import subprocess
import sys

try:
    from functools import lru_cache
except ImportError:
    from functools32 import lru_cache

from filesystems.exceptions import FileExists, FileNotFound
from pyrsistent import thaw
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):
    raise


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


@lru_cache()
def _version_of(python):
    return subprocess.check_output(
        [python, "--version"],
        stderr=subprocess.STDOUT,
    ).decode("ascii")


@_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.",
)
def main(filesystem, locator, link_dir, handle_error):
    """
    Converge the configured set of tracked virtualenvs.
    """
    for name, config in _loop(
        config=Config.from_locator(filesystem=filesystem, locator=locator),
        handle_error=handle_error,
    ):
        python = config["python"]
        config = config.set("sys.version", _version_of(python))
        json_config = json.dumps(thaw(config), ensure_ascii=False, indent=2)

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

        try:
            existing_config = json.loads(
                filesystem.get_contents(existing_config_path),
            )
        except FileNotFound:
            pass
        else:
            if existing_config == json.loads(json_config):
                continue
        virtualenv.recreate_on(filesystem=filesystem, python=python)

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

        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,
            )

        filesystem.set_contents(
            existing_config_path,
            mode="t",
            contents=json_config,
        )


def _loop(config, handle_error):
    progress = tqdm(iterable=config, total=len(config), unit="venv")
    iterable = iter(progress)
    while True:
        try:
            name, config = next(iterable)
        except StopIteration:
            return
        except Exception:
            handle_error(None)
        else:
            progress.set_description(name)
            yield name, config


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)

@@ -1824,85 +1824,8 @@
 on))
-%0A        json_config = json.dumps(thaw(config), ensure_ascii=False, indent=2)
 %0A%0A  
@@ -2168,31 +2168,14 @@
  == 
-json.loads(json_
 config
-)
 :%0A  
@@ -3114,23 +3114,66 @@
 nts=json
-_config
+.dumps(thaw(config), ensure_ascii=False, indent=2)
 ,%0A      

Remove the unnecesary roundtrip.