#    Copyright 2023 Red Hat, Inc. All rights reserved.
#
#    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.

import inspect
import threading

from futurist import periodics
import netaddr
from neutron_lib import constants as n_const
from oslo_config import cfg
from oslo_log import log as logging
from ovsdbapp.backend.ovs_idl import connection

from ovn_octavia_provider.common import clients
from ovn_octavia_provider.common import config as ovn_conf
from ovn_octavia_provider.common import constants as ovn_const
from ovn_octavia_provider.ovsdb import impl_idl_ovn

CONF = cfg.CONF  # Gets Octavia Conf as it runs under o-api domain

LOG = logging.getLogger(__name__)


class MaintenanceThread(object):

    def __init__(self):
        self._callables = []
        self._thread = None
        self._worker = None

    def add_periodics(self, obj):
        for name, member in inspect.getmembers(obj):
            if periodics.is_periodic(member):
                LOG.info('Periodic task found: %(owner)s.%(member)s',
                         {'owner': obj.__class__.__name__, 'member': name})
                self._callables.append((member, (), {}))

    def start(self):
        if self._thread is None:
            self._worker = periodics.PeriodicWorker(self._callables)
            self._thread = threading.Thread(target=self._worker.start)
            self._thread.daemon = True
            self._thread.start()

    def stop(self):
        self._worker.stop()
        self._worker.wait()
        self._thread.join()
        self._worker = self._thread = None


class DBInconsistenciesPeriodics(object):

    def __init__(self):
        self.ovn_nbdb = impl_idl_ovn.OvnNbIdlForLb()
        c = connection.Connection(self.ovn_nbdb,
                                  ovn_conf.get_ovn_ovsdb_timeout())
        self.ovn_nbdb_api = impl_idl_ovn.OvsdbNbOvnIdl(c)

    @periodics.periodic(spacing=600, run_immediately=True)
    def change_device_owner_lb_hm_ports(self):
        """Change the device_owner for the OVN LB HM port existing.

        The OVN LB HM port used for send the health checks to the backend
        members has a new device_owner, it will use the value
        onv-lb-hm:distributed in order to keep the behaviour on Neutron as a
        LOCALPORT. Also this change will add device-id as ovn-lb-hm:{subnet}
        to get more robust.
        """
        LOG.debug('Maintenance task: checking device_owner for OVN LB HM '
                  'ports.')
        neutron_client = clients.get_neutron_client()
        ovn_lb_hm_ports = neutron_client.ports(
            device_owner=n_const.DEVICE_OWNER_DISTRIBUTED)

        check_neutron_support_new_device_owner = True
        for port in ovn_lb_hm_ports:
            if port.name.startswith('ovn-lb-hm'):
                LOG.debug('Maintenance task: updating device_owner and '
                          'adding device_id for port id %s', port.id)
                neutron_client.update_port(
                    port.id, device_owner=ovn_const.OVN_LB_HM_PORT_DISTRIBUTED,
                    device_id=port.name)

                # NOTE(froyo): Check that the port is now of type LOCALPORT in
                # the OVN NB DB or perform a rollback in other cases. Such
                # cases could indicate that Neutron is in the process of being
                # updated or that the user has forgotten to update Neutron to a
                # version that supports this change
                if check_neutron_support_new_device_owner:
                    port_ovn = self.ovn_nbdb_api.db_find_rows(
                        "Logical_Switch_Port", ("name", "=", port.id)).execute(
                        check_error=True)
                    if len(port_ovn) and port_ovn[0].type != 'localport':
                        LOG.debug('Maintenance task: port %s updated but '
                                  'looks like Neutron does not support this '
                                  'new device_owner, or maybe is updating '
                                  'version, so restoring to old values and '
                                  'waiting another iteration of this task',
                                  port.id)
                        neutron_client.update_port(
                            port.id,
                            device_owner=n_const.DEVICE_OWNER_DISTRIBUTED,
                            device_id='')
                        # Break the loop as do not make sense change the rest
                        break
                    check_neutron_support_new_device_owner = False
        else:
            # NOTE(froyo): No ports found to update, or all of them done.
            LOG.debug('Maintenance task: no more ports left, stopping the '
                      'periodic task.')
            raise periodics.NeverAgain()
        LOG.debug('Maintenance task: device_owner and device_id checked for '
                  'OVN LB HM ports.')

    # TODO(froyo): Remove this in the Caracal+4 cycle
    @periodics.periodic(spacing=600, run_immediately=True)
    def format_ip_port_mappings_ipv6(self):
        """Give correct format to `ip_port_mappings` for IPv6 backend members.

        The `ip_port_mappings` field for OVN LBs should be a dictionary with
        keys following the format:
        `${MEMBER_IP}=${LSP_NAME_MEMBER}:${HEALTH_SRC_IP}`. However, when
        `MEMBER_IP` and `HEALTH_SRC_IP` are IPv6 addresses, they should be
        enclosed in `[]`.
        """
        LOG.debug('Maintenance task: Ensure correct formatting of '
                  'ip_port_mappings for IPv6 backend members.')
        ovn_lbs = self.ovn_nbdb_api.db_find_rows(
            'Load_Balancer', ('ip_port_mappings', '!=', {})).execute()
        for lb in ovn_lbs:
            mappings = {}
            for k, v in lb.ip_port_mappings.items():
                try:
                    # If first element is IPv4 (mixing IPv4 and IPv6 not
                    # allowed) or get AddrFormatError (IPv6 already fixed) we
                    # can jump to next item
                    if netaddr.IPNetwork(k).version == n_const.IP_VERSION_4:
                        break
                except netaddr.AddrFormatError:
                    break
                port_uuid, src_ip = v.split(':', 1)
                mappings[f'[{k}]'] = f'{port_uuid}:[{src_ip}]'
            if mappings:
                self.ovn_nbdb_api.db_clear(
                    'Load_Balancer', lb.uuid, 'ip_port_mappings'
                ).execute(check_error=True)
                self.ovn_nbdb_api.db_set(
                    'Load_Balancer', lb.uuid, ('ip_port_mappings', mappings)
                ).execute(check_error=True)

        LOG.debug('Maintenance task: no more ip_port_mappings to format, '
                  'stopping the periodic task.')
        raise periodics.NeverAgain()
