###############################################################################
# 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, Mode, Order, Result
from takeoff.finder import short_hostname
from takeoff.worker import Worker

logger = logging.getLogger(__name__)


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

    def up(self, hosts: list[Host], timeout: int, starter_timeout: int):
        self.timeout = timeout
        self.starter_timeout = starter_timeout
        self.worker.work(hosts, Order.ASCENDING, self._start)

    async def _start(self, hostname: str, level: int, servername: str):
        starter = await AsyncProxy(f"tango/admin/{hostname}")
        info = self.database.get_server_info(servername)

        if (
            short_hostname(info.host) != hostname
            or info.level != level
            or info.mode != Mode.CONTROLLED.to_int()
        ):
            info.mode = Mode.CONTROLLED.to_int()
            info.host = hostname
            info.level = level
            self.database.put_server_info(info)
            self.result.changed_info.append(servername)
        t0 = time.monotonic()
        while True:
            try:
                logger.debug(f"Sending DevStart {servername} on {hostname}")
                await starter.DevStart(servername)
            except DevFailed as e:
                if e.args[0].reason == "ALREADY_RUNNING":
                    logger.info(f"{servername} already running on {hostname}")
                    self.result.already_running.append(servername)
                    return
                logger.error(f"Failed to start {servername} on {hostname}: {e}")
                self.result.failed_to_start.append(servername)
                return
            except AttributeError as e:
                # Starter device not exported - retry (Starter could be starting)
                logger.debug(
                    f"Sending DevStart {servername} on {hostname} failed. Starter not running?"
                )
                await asyncio.sleep(1)
                if time.monotonic() - t0 > self.starter_timeout:
                    logger.error(
                        f"Failed to start {servername} on {hostname}. Starter not running? {e}"
                    )
                    self.result.failed_to_start.append(servername)
                    return
            else:
                break

        t0 = time.monotonic()
        while True:
            try:
                servernames = await starter.DevGetRunningServers(False)
            except DevFailed as e:
                # Can be a API_DeviceTimedOut
                # Try again in next iteration - will exit eventually
                logger.warning(f"DevGetRunningServers on {hostname} failed: {e}")
                pass
            else:
                if servername in servernames:
                    logger.info(f"{servername} started on {hostname}")
                    self.result.started.append(servername)
                    break
                else:
                    logger.debug(
                        f"Running Servers on {hostname}: {' '.join(servernames)}"
                    )
            if time.monotonic() - t0 > self.timeout:
                logger.error(
                    f"Failed to start {servername} on {hostname}. Server still not running after {self.timeout} seconds..."
                )
                self.result.failed_to_start.append(servername)
                break
            await asyncio.sleep(1)
