# Copyright 2019 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 metalsmith
import netaddr
from oslo_log import log

import tobiko
from tobiko import config
from tobiko.openstack import neutron
from tobiko.openstack import nova
from tobiko.openstack import topology
from tobiko.podified import _openshift
from tobiko import rhosp
from tobiko.shell import files
from tobiko.shell import iperf3
from tobiko.shell import http_ping
from tobiko.shell import sh
from tobiko.shell import ssh
from tobiko.tripleo import _overcloud
from tobiko.tripleo import _undercloud
from tobiko.tripleo import containers
from tobiko.tripleo import nova as tripleo_nova

CONF = config.CONF
LOG = log.getLogger(__name__)


class TripleoTopology(rhosp.RhospTopology):

    agent_to_service_name_mappings = {
        neutron.DHCP_AGENT: 'tripleo_neutron_dhcp',
        neutron.L3_AGENT:  'tripleo_neutron_l3_agent',
        neutron.OPENVSWITCH_AGENT: 'tripleo_neutron_ovs_agent',
        neutron.METADATA_AGENT: 'tripleo_neutron_metadata_agent',
        neutron.OVN_METADATA_AGENT: 'tripleo_ovn_metadata_agent',
        neutron.NEUTRON_OVN_METADATA_AGENT: 'tripleo_ovn_metadata_agent',
        neutron.OVN_CONTROLLER: 'tripleo_ovn_controller',
        neutron.OVN_BGP_AGENT: 'tripleo_ovn_bgp_agent',
        neutron.FRR: 'tripleo_frr',
        neutron.NEUTRON: 'tripleo_neutron_api'
    }

    agent_to_container_name_mappings = {
        neutron.DHCP_AGENT: 'neutron_dhcp',
        neutron.L3_AGENT:  'neutron_l3_agent',
        neutron.OPENVSWITCH_AGENT: 'neutron_ovs_agent',
        neutron.METADATA_AGENT: 'neutron_metadata_agent',
        neutron.OVN_METADATA_AGENT: 'ovn_metadata_agent',
        neutron.NEUTRON_OVN_METADATA_AGENT: 'ovn_metadata_agent',
        neutron.OVN_CONTROLLER: 'ovn_controller',
        neutron.OVN_BGP_AGENT: 'ovn_bgp_agent',
        neutron.FRR: 'frr'
    }

    config_file_mappings = {
        'ml2_conf.ini': '/var/lib/config-data/puppet-generated/neutron'
                        '/etc/neutron/plugins/ml2/ml2_conf.ini',
        'bgp-agent.conf': '/var/lib/config-data/ansible-generated/'
                          'ovn-bgp-agent/etc/ovn-bgp-agent/bgp-agent.conf'
    }

    # TODO: add more known subgrups here
    known_subgroups: typing.List[str] = ['controller', 'compute']

    # In TripleO we need to parse log files directly
    file_digger_class = files.LogFileDigger

    # This is dict which handles mapping of the log file and systemd_unit (if
    # needed) for the OpenStack services
    # Format of this dict is like below:
    # service_name: (log_filename, systemd_unit_name)
    log_names_mappings = {
        neutron.SERVER: '/var/log/containers/neutron/server.log*',
    }

    _container_runtime_cmd = None

    pcs_resource_list = ['haproxy', 'galera', 'redis', 'ovn-dbs', 'cinder',
                         'rabbitmq', 'manila', 'ceph', 'pacemaker']

    sidecar_container_list = [
        'neutron-haproxy-ovnmeta', 'neutron-haproxy-qrouter',
        'neutron-dnsmasq-qdhcp', 'neutron-keepalived-qrouter', 'neutron-radvd']

    @property
    def container_runtime_cmd(self):
        if self._container_runtime_cmd is None:
            self._container_runtime_cmd = \
                    containers.get_container_runtime_name()
        return self._container_runtime_cmd

    @property
    def ignore_containers_list(self):
        return self.pcs_resource_list + self.sidecar_container_list

    def create_node(self, name, ssh_client, **kwargs):
        return TripleoTopologyNode(topology=self,
                                   name=name,
                                   ssh_client=ssh_client,
                                   **kwargs)

    def assert_containers_running(self, expected_containers,
                                  group=None,
                                  full_name=True, bool_check=False,
                                  nodenames=None):
        group = group or 'overcloud'
        return containers.assert_containers_running(
            group=group,
            expected_containers=expected_containers,
            full_name=full_name,
            bool_check=bool_check,
            nodenames=nodenames)

    def list_containers_td(self, group=None):
        return containers.list_containers_td(group)

    def discover_nodes(self):
        self.discover_ssh_proxy_jump_node()
        self.discover_undercloud_nodes()
        self.discover_overcloud_nodes()

    def discover_undercloud_nodes(self):
        if _undercloud.has_undercloud():
            uc_config = _undercloud.undercloud_host_config()
            ssh_client = _undercloud.undercloud_ssh_client()
            self.add_node(address=uc_config.hostname,
                          group='undercloud',
                          ssh_client=ssh_client)

    def discover_overcloud_nodes(self):
        if _undercloud.has_undercloud():
            for instance in _overcloud.list_overcloud_nodes():
                try:
                    _overcloud.power_on_overcloud_node(instance)
                except Exception:
                    LOG.exception("Error ensuring overcloud node power "
                                  "status is on")
                host_config = _overcloud.overcloud_host_config(
                    instance=instance)
                ssh_client = _overcloud.overcloud_ssh_client(
                    instance=instance,
                    host_config=host_config)
                node = self.add_node(address=host_config.hostname,
                                     group='overcloud',
                                     ssh_client=ssh_client,
                                     overcloud_instance=instance)
                assert isinstance(node, TripleoTopologyNode)
                self.discover_overcloud_node_subgroups(node)

    def discover_overcloud_node_subgroups(self, node):
        # set of subgroups extracted from node name
        subgroups: typing.Set[str] = set()

        oc_groups_dict = CONF.tobiko.tripleo.overcloud_groups_dict
        # extract subgroups names from node name
        subgroups.update(oc_groups_dict.get(subgroup) or subgroup
                         for subgroup in node.name.split('-')
                         if is_valid_overcloud_group_name(group_name=subgroup,
                                                          node_name=node.name))

        # add all those known subgroups names that are contained in
        # the node name (controller, compute, ...)
        subgroups.update(subgroup
                         for subgroup in self.known_subgroups
                         if subgroup in node.name)

        # bind node to discovered subgroups
        if subgroups:
            for subgroup in sorted(subgroups):
                LOG.debug("Add node '%s' to subgroup '%s'", node.name,
                          subgroup)
                self.add_node(hostname=node.name, group=subgroup)
        else:
            LOG.warning("Unable to obtain any node subgroup from node "
                        "name: '%s'", node.name)
        return subgroups

    def check_or_start_background_vm_ping(
            self,
            server_ip: typing.Union[str, netaddr.IPAddress],
            ssh_client: ssh.SSHClientType = None):
        if (not ssh_client and
                CONF.tobiko.tripleo.run_background_services_in_pod):
            # this fails if `oc` (openshift client) is not available
            # so, if `run_background_services_in_pod` is true, make sure
            # `oc` is available
            _openshift.check_or_start_tobiko_ping_command(server_ip)
        else:
            tripleo_nova.check_or_start_background_vm_ping(
                server_ip, ssh_client=ssh_client)

    def check_or_start_background_iperf_connection(
            self,
            server_ip: typing.Union[str, netaddr.IPAddress],
            port: int,
            protocol: str,
            ssh_client: ssh.SSHClientType = None,
            iperf3_server_ssh_client: ssh.SSHClientType = None):

        kwargs = {
            'port': port,
            'protocol': protocol,
            'ssh_client': ssh_client,
            'iperf3_server_ssh_client': iperf3_server_ssh_client,
        }

        if (not ssh_client and
                CONF.tobiko.tripleo.run_background_services_in_pod):
            # this fails if `oc` (openshift client) is not available
            # so, if `run_background_services_in_pod` is true, make sure `oc`
            # is available
            kwargs['address'] = server_ip
            kwargs['check_function'] = iperf3.check_iperf3_client_results
            kwargs['start_function'] = _openshift.start_iperf3
            kwargs['liveness_function'] = _openshift.iperf3_pod_alive
            kwargs['stop_function'] = _openshift.stop_iperf3_client
            sh.check_or_start_external_process(**kwargs)
        else:
            kwargs['server_ip'] = server_ip
            tripleo_nova.check_or_start_background_iperf_connection(**kwargs)

    def check_or_start_background_http_ping(
            self,
            server_ip: typing.Union[str, netaddr.IPAddress],  # noqa; pylint: disable=W0613
            ssh_client: ssh.SSHClientType = None):  # noqa; pylint: disable=W0613
        if (not ssh_client and
                CONF.tobiko.tripleo.run_background_services_in_pod):
            _openshift.check_or_start_tobiko_http_ping_command(
                server_ip=server_ip
            )
        else:
            sh.check_or_start_external_process(
                start_function=http_ping.start_http_ping_process,
                stop_function=http_ping.stop_http_ping_process,
                liveness_function=http_ping.http_ping_process_alive,
                check_function=http_ping.check_http_ping_results,
                server_ip=server_ip,
                ssh_client=ssh_client)


class TripleoTopologyNode(rhosp.RhospNode):

    def __init__(self,
                 topology: topology.OpenStackTopology,
                 name: str,
                 ssh_client: ssh.SSHClientFixture,
                 addresses: typing.Iterable[netaddr.IPAddress],
                 hostname: str,
                 overcloud_instance: metalsmith.Instance = None,
                 rhosp_version: tobiko.Version = None):
        # pylint: disable=redefined-outer-name
        super().__init__(topology=topology,
                         name=name,
                         ssh_client=ssh_client,
                         addresses=addresses,
                         hostname=hostname)
        self._overcloud_instance = overcloud_instance
        self._rhosp_version = rhosp_version

    @property
    def overcloud_instance(self) -> typing.Optional[metalsmith.Instance]:
        return self._overcloud_instance

    @property
    def rhosp_version(self) -> tobiko.Version:
        if self._rhosp_version is None:
            self._rhosp_version = self._get_rhosp_version()
        return self._rhosp_version

    l3_agent_conf_path = (
        '/var/lib/config-data/neutron/etc/neutron/l3_agent.ini')

    def list_running_servers(self) -> typing.List[nova.NovaServer]:
        running_servers = list()
        for server in nova.list_servers():
            if server.status != 'SHUTOFF':
                hypervisor_name = nova.get_server_hypervisor(server,
                                                             short=True)
                if self.name == hypervisor_name:
                    running_servers.append(server)
        return running_servers

    def power_on_node(self):
        if self.overcloud_instance is None:
            raise TypeError(f"Node {self.name} is not and Overcloud server")
        self.ssh_client.close()
        LOG.debug(f"Ensuring overcloud node {self.name} power is on...")
        _overcloud.power_on_overcloud_node(instance=self.overcloud_instance)
        hostname = sh.get_hostname(ssh_client=self.ssh_client)
        LOG.debug(f"Overcloud node {self.name} power is on ("
                  f"hostname={hostname})")

    def power_off_node(self):
        if self.overcloud_instance is None:
            raise TypeError(f"Node {self.name} is not and Overcloud server")
        self.ssh_client.close()
        LOG.debug(f"Ensuring overcloud node {self.name} power is off...")
        _overcloud.power_off_overcloud_node(instance=self.overcloud_instance)
        LOG.debug(f"Overcloud server node {self.name} power is off.")


def is_valid_overcloud_group_name(group_name: str, node_name: str = None):
    if not group_name:
        return False
    if group_name in ['overcloud', node_name]:
        return False
    if is_number(group_name):
        return False
    return True


def is_number(text: str):
    try:
        float(text)
    except ValueError:
        return False
    else:
        return True


def setup_tripleo_topology():
    if _undercloud.has_undercloud() or _overcloud.has_overcloud():
        topology.set_default_openstack_topology_class(TripleoTopology)


def actual_node_groups(groups):
    """return only existing node groups"""
    return set(groups).intersection(topology.list_openstack_node_groups())


def get_node(node_name):
    node_name = node_name.split('.')[0]
    return [node for node in topology.list_openstack_nodes() if
            node.name == node_name][0]


def is_composable_roles_env():
    composable_nodes = ['messaging', 'database', 'networker']
    for nodes in composable_nodes:
        if nodes in topology.list_openstack_node_groups():
            return True
    return False
