# Copyright (c) 2026 Thomas Goirand <zigo@debian.org>
#
# 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.

"""Base Manager class for Vigie.

Managers are responsible for a certain aspect of the system. It is a logical
grouping of code relating to a portion of the system. In general other
components should be using the manager to make changes to the components that
it is responsible for.

This module provides Manager, a base class for managers.
"""

import time
from typing import Optional
import uuid

from oslo_config import cfg
from oslo_log import log as logging
import oslo_messaging as messaging
from oslo_messaging import Target
from oslo_service import periodic_task
from oslo_utils import timeutils

from vigie import context
from vigie import rpc
from vigie.config import get_config

CONF = get_config()
LOG = logging.getLogger(__name__)


class PeriodicTasks(periodic_task.PeriodicTasks):
    """Helper class to wrap oslo_service periodic tasks."""
    def __init__(self):
        super(PeriodicTasks, self).__init__(CONF)


class Manager(PeriodicTasks):
    """Base Manager class for Vigie services.

    Managers handle the business logic for a specific service type.
    They are wrapped by the Service class which handles RPC and DB reporting.
    """

    # Set RPC API version to 1.0 by default.
    RPC_API_VERSION = '1.0'
    target = messaging.Target(version=RPC_API_VERSION)

    # Service-specific attributes to be overridden by subclasses
    RPC_HEARTBEAT_TOPIC: Optional[str] = None
    RPC_HEARTBEAT_SERVICE_NAME: Optional[str] = None

    def __init__(self, host: Optional[str] = None, **kwargs):
        if not host:
            host = CONF.host

        self.host = host
        self.additional_endpoints = []

        # Mandatory: Heartbeat timeout must be present for the service wrapper
        self.rpc_heartbeat_timeout = CONF.rpc_heartbeat_timeout

        # Optional but configured: Availability zone.
        # Defaults to 'vigie' in config.py if not specified by the user.
        self.availability_zone = CONF.availability_zone

        super(Manager, self).__init__()

    def init_host(self, service_id: Optional[str] = None):
        """Handle initialization if this is a standalone service.

        A hook point for services to execute tasks before the services are made
        available (i.e. showing up on RPC and starting to accept RPC calls).
        Child classes should override this method.

        :param service_id: ID of the service where the manager is running.
        """
        LOG.info("Manager for host %s initialized.", self.host)

    def init_host_with_rpc(self):
        """A hook for service to do jobs after RPC is ready.

        Like init_host(), this method is a hook where services get a chance
        to execute tasks that *need* RPC. Child classes should override
        this method.
        """
        pass

    def is_working(self) -> bool:
        """Method indicating if service is working correctly.

        This method is supposed to be overridden by subclasses and return if
        manager is working correctly. By default, it checks the RPC heartbeat.
        """
        return self._rpc_heartbeat_ping()

    def _rpc_heartbeat_ping(self) -> bool:
        """Perform RPC ping to verify service responsiveness.

        This is the common heartbeat logic used by all services.
        Subclasses should call this from their is_working() method
        after performing any service-specific pre-checks.

        :returns: True if service is working, False otherwise
        """
        if not self.RPC_HEARTBEAT_TOPIC:
            # Service doesn't support heartbeat, assume working
            return True

        ping_id = str(uuid.uuid4())
        timeout = self.rpc_heartbeat_timeout

        try:
            LOG.debug("Heartbeat: ping to %s, topic=%s",
                      self.host, self.RPC_HEARTBEAT_TOPIC)
            target = Target(
                topic=self.RPC_HEARTBEAT_TOPIC,
                server=self.host,
            )
            client = rpc.get_client(target=target)
            ctxt = context.get_admin_context()

            start = time.time()
            reply = client.call(ctxt, 'ping', timeout=timeout, ping_id=ping_id)

            if reply and reply.get('pong') == ping_id:
                LOG.debug("Heartbeat: ping %s replied in %.3f sec",
                          ping_id, time.time() - start)
                return True
            else:
                LOG.warning("Heartbeat: ping %s got invalid reply: %s",
                            ping_id, reply)
                return False
        except Exception as e:
            LOG.error("Heartbeat: ping %s failed: %s", ping_id, str(e))
            return False

    def ping(self, ctxt, ping_id, timeout=None):
        """Reply to a ping message from the heartbeat thread.

        This runs in the main thread's RPC server greenthread.
        If this method is called, the main thread is alive.

        :param ctxt: Security context
        :param ping_id: Unique identifier for this ping
        :param timeout: Timeout value (unused, kept for compatibility)
        :returns: Pong response with metadata
        """
        LOG.debug("Heartbeat: received ping %s on %s", ping_id, self.host)
        return {
            'pong': ping_id,
            'timestamp': time.time(),
            'host': self.host,
            'service': self.RPC_HEARTBEAT_SERVICE_NAME or 'vigie-unknown',
        }

    def reset(self):
        """Method executed when SIGHUP is caught by the process.

        We're utilizing it to reset RPC API version pins to avoid restart of
        the service when rolling upgrade is completed.
        """
        LOG.info('Resetting cached RPC version pins.')
        # Ensure these exist in vigie/rpc.py, or remove this block if not needed yet
        if hasattr(rpc, 'LAST_OBJ_VERSIONS'):
            rpc.LAST_OBJ_VERSIONS = {}
        if hasattr(rpc, 'LAST_RPC_VERSIONS'):
            rpc.LAST_RPC_VERSIONS = {}

    def run_periodic_tasks(self, ctxt, raise_on_error=False):
        """Run periodic tasks for this manager.

        This method is called by the Service wrapper periodically.
        It delegates to the oslo_service PeriodicTasks implementation.
        """
        return super(Manager, self).run_periodic_tasks(ctxt, raise_on_error=raise_on_error)
