#!/usr/bin/env python3
import configparser
import logging
import re
import sys
from collections import Counter
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version
from multiprocessing import Process
from pathlib import Path
from typing import Mapping, Optional, Sequence

import requests
import typer
from requests import ConnectTimeout, Response

import beers  # noqa

pylogger = logging.getLogger(__name__)


service_app = typer.Typer(name="service")

_dependency_pattern = re.compile(r"\s*([\w-]+).*")


@service_app.callback(invoke_without_command=True, no_args_is_help=True)
def main(ctx: typer.Context):
    if (subcommand := ctx.invoked_subcommand) is not None:
        for dependency in parse_requires(subcommand):
            dependency: str
            dependency: re.Match = _dependency_pattern.match(dependency)
            dependency: str = dependency.group(1)
            try:
                version(dependency)
            except PackageNotFoundError as e:
                error_message = (
                    f"PackageNotFoundError: {dependency} not installed. "
                    f"If you are running a `beers {subcommand}` command, make sure you have run "
                    f"`pip install beers[{subcommand}]`"
                )
                pylogger.error(error_message)
                raise PackageNotFoundError(error_message) from e


def parse_requires(subcommand: str) -> Sequence[str]:
    config = Path(sys.prefix) / "setup.cfg"
    if not config.exists():
        return []

    parser = configparser.ConfigParser()
    parser.read(config)

    # TODO: handle multiple ways to declare extras_require (e.g. inline and mixed)
    return parser["options.extras_require"].get(subcommand, "").strip().splitlines()


def _bash_run(script_path: Path, env: Mapping[str, str]):
    import subprocess

    # Read the file in case there are no execution permissions. Better safe than sorry.
    script = script_path.read_text(encoding="utf-8")

    return subprocess.call(script, shell=True, env=env)


def _setup_traefik(
    hostname: Optional[str], force_reset: bool, traefik_domain: str, traefik_email: str, traefik_username: str
):
    assert all(x is not None and len(x) > 0 for x in (traefik_domain, traefik_email, traefik_username))
    # Check hostname is set
    if hostname is None:
        # We need to read it.
        hostname_path: Path = Path("/etc/hostname")
        if not hostname_path.exists():
            # To set it, we need sudo power.
            # Since with great power comes great responsibility, we ask the users to set it manually.
            pylogger.error(
                """Missing hostname in /etc/hostname. Please run
```sudo echo your_hostname >/etc/hostname
sudo hostname -F /etc/hostname```"""
            )

        hostname = Path("/etc/hostname").read_text(encoding="utf-8").strip()

        if len(hostname) == 0:
            # To set it, we need sudo power.
            # Since with great power comes great responsibility, we ask the users to set it manually.
            pylogger.error(
                """Hostname in /etc/hostname is empty. Please run
```sudo echo your_hostname >/etc/hostname
sudo hostname -F /etc/hostname```"""
            )

    if force_reset:
        import subprocess

        subprocess.call("docker stack rm traefik", shell=True)
        print("traefick public", subprocess.call("docker network rm traefik-public", shell=True))

    traefik_path: Path = Path("./swarmrocks/traefik.sh")
    assert traefik_path.exists()

    traefik_env = dict(HOSTNAME=hostname, USERNAME=traefik_username, EMAIL=traefik_email, DOMAIN=traefik_domain)
    traefik_out = _bash_run(script_path=traefik_path, env=traefik_env)
    assert traefik_out == 0, traefik_out


def _setup_swarmpit(swarmpit_domain: str, traefik: bool, force_reset: bool):
    if not traefik:
        pylogger.warning(
            "Swarmpit needs Traefik to work properly. Make sure it is set up or pass USE_TRAEFIK=true to this script"
        )
    assert all(x is not None and len(x) > 0 for x in (swarmpit_domain,))

    if force_reset:
        import subprocess

        subprocess.call("docker stack rm swarmpit", shell=True)

    swarmpit_path: Path = Path("./swarmrocks/swarmpit.sh")
    assert swarmpit_path.exists()

    swarmpit_env = dict(DOMAIN=swarmpit_domain)
    swarmpit_out = _bash_run(script_path=swarmpit_path, env=swarmpit_env)
    assert swarmpit_out == 0, swarmpit_out


def _setup_swarmprom(swarmprom_base_domain: str, swarmprom_user: str, traefik: bool, force_reset: bool):
    if not traefik:
        pylogger.warning(
            "Swarmprom needs Traefik to work properly. Make sure it is set up or pass USE_TRAEFIK=true to this script"
        )
    assert all(x is not None and len(x) > 0 for x in (swarmprom_base_domain,))

    if force_reset:
        import subprocess

        subprocess.call("docker stack rm swarmpit", shell=True)

    swarmprom_path: Path = Path("./swarmrocks/swarmprom.sh")
    assert swarmprom_path.exists()

    swarmprom_env = dict(DOMAIN=swarmprom_base_domain, SWARMPROM_USER=swarmprom_user)
    swarmprom_out = _bash_run(script_path=swarmprom_path, env=swarmprom_env)

    assert swarmprom_out == 0, swarmprom_out


def run_event_listener():
    import docker
    from docker.models.nodes import Node

    logger = logging.getLogger("SwarmEventListener")

    client = docker.from_env()
    events = client.events(
        decode=True,
        filters={"scope": "swarm", "type": "node"},
    )

    for event in events:
        nodes: Sequence[Node] = list(client.nodes.list())
        logger.warning([node.attrs for node in nodes])
        hostname2count: Mapping[str, int] = Counter([node.attrs["Description"]["Hostname"] for node in nodes])
        logger.warning(f"{hostname2count=}")

        # {'Type': 'node', 'Action': 'update',
        #  'Actor': {'ID': 'pou5nqcitq0y0x1wmhrrwdn8q', 'Attributes': {'name': __main__: 124
        # 'cm-shannon', 'state.new': 'down', 'state.old': 'ready'}}, 'scope': 'swarm', 'time': 1650372820, 'timeNano':
        # 1650372820734862390}
        logger.debug(f"[Swarm Event]: {event}")

        # TODO: remove nodes with same hostname when state.new == down or state.now == ready.
        #  This can only happen when a `docker swarm leave` is issued on the worker side

        # try:
        #     actor: dict = event["Actor"]
        #     attrs: dict = actor["Attributes"]
        #     if attrs.get("state.new") == "down":
        #         node: Node = client.nodes.get(node_id=actor["ID"])
        #         if hostname2count.get(node.attrs["Description"]["Hostname"], 0) > 1:
        #             node.remove(force=True)
        # except Exception as e:
        #     logger.error(f"Error with events: {e}")
    events.close()  # TODO: where/when?


@service_app.command("manager")
def _init_manager(
    ip: str = typer.Option(..., prompt=True, envvar="MANAGER_IP"),
    swarm_port: int = typer.Option(..., prompt=True, envvar="MANAGER_SWARM_PORT"),
    rest_port: int = typer.Option(4242, prompt=True, envvar="MANAGER_REST_PORT"),
    rest_host: str = typer.Option("0.0.0.0", prompt=True, envvar="MANAGER_REST_HOST"),
    owner_id: str = typer.Option(..., prompt=True, envvar="OWNER_ID"),
    db_path: str = typer.Option(..., prompt=True, envvar="BEERS_DB_PATH"),
    advertise_addr: str = typer.Option(default="tun0", prompt=True),
    hostname: str = typer.Option(None, prompt=False, envvar="HOSTNAME"),
    #
    traefik: bool = typer.Option(False, prompt=True, envvar="USE_TRAEFIK"),
    traefik_username: str = typer.Option(None, prompt=False, envvar="TRAEFIK_USERNAME"),
    traefik_email: str = typer.Option(None, prompt=False, envvar="TRAEFIK_EMAIL"),
    traefik_domain: str = typer.Option(None, prompt=False, envvar="TRAEFIK_DOMAIN"),
    #
    swarmpit: bool = typer.Option(False, prompt=True, envvar="USE_SWARMPIT"),
    swarmpit_domain: str = typer.Option(None, prompt=False, envvar="SWARMPIT_DOMAIN"),
    #
    swarmprom: bool = typer.Option(False, prompt=True, envvar="SWARMPROM"),
    swarmprom_base_domain: str = typer.Option(None, prompt=False, envvar="SWARMPROM_BASE_DOMAIN"),
    swarmprom_user: str = typer.Option(None, prompt=False, envvar="SWARMPROM_USER"),
    #
    swarm_reset: bool = typer.Option(True, prompt=True, envvar="SWARM_RESET"),
):
    import docker

    client = docker.from_env()

    if swarm_reset:
        client.swarm.leave(force=True)

    _: str = client.swarm.init(
        advertise_addr=advertise_addr,
        listen_addr=f"{ip}:{swarm_port}",
        force_new_cluster=swarm_reset,
        # default_addr_pool=["10.43.0.0/16"],
        # subnet_size=24,
        # snapshot_interval=5000,
        # log_entries_for_slow_followers=1200,
    )
    Process(target=run_event_listener).start()

    worker_token: str = client.swarm.attrs["JoinTokens"]["Worker"]
    pylogger.info(f"WORKER_TOKEN: <{worker_token}>")

    # if traefik:
    #     _setup_traefik(
    #         hostname=hostname,
    #         force_reset=swarm_reset,
    #         traefik_domain=traefik_domain,
    #         traefik_email=traefik_email,
    #         traefik_username=traefik_username,
    #     )
    #
    # if swarmpit:
    #     _setup_swarmpit(swarmpit_domain=swarmpit_domain, traefik=traefik, force_reset=swarm_reset)
    #
    # if swarmprom:
    #     _setup_swarmprom(
    #         swarmprom_base_domain=swarmprom_base_domain,
    #         swarmprom_user=swarmprom_user,
    #         traefik=traefik,
    #         force_reset=swarm_reset,
    #     )

    from beers.manager import service

    db_path: Path = Path(db_path)
    service.run(service_host=rest_host, service_port=rest_port, owner_id=owner_id, db_path=db_path)


@service_app.command("worker")
def _init_worker(
    manager_ip: str = typer.Option(..., prompt=True),
    manager_swarm_port: int = typer.Option(..., prompt=True),
    manager_rest_port: int = typer.Option(..., prompt=True),
    local_nfs_root: str = typer.Option(default=None, prompt=False),
    advertise_addr: str = typer.Option(default="tun0", prompt=True),
    token: str = typer.Option(..., prompt=True),
    protocol: str = typer.Argument("http"),
):
    import docker
    from docker.errors import APIError
    from beers.models import WorkerModel
    from beers.worker_utils import build_worker_specs

    manager_url: str = f"{manager_ip}:{manager_swarm_port}"
    client = docker.from_env()

    try:
        client.swarm.leave()
    except APIError:
        # Most likely it was a Service Unavailable ("This node is not part of a swarm")
        # TODO: we could check via client.info()
        pass

    join_result: bool = client.swarm.join(
        remote_addrs=[manager_url],
        join_token=token,
        # data_path_addr="tun0",
        # listen_addr="",
        advertise_addr=advertise_addr,
    )
    if join_result:
        worker_model: WorkerModel = build_worker_specs(local_nfs_root=local_nfs_root)
        # TODO: let users confirm gathered specs?

        manager_url: str = f"{protocol}://{manager_ip}:{manager_rest_port}"

        try:
            response: Response = requests.post(url=f"{manager_url}/join", json=worker_model.dict(), timeout=5)
            print(response.json())
        except ConnectTimeout:
            pylogger.error(f"Could not connect to {manager_url} (timed out)")
            return
        except Exception as exc:
            # TODO
            pylogger.error(f"Could not connect to {manager_url}: {exc.args})")
            return


@service_app.command("worker_setup")
def _setup_worker(
    local_nfs_root: str = typer.Option(default="", prompt=True),
):
    import importlib.resources as pkg_resources
    import getpass
    import subprocess
    from beers import scripts

    script_path = pkg_resources.path(scripts, "worker_setup.sh")

    username = getpass.getuser()
    if username != "root":
        raise RuntimeError(f"This command must be executed with root privileges! Current username: {username}")

    subprocess.call(f"{script_path} {local_nfs_root}", shell=True)


@service_app.command("bot")
def _init_bot(
    manager_ip: str = typer.Option(..., prompt=True),
    manager_rest_port: int = typer.Option(..., prompt=True),
    telegram_api_key: str = typer.Option(..., prompt=True, envvar="TELEGRAM_API_KEY"),
    protocol: str = typer.Argument("http"),
):
    from beers.bot.telegram_bot import BeersBot

    manager_url: str = f"{protocol}://{manager_ip}:{manager_rest_port}"

    BeersBot(bot_token=telegram_api_key, manager_url=manager_url).run()


if __name__ == "__main__":
    service_app()
