# Copyright 2021 Red Hat
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.
from __future__ import absolute_import

import typing

import ironicclient.v1.client
from oslo_log import log

import tobiko
from tobiko.openstack.ironic import _client


LOG = log.getLogger(__name__)

IronicNode = typing.Union[ironicclient.v1.node.Node]
IronicNodeType = typing.Union[str, IronicNode]


def get_node_id(node: typing.Optional[IronicNodeType] = None,
                node_id: typing.Optional[str] = None) -> str:
    if node_id is None:
        if isinstance(node, str):
            node_id = node
        else:
            assert node is not None
            node_id = node.uuid
    return node_id


def get_node(node: typing.Optional[IronicNodeType] = None,
             node_id: typing.Optional[str] = None,
             client: _client.IronicClientType = None,
             **params) -> IronicNode:
    node_id = get_node_id(node=node, node_id=node_id)
    return _client.ironic_client(client).node.get(node_id, **params)


class WaitForNodePowerStateError(tobiko.TobikoException):
    message = ("Node {node_id} not changing power state from "
               "{node_power_state} to {power_state}")


class WaitForNodePowerStateTimeout(WaitForNodePowerStateError):
    message = ("Node {node_id} didn't change its state from "
               "{node_power_state} to {power_state} state after "
               "{timeout} seconds")


IRONIC_NODE_TRANSIENT_POWER_STATES: typing.Dict[str, typing.List[str]] = {
    'power on': ['power off'],
    'power off': ['power on'],
}


def wait_for_node_power_state(
        node: IronicNodeType,
        power_state: str,
        client: _client.IronicClientType = None,
        timeout: tobiko.Seconds = None,
        sleep_time: tobiko.Seconds = None,
        transient_status: typing.Optional[typing.List[str]] = None) -> \
            IronicNode:
    if transient_status is None:
        transient_status = IRONIC_NODE_TRANSIENT_POWER_STATES.get(
            power_state) or []
    node_id = get_node_id(node)
    for attempt in tobiko.retry(timeout=timeout,
                                interval=sleep_time,
                                default_timeout=300.,
                                default_interval=5.):
        _node = get_node(node_id=node_id, client=client)
        if _node.power_state == power_state:
            break
        if _node.power_state not in transient_status:
            raise WaitForNodePowerStateError(
                node_id=node_id,
                node_power_state=_node.power_state,
                power_state=power_state)
        if attempt.is_last:
            raise WaitForNodePowerStateTimeout(
                node_id=node_id,
                node_power_state=_node.power_state,
                power_state=power_state,
                timeout=timeout)

        LOG.debug(f"Waiting for Ironic node '{node_id}' power state to get "
                  f"from {_node.power_state} to {power_state}...")
    else:
        raise RuntimeError("Retry look break before timing out")

    return _node


def power_off_node(node: IronicNodeType,
                   soft=False,
                   client: _client.IronicClientType = None,
                   timeout: tobiko.Seconds = None,
                   sleep_time: tobiko.Seconds = None) \
        -> IronicNode:
    client = _client.ironic_client(client)
    node = get_node(node=node, client=client)
    if node.power_state == 'power off':
        return node

    LOG.info(f"Power off baremetal node '{node.uuid}' "
             f"(power state = '{node.power_state}').")
    client.node.set_power_state(node.uuid,
                                state='off',
                                soft=soft)
    return wait_for_node_power_state(node=node.uuid,
                                     power_state='power off',
                                     client=client,
                                     timeout=timeout,
                                     sleep_time=sleep_time)


def power_on_node(node: IronicNodeType,
                  client: _client.IronicClientType = None,
                  timeout: tobiko.Seconds = None,
                  sleep_time: tobiko.Seconds = None) -> \
        IronicNode:
    client = _client.ironic_client(client)
    node = get_node(node=node, client=client)
    if node.power_state == 'power on':
        return node

    LOG.info(f"Power on baremetal node '{node.uuid}' "
             f"(power_state='{node.power_state}').")
    client.node.set_power_state(node_id=node.uuid,
                                state='on')
    return wait_for_node_power_state(node=node.uuid,
                                     power_state='power on',
                                     client=client,
                                     timeout=timeout,
                                     sleep_time=sleep_time)
