###############################################################################
# This file is part of the tango-takeoff project.
#
# Copyright Lund University
#
# Distributed under the GNU GPLv3 license. See LICENSE file for more info.
###############################################################################
import argparse
import logging
from typing import Optional, Union

import tango  # type: ignore

from takeoff.downer import Downer
from takeoff.error import TakeOffCommandFailed, TakeOffError
from takeoff.finder import Finder
from takeoff.printer import PlainPrinter, StructuredPrinter
from takeoff.takeoff import TakeOff
from takeoff.upper import Upper
from takeoff.worker import Worker

DEFAULT_FORCE = False
DEFAULT_FORCE_TIMEOUT = 30
DEFAULT_RUNNING_TIMEOUT = 60
DEFAULT_STARTER_TIMEOUT = 10
DEFAULT_WAIT_AFTER_STOP = 1


class Database(tango.Database):
    """Custom Database class to override get_server_info

    Since cpptango 10.1, get_server_info raises an exception if the server was never started by Starter
    See https://gitlab.com/tango-controls/cppTango/-/work_items/1553
    Planned to be fixed in 10.4
    """

    def get_server_info(self, server_name: str) -> tango.DbServerInfo:
        """Get server info for a server name

        Overrides the default get_server_info to keep the same behavior as before 10.1
        and avoid raising an exception if the server was never started by Starter
        """
        try:
            server_info = super().get_server_info(server_name)
        except tango.DevFailed as e:
            if e.args[0].reason == "API_InvalidArgs":
                # We only want to catch:
                # tango._tango.DevFailed: DevFailed[
                #    DevError[
                #        desc = " " cannot be entirely parsed into int
                #        origin = T Tango::detail::parse_as(std::string_view) [T = int] at ...
                #        reason = API_InvalidArgs
                #        severity = ERR
                # ]
                server_info = tango.DbServerInfo()
                server_info.name = server_name
            else:
                raise e
        return server_info


def run(arg_list: Optional[list[str]] = None):
    args = parse_args(arg_list)
    logging.basicConfig(
        format="%(asctime)s [%(levelname)s] %(message)s",
        handlers=[logging.StreamHandler()],
        level=get_logging_level(args.verbose),
    )
    if args.output_json:
        # Disable logging to output only valid json
        logging.disable(logging.CRITICAL)
        printer: Union[PlainPrinter, StructuredPrinter] = StructuredPrinter()
    else:
        printer = PlainPrinter(args.verbose)

    try:
        db = Database()
        takeoff = TakeOff(
            Upper(Worker(), db),
            Downer(Worker()),
            Finder(args.default_host, db),
            printer,
        )

        if args.sub_parser == "find":
            takeoff.find(args.server_name)
        elif args.sub_parser in ("up", "start"):
            takeoff.up(
                args.server_name,
                args.dry_run,
                args.running_timeout,
                args.starter_timeout,
            )
        elif args.sub_parser in ("down", "stop"):
            takeoff.down(
                args.server_name,
                args.dry_run,
                args.force,
                args.kill_timeout,
                args.starter_timeout,
            )
        elif args.sub_parser == "restart":
            takeoff.restart(
                args.server_name,
                args.dry_run,
                args.force,
                args.kill_timeout,
                args.running_timeout,
                args.starter_timeout,
                args.wait_after_stop,
            )

    except TakeOffCommandFailed:
        # Command completed but some servers couldn't be started or stopped
        exit(3)
    except TakeOffError as e:
        printer.error(e)
        exit(1)

    except tango.DevFailed as e:
        printer.devfailed(e)
        exit(1)

    exit(0)


def get_logging_level(verbose: Optional[int]) -> int:
    """Return a logging level based on verbose count"""
    if verbose is None:
        return logging.WARNING
    elif verbose == 1:
        return logging.INFO
    else:
        return logging.DEBUG


def parse_args(arg_list: Optional[list[str]]):
    desc = "Start (up), stop (down) or restart (restart) Tango Device Servers"
    parser = argparse.ArgumentParser(
        prog="takeoff",
        description=desc,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )

    sub_parsers = parser.add_subparsers(dest="sub_parser")

    find_parser = sub_parsers.add_parser(
        "find",
        help="Finds servers based on terms and shows the 'dry run' version of what will be used in 'up' and 'down'",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    up_parser = sub_parsers.add_parser(
        "up",
        aliases=["start"],
        help="Bring servers up based on terms",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    down_parser = sub_parsers.add_parser(
        "down",
        aliases=["stop"],
        help="Bring servers down based on terms",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    restart_parser = sub_parsers.add_parser(
        "restart",
        help="Restart servers based on terms",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )

    _define_common_flags(find_parser, "Use the format [host,]<server-name>")
    _define_common_flags(up_parser, "Use the format [host,]<server-name>[,<level>]")
    _define_common_flags(down_parser, "Use the format [host,]<server-name>")
    _define_common_flags(restart_parser, "Use the format [host,]<server-name>")

    up_parser.add_argument(
        "-t",
        "--timeout",
        dest="running_timeout",
        default=DEFAULT_RUNNING_TIMEOUT,
        type=int,
        help="Time to wait for a server to be running (in seconds)",
    )

    up_parser.add_argument(
        "-s",
        "--starter-timeout",
        dest="starter_timeout",
        default=DEFAULT_STARTER_TIMEOUT,
        type=int,
        help="Time to wait for starter if it's not running (in seconds)",
    )

    up_parser.add_argument(
        "-d",
        "--dry-run",
        default=False,
        dest="dry_run",
        action="store_true",
        help="outputs the servers with the details that will take effect by the command",
    )

    down_parser.add_argument(
        "-f",
        "--force",
        default=DEFAULT_FORCE,
        dest="force",
        action="store_true",
        help="Force kill servers",
    )

    down_parser.add_argument(
        "-k",
        "--kill-timeout",
        dest="kill_timeout",
        default=DEFAULT_FORCE_TIMEOUT,
        type=int,
        help="Time before force killing (in seconds)",
    )

    down_parser.add_argument(
        "-s",
        "--starter-timeout",
        dest="starter_timeout",
        default=DEFAULT_STARTER_TIMEOUT,
        type=int,
        help="Time to wait for starter if it's not running (in seconds)",
    )

    down_parser.add_argument(
        "-d",
        "--dry-run",
        default=False,
        dest="dry_run",
        action="store_true",
        help="outputs the servers with the details that will take effect by the command",
    )

    restart_parser.add_argument(
        "-f",
        "--force",
        default=DEFAULT_FORCE,
        dest="force",
        action="store_true",
        help="Force kill servers",
    )

    restart_parser.add_argument(
        "-k",
        "--kill-timeout",
        dest="kill_timeout",
        default=DEFAULT_FORCE_TIMEOUT,
        type=int,
        help="Time before force killing (in seconds)",
    )

    restart_parser.add_argument(
        "-t",
        "--timeout",
        dest="running_timeout",
        default=DEFAULT_RUNNING_TIMEOUT,
        type=int,
        help="Time to wait for a server to be running (in seconds)",
    )
    restart_parser.add_argument(
        "-s",
        "--starter-timeout",
        dest="starter_timeout",
        default=DEFAULT_STARTER_TIMEOUT,
        type=int,
        help="Time to wait for starter if it's not running (in seconds)",
    )

    restart_parser.add_argument(
        "-d",
        "--dry-run",
        default=False,
        dest="dry_run",
        action="store_true",
        help="outputs the servers with the details that will take effect by the command",
    )

    restart_parser.add_argument(
        "-w",
        "--wait-after-stop",
        dest="wait_after_stop",
        default=DEFAULT_WAIT_AFTER_STOP,
        type=int,
        help="Time to wait between stop / start (in seconds)",
    )

    args = parser.parse_args(arg_list)
    if args.sub_parser is None:
        parser.print_help()
        exit(0)
    return args


def _define_common_flags(parser, search_format):
    parser.add_argument(
        "--json",
        default=False,
        action="store_true",
        dest="output_json",
        help="Output structured as JSON",
    )

    parser.add_argument(
        "--default-host",
        default=None,
        dest="default_host",
        help="Default host for Tango device servers",
    )

    parser.add_argument(
        "server_name",
        metavar="servers",
        type=str,
        nargs="+",
        help="A name or search term of a server, use * for wildcard in the end of the name. "
        + search_format,
    )

    parser.add_argument(
        "-v",
        "--verbose",
        dest="verbose",
        action="count",
        help="Set the trace level. Can be used in count way: -vv or --verbose --verbose",
    )
