from __future__ import print_function, absolute_import

import argparse
import os
import sys


def render_jinja(contents, filename):
    import jinja2
    import sys
    import platform

    jinja_dict = {
        "root": os.path.dirname(os.path.abspath(filename)),
        "os": os,
        "sys": sys,
        "platform": platform,
    }

    return jinja2.Template(contents).render(**jinja_dict)


def handle_includes(root_filename, root_yaml):
    # This is a depth-first search
    import yaml
    import collections
    queue = collections.OrderedDict({root_filename: root_yaml})
    visited = collections.OrderedDict()

    if root_yaml is None:
        raise ValueError("The root file '{root_filename}' is empty."
                         .format(root_filename=root_filename))

    while queue:
        filename, yaml_dict = queue.popitem()
        if filename in visited:
            continue

        for included_filename in yaml_dict.get("includes", []):
            included_filename = os.path.abspath(included_filename)
            if not os.path.isfile(included_filename):
                raise ValueError(
                    "Couldn't find the file '{included_filename}' "
                    "while processing the file '{filename}'."
                    .format(
                        included_filename=included_filename,
                        filename=filename
                    ))
            with open(included_filename, "r") as f:
                jinja_contents = render_jinja(f.read(), included_filename)
            included_yaml_dict = yaml.load(jinja_contents)
            if included_yaml_dict is None:
                raise ValueError("The file '{included_filename}' which was"
                                 " included by '{filename}' is empty."
                                 .format(
                                     included_filename=included_filename,
                                     filename=filename
                                 ))
            queue[included_filename] = included_yaml_dict

        if "includes" in yaml_dict:
            del yaml_dict["includes"]

        visited[filename] = yaml_dict

    return visited


def merge(dicts, keys_to_skip=('name',)):
    final_dict = {}

    for d in dicts:
        for key, value in d.items():
            if key in keys_to_skip:
                continue

            if key in final_dict:
                if isinstance(value, dict):
                    final_dict[key] = merge([final_dict[key], value])
                elif isinstance(value, list):
                    s = set()
                    s.update(final_dict[key])
                    s.update(value)
                    final_dict[key] = sorted(list(s))
                elif value is None:
                    continue
                else:
                    message = "Can't merge the key: '{key}' because it will override the previous value. " \
                              "Only lists and dicts can be merged. The type obtained was: {type}"\
                        .format(
                            key=key,
                            type=type(value)
                        )
                    raise ValueError(message)
            elif value is not None:
                final_dict[key] = value
    return final_dict


def load_yaml_dict(filename):
    with open(filename, "r") as f:
        contents = f.read()
    rendered_contents = render_jinja(contents, filename)

    import yaml
    root_yaml = yaml.load(rendered_contents)

    all_yaml_dicts = handle_includes(filename, root_yaml)

    for filename, yaml_dict in all_yaml_dicts.items():
        environment_key_value = yaml_dict.get("environment", {})
        if not isinstance(environment_key_value, dict):
            raise ValueError("The 'environment' key is supposed to be a dictionary, but you have the type "
                             "'{type}' at '{filename}'.".format(type=type(environment_key_value), filename=filename))

    merged_dict = merge(all_yaml_dicts.values())

    # Force the "name" because we want to keep the name of the root yaml
    merged_dict["name"] = root_yaml["name"]

    environment = merged_dict.pop("environment", {})
    return merged_dict, environment


DEFAULT_HEADER = "# generated by conda-devenv, do not modify and do not commit to VCS\n"


def render_for_conda_env(yaml_dict, header=DEFAULT_HEADER):
    import yaml
    contents = header
    contents += yaml.dump(yaml_dict, default_flow_style=False)
    return contents


def render_activate_script(environment, shell):
    """
    :param dict environment:
    :param string shell:
        Valid values are:
            - bash
            - fish
            - cmd
    :return: string
    """
    script = []
    if shell == "bash":
        script = ["#!/bin/bash"]
    elif shell == "cmd":
        script = ["@echo off"]

    for variable in sorted(environment):
        value = environment[variable]
        if shell == "bash":
            pathsep = ":"

            if isinstance(value, list):
                # Lists are supposed to prepend to the existing value
                value = pathsep.join(value) + pathsep + "${variable}".format(variable=variable)

            script.append("if [ ! -z ${{{variable}+x}} ]; then".format(variable=variable))
            script.append("    export CONDA_DEVENV_BKP_{variable}=\"${variable}\"".format(variable=variable))
            script.append("fi")
            script.append("export {variable}=\"{value}\"".format(variable=variable, value=value))

        elif shell == "cmd":
            pathsep = ";"
            if isinstance(value, list):
                # Lists are supposed to prepend to the existing value
                value = pathsep.join(value) + pathsep + "%{variable}%".format(variable=variable)

            script.append("set \"CONDA_DEVENV_BKP_{variable}=%{variable}%\"".format(variable=variable))
            script.append("set \"{variable}={value}\"".format(variable=variable, value=value))

        elif shell == "fish":
            quote = '"'
            if isinstance(value, list):
                # Lists are supposed to prepend to the existing value
                if variable == "PATH":
                    # HACK: Fish handles the PATH variable in a different way
                    # than other variables. So it needs a specific syntax to add
                    # values to PATH
                    pathsep = " "
                    quote = ""
                else:
                    pathsep = ":"
                value = pathsep.join(value) + pathsep + ("$%s" % variable)

            script.append("set -gx CONDA_DEVENV_BKP_{variable} ${variable}".format(variable=variable))
            script.append("set -gx {variable} {quote}{value}{quote}".format(
                    variable=variable, value=value, quote=quote
                ))

        else:
            raise ValueError("Unknown shell: %s" % shell)

    return "\n".join(script)


def render_deactivate_script(environment, shell='bash'):
    script = []
    if shell == "bash":
        script = ["#!/bin/bash"]
    elif shell == "cmd":
        script = ["@echo off"]

    for variable in sorted(environment):
        if shell == "bash":
            script.append("if [ ! -z ${{CONDA_DEVENV_BKP_{variable}+x}} ]; then".format(variable=variable))
            script.append("    export {variable}=\"$CONDA_DEVENV_BKP_{variable}\"".format(variable=variable))
            script.append("    unset CONDA_DEVENV_BKP_{variable}".format(variable=variable))
            script.append("else")
            script.append("    unset {variable}".format(variable=variable))
            script.append("fi")

        elif shell == "cmd":
            script.append("set \"{variable}=%CONDA_DEVENV_BKP_{variable}%\"".format(variable=variable))
            script.append("set CONDA_DEVENV_BKP_{variable}=".format(variable=variable))

        elif shell == "fish":
            script.append("set -gx {variable} $CONDA_DEVENV_BKP_{variable}".format(variable=variable))
            script.append("set -e CONDA_DEVENV_BKP_{variable}".format(variable=variable))

        else:
            raise ValueError("Unknown platform")

    return '\n'.join(script)


def __write_conda_environment_file(args, filename, rendered_contents):
    if args.output_file:
        output_filename = args.output_file
    else:
        output_filename, yaml_ext = os.path.splitext(filename)
        output_filename, devenv_ext = os.path.splitext(output_filename)
        if yaml_ext == "" or devenv_ext == "":
            # File has no extension or has a single extension, if we proceed we
            # will override the input file
            raise ValueError("Can't guess the output filename, please provide "
                             "the output filename with the --output-filename "
                             "flag")
        output_filename += yaml_ext

    with open(output_filename, 'w') as f:
        f.write(rendered_contents)

    return output_filename


def __call_conda_env_update(args, output_filename):
    import sys
    command = [
        "conda",
        "env",
        "update",
        "--file",
        output_filename,
    ]
    if not args.no_prune:
        command.append("--prune")
    if args.name:
        command.extend(["--name", args.name])
    if args.quiet:
        command.extend(["--quiet"])

    if not args.quiet:
        print("> Executing: %s" % ' '.join(command))

    old_argv = sys.argv[:]
    try:
        del command[0]
        sys.argv = command
        try:
            return _call_conda()
        except SystemExit as e:
            return e.code
    finally:
        sys.argv = old_argv


def _call_conda():
    """
    Calls conda-env directly using its internal API. ``sys.argv`` must already be configured at this point.

    We have this indirection here so we can mock this function during testing.
    """
    from conda_env.cli.main import main
    return main()


def write_activate_deactivate_scripts(args, conda_yaml_dict, environment):
    env_name = args.name or conda_yaml_dict["name"]

    import subprocess
    import json
    info = subprocess.check_output(["conda", "info", "--json"]).decode()
    info = json.loads(info)
    envs = info["envs"]

    env_directory = None
    for env in envs:
        if os.path.basename(env) == env_name:
            env_directory = env
            break
    else:
        raise ValueError("Couldn't find directory of environment '%s'" % env_name)

    from os.path import join
    activate_directory = join(env_directory, "etc", "conda", "activate.d")
    deactivate_directory = join(env_directory, "etc", "conda", "deactivate.d")

    if not os.path.exists(activate_directory):
        os.makedirs(activate_directory)
    if not os.path.exists(deactivate_directory):
        os.makedirs(deactivate_directory)

    if sys.platform.startswith("linux"):
        files = [("devenv-vars.sh", "bash"), ("devenv-vars.fish", "fish")]
    else:
        files = [("devenv-vars.bat", "cmd")]

    for filename, shell in files:
        activate_script = render_activate_script(environment, shell)
        deactivate_script = render_deactivate_script(environment, shell)

        with open(join(activate_directory, filename), "w") as f:
            f.write(activate_script)
        with open(join(deactivate_directory, filename), "w") as f:
            f.write(deactivate_script)


def main(args=None):
    if args is None:
        args = sys.argv[1:]
    parser = argparse.ArgumentParser(description="Work with multiple conda-environment-like yaml files in dev mode.")
    parser.add_argument("--file", "-f", nargs="?", help="The environment.devenv.yml file to process. "
                                                        "The default value is '%(default)s'.",
                        default="environment.devenv.yml")
    parser.add_argument("--name", "-n", nargs="?", help="Name of environment.")
    parser.add_argument("--print", help="Prints the rendered file as will be sent to conda-"
                                        "env to stdout and exits.", action="store_true")
    parser.add_argument("--print-full", help="Similar to --print, but also "
                                             "includes the 'environment' section.", action="store_true")
    parser.add_argument("--no-prune", help="Don't pass --prune flag to conda-env.", action="store_true")
    parser.add_argument("--output-file", nargs="?", help="Output filename.")
    parser.add_argument("--quiet", action="store_true", default=False)

    args = parser.parse_args(args)

    filename = args.file
    filename = os.path.abspath(filename)

    is_devenv_input_file = filename.endswith('.devenv.yml')
    if is_devenv_input_file:
        # render conda-devenv file
        conda_yaml_dict, environment = load_yaml_dict(filename)
        rendered_contents = render_for_conda_env(conda_yaml_dict)

        if args.print or args.print_full:
            print(rendered_contents)
            if args.print_full:
                print(render_for_conda_env({'environment': environment}, header=''))
            return 0

        # Write to the output file
        output_filename = __write_conda_environment_file(args, filename, rendered_contents)
    else:
        # Just call conda-env directly in plain environment.yml files
        output_filename = filename
        if args.print:
            with open(filename) as f:
                print(f.read())
            return 0

    # Call conda-env update
    retcode = __call_conda_env_update(args, output_filename)
    if retcode:
        return retcode

    if is_devenv_input_file:
        write_activate_deactivate_scripts(args, conda_yaml_dict, environment)
    return 0


if __name__ == "__main__":
    sys.exit(main())
