#!/usr/bin/env python3
import configparser
import logging
import os
import re
import sys
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 beer  # 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 `beer {subcommand}` command, make sure you have run "
                    f"`pip install beer[{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"][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:
        # {'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}")

        try:
            actor: dict = event["Actor"]
            attrs: dict = actor["Attributes"]
            if attrs.get("state.new") == "down":
                node: Node = client.nodes.get(node_id=actor["ID"])
                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"),
    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="tun0",
        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 beer.manager import service

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


@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),
    token: str = typer.Option(..., prompt=True),
    protocol: str = typer.Argument("http"),
):
    import docker
    from docker.errors import APIError
    from beer.models import WorkerModel
    from beer.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)
    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("bot")
def _init_bot(
    manager_ip: str = typer.Option(..., prompt=True),
    manager_rest_port: int = typer.Option(..., prompt=True),
    protocol: str = typer.Argument("http"),
):
    from beer.bot.telegram_bot import BeerBot

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

    BeerBot(bot_token=os.environ["TELEGRAM_API_KEY"], manager_url=manager_url).run()


if __name__ == "__main__":
    service_app()
