###############################################################################
# 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 asyncio
import logging
import time

from tango import DevFailed  # type: ignore
from tango.asyncio import DeviceProxy as AsyncProxy  # type: ignore

from takeoff.custom_types import Host, Order, Result
from takeoff.worker import Worker

logger = logging.getLogger(__name__)


class Downer:
    def __init__(self, worker: Worker):
        self.worker = worker
        self.result = Result()

    def down(self, hosts: list[Host], force: bool, kill_timeout: int, starter_timeout):
        self.force = force
        self.kill_timeout = kill_timeout
        self.starter_timeout = starter_timeout
        self.worker.work(hosts, Order.DESCENDING, self._stop)

    async def _stop(self, hostname: str, level: int, servername: str):
        starter = await AsyncProxy(f"tango/admin/{hostname}")

        t0 = time.monotonic()
        while True:
            # Stop the server. Either the hard way or the polite way
            try:
                if self.force:
                    logger.debug(f"Sending HardKillServer {servername} on {hostname}")
                    await starter.HardKillServer(servername)
                else:
                    logger.debug(f"Sending DevStop {servername} on {hostname}")
                    await starter.DevStop(servername)

            except DevFailed as e:
                if "Unknown Server" in e.args[0].reason:
                    # The device server exists but is unknown
                    logger.info(f"{servername} is unknown {hostname}. Never started?")
                    self.result.already_stopped.append(servername)
                    return
                if "not controlled" in e.args[0].reason:
                    # This is raised only when the server doesn't even appear under "Not Controlled"
                    logger.info(
                        f"{servername} is not controlled and not started on {hostname}."
                    )
                    self.result.already_stopped.append(servername)
                    return
                # If it's not running, peachy! We're leaving
                if e.args[0].reason == "SERVER_NOT_RUNNING":
                    logger.info(f"{servername} already stopped on {hostname}")
                    self.result.already_stopped.append(servername)
                    return
                # Otherwise try hard kill
                try:
                    logger.debug(f"Sending HardKillServer {servername} on {hostname}")
                    await starter.HardKillServer(servername)
                except DevFailed as e:
                    # Can be a API_DeviceTimedOut
                    # Let's give the next loop another chance
                    logger.debug(
                        f"HardKillServer {servername} on {hostname} failed? {e}"
                    )
                    break
            except AttributeError as e:
                # Starter device not exported - retry (Starter could be starting)
                logger.debug(
                    f"Sending DevStop {servername} on {hostname} failed. Starter not running?"
                )
                await asyncio.sleep(1)
                if time.monotonic() - t0 > self.starter_timeout:
                    logger.error(
                        f"Failed to stop {servername} on {hostname}. Starter not running? {e}"
                    )
                    self.result.failed_to_stop.append(servername)
                    return
            else:
                break

        t0 = time.monotonic()
        force_kill = False
        while True:
            try:
                # Return all servers which are not running
                # True is for all servers / False would be for controlled servers only
                stopped_servers = await starter.DevGetStopServers(True)
            except DevFailed as e:
                # Can be a API_DeviceTimedOut
                # Try again in next iteration - will force kill and exit eventually
                logger.warning(f"DevGetStopServers on {hostname} failed: {e}")
                pass
            else:
                if servername in stopped_servers:
                    logger.info(f"{servername} stopped on {hostname}")
                    self.result.stopped.append(servername)
                    break
                else:
                    logger.debug(
                        f"Stopped Servers on {hostname}: {' '.join(stopped_servers)}"
                    )
            if force_kill:
                logger.error(
                    f"Failed to stop {servername} on {hostname}. Server still not stopped after HardKillServer..."
                )
                self.result.failed_to_stop.append(servername)
                break
            # Force stop
            if time.monotonic() - t0 > self.kill_timeout:
                try:
                    logger.debug(
                        f"Server still not stopped after {self.kill_timeout} seconds. Sending HardKillServer {servername} on {hostname}"
                    )
                    await starter.HardKillServer(servername)
                except DevFailed as e:
                    # If it's not running, peachy! We're leaving
                    if e.args[0].reason == "SERVER_NOT_RUNNING":
                        # No exception on the first trial so assume it was running
                        logger.info(f"{servername} stopped on {hostname}")
                        self.result.stopped.append(servername)
                        return
                    # If the command failed, we give up
                    logger.error(
                        f"Failed to stop {servername} on {hostname}. HardKillServer failed: {e}"
                    )
                    self.result.failed_to_stop.append(servername)
                    return
                # Give Hard kill some time
                time.sleep(3)
                force_kill = True
            await asyncio.sleep(1)
