"""
Visualizes a completed pathogen build in Auspice, the Nextstrain visualization app.

The data directory should contain sets of Auspice JSON¹ files like

    <name>.json

or

    <name>_tree.json
    <name>_meta.json

The viewer runs inside a container, which requires Docker.  Run `nextstrain
check-setup` to check if Docker is installed and works.

¹ <https://nextstrain.github.io/auspice/introduction/how-to-run#input-file-formats>
"""

import re
import netifaces as net
from pathlib import Path
from typing import Iterable
from .. import runner
from ..argparse import add_extended_help_flags
from ..runner import docker, native
from ..util import colored, remove_suffix, warn
from ..volume import store_volume


def register_parser(subparser):
    """
    %(prog)s [options] <directory>
    %(prog)s --help
    """

    parser = subparser.add_parser("view", help = "View pathogen build", add_help = False)

    # Support --help and --help-all
    add_extended_help_flags(parser)

    parser.add_argument(
        "--allow-remote-access",
        help   = "Allow other computers on the network to access the website",
        action = "store_true")

    parser.add_argument(
        "--port",
        help    = "Listen on the given port instead of the default port %(default)s",
        metavar = "<number>",
        type    = int,
        default = 4000)

    # Positional parameters
    parser.add_argument(
        "directory",
        help    = "Path to directory containing JSONs for Auspice",
        metavar = "<directory>",
        action  = store_volume("auspice/data"))

    # Register runners; only Docker is supported for now.
    runner.register_runners(
        parser,
        exec    = ["auspice", "view", "--verbose", "--datasetDir=."],
        runners = [docker, native])

    return parser


def run(opts):
    # Ensure our data path is a directory that exists
    data_dir = opts.auspice_data.src

    if not data_dir.is_dir():
        warn("Error: Data path \"%s\" does not exist or is not a directory." % data_dir)

        if not data_dir.is_absolute():
            warn()
            warn("Perhaps your current working directory is different than you expect?")

        return 1

    # Find the available dataset paths
    datasets = dataset_paths(data_dir)

    # Setup the published port.  Default to localhost for security reasons
    # unless explicitly told otherwise.
    #
    # The environment variables HOST and PORT are respected by auspice's
    # cli/view.js.  HOST requires a new enough version of Auspice; 1.35.7 and
    # earlier always listen on 0.0.0.0 or ::.
    host = "0.0.0.0" if opts.allow_remote_access else "127.0.0.1"
    port = opts.port

    env = {
        'HOST': host,
        'PORT': str(port)
    }

    # These are docker-specific details which will only be used when the
    # docker runner (--docker flag) is in use.
    opts.docker_args = [
        *opts.docker_args,

        # auspice's cli (probably thanks to Express or Node?) when run in the
        # circumstances of the container seems to ignore signals (like SIGINT,
        # ^C, or SIGTERM), so run it under an init process that does respect
        # signals.
        "--init",

        # Inside the container, always bind to all interfaces.  This is
        # required for Docker to forward a port from the container's host into
        # the container because of how it does port publishing.  Note that
        # container ports aren't automatically published outside the container,
        # so this still doesn't allow arbitrary access from the outside world.
        # The published port on the container's host is still bound to
        # 127.0.0.1 by default.
        "--env=HOST=0.0.0.0",

        # Publish the port
        "--publish=%s:%d:%d" % (host, port, port),
    ]

    # Find the best remote address if we're allowing remote access.  While we
    # listen on all interfaces (0.0.0.0), only the local host can connect to
    # that successfully.  Remote hosts need a real IP on the network, which we
    # do our best to discover.  If something goes wrong, ignore it and leave
    # the host IP as-is (0.0.0.0); it'll at least work for local access.
    if opts.allow_remote_access:
        try:
            remote_address = best_remote_address()
        except:
            pass
        else:
            host = remote_address

    # Show a helpful message about where to connect
    print_url(host, port, datasets)

    return runner.run(opts, working_volume = opts.auspice_data, extra_env = env)


def dataset_paths(data_dir: Path) -> Iterable[str]:
    """
    Returns a :py:class:`set` of Auspice (not filesystem) paths for datasets in
    *data_dir*.
    """
    # v2: All *.json files which don't end with a known sidecar or v1 suffix.
    sidecar_suffixes = {"meta", "tree", "root-sequence", "seq", "sequences", "tip-frequencies", "entropy"}

    def sidecar_file(path):
        return any(path.name.endswith("_%s.json" % suffix) for suffix in sidecar_suffixes)

    datasets_v2 = set(
        path.stem.replace("_", "/")
            for path in data_dir.glob("*.json")
            if not sidecar_file(path))

    # v1: All *_tree.json files with corresponding *_meta.json files.
    def meta_exists(path):
        return path.with_name(remove_suffix("_tree.json", path.name) + "_meta.json").exists()

    datasets_v1 = set(
        re.sub(r"_tree$", "", path.stem).replace("_", "/")
            for path in data_dir.glob("*_tree.json")
            if meta_exists(path))

    return datasets_v2 | datasets_v1


def print_url(host, port, datasets):
    """
    Prints a list of available dataset URLs, if any.  Otherwise, prints a
    generic URL.
    """

    def url(path = None):
        return colored(
            "blue",
            "http://{host}:{port}/{path}".format(
                host = host,
                port = port,
                path = path if path is not None else ""))

    horizontal_rule = colored("green", "—" * 78)

    print()
    print(horizontal_rule)

    if len(datasets):
        print("    The following datasets should be available in a moment:")
        for path in sorted(datasets, key = str.casefold):
            print("       • %s" % url(path))
    else:
        print("    Open <%s> in your browser." % url())
        print()
        print("   ", colored("yellow", "Warning: No datasets detected."))

    print(horizontal_rule)
    print()


def best_remote_address():
    """
    Returns the "best" non-localback IP address for the local host, if
    possible.  The "best" IP address is that bound to either the default
    gateway interface, if any, else the arbitrary first interface found.

    IPv4 is preferred, but IPv6 will be used if no IPv4 interfaces/addresses
    are available.
    """
    default_gateway   = net.gateways().get("default", {})
    default_interface = default_gateway.get(net.AF_INET,  (None, None))[1] \
                     or default_gateway.get(net.AF_INET6, (None, None))[1] \
                     or net.interfaces()[0]

    interface_addresses = net.ifaddresses(default_interface).get(net.AF_INET)  \
                       or net.ifaddresses(default_interface).get(net.AF_INET6) \
                       or []

    addresses = [
        address["addr"]
            for address in interface_addresses
             if address.get("addr")
    ]

    return addresses[0] if addresses else None