# 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.

# This was largely inspired from the Cinder code.

"""Generic Node base class for all workers that run on hosts."""

import inspect
import os
import random
import socket
import uuid
from datetime import datetime
from typing import Optional

from oslo_db import exception as db_exc
from oslo_log import log as logging
import oslo_messaging as messaging
from oslo_service import service
from oslo_utils import importutils

from vigie import context
from vigie import rpc
from vigie.config import get_config
from vigie.db.sqlalchemy.database import get_session
from vigie.db.sqlalchemy.models import Service as ServiceModel

LOG = logging.getLogger(__name__)


def _sd_notify(state: str) -> bool:
    """Send a notification to systemd.

    :param state: The state string (e.g., 'READY=1').
    :return: True if notification was sent, False otherwise.
    """
    notify_socket = os.getenv("NOTIFY_SOCKET")
    if not notify_socket:
        # Not running under systemd with notify support, silently ignore
        return False

    # Abstract namespace socket if starts with '@'
    socket_path = notify_socket
    if notify_socket[0] == '@':
        socket_path = '\0' + notify_socket[1:]

    sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
    try:
        sock.connect(socket_path)
        sock.sendall(state.encode('utf-8'))
        LOG.debug(f"Sent systemd notification: {state}")
        return True
    except Exception as e:
        LOG.debug(f"Failed to notify systemd: {e}")
        return False
    finally:
        sock.close()


class Service(service.Service):
    """Service object for binaries running on hosts.

    A service takes a manager and enables rpc by listening to queues based
    on topic. It also periodically runs tasks on the manager and reports
    its state to the database services table.
    """

    service_id = None

    def __init__(self, host: str, binary: str, topic: str,
                 manager: str,
                 report_interval: Optional[int] = None,
                 periodic_interval: Optional[int] = None,
                 periodic_fuzzy_delay: Optional[int] = None,
                 *args, **kwargs):
        super(Service, self).__init__()

        # Store CONF as instance attribute
        self.CONF = get_config()

        # Initialize RPC
        if not rpc.initialized():
            rpc.init(self.CONF)

        self.host = host
        self.binary = binary
        self.topic = topic
        self.manager_class_name = manager

        # Import and instantiate manager
        manager_class = importutils.import_class(self.manager_class_name)
        self.manager = manager_class(host=self.host, *args, **kwargs)

        # Create service record in database
        session = None
        try:
            session = get_session()

            # Try to find existing service
            existing = session.query(ServiceModel).filter_by(
                host=self.host,
                binary=self.binary
            ).first()

            if existing:
                # Update existing service
                existing.topic = self.topic
                existing.rpc_version = getattr(manager_class, 'RPC_API_VERSION', '1.0')
                session.commit()
                Service.service_id = existing.id
                LOG.info(f"Updated existing service record: {existing.id}")
            else:
                # Create new service
                new_service = ServiceModel(
                    id=str(uuid.uuid4()),
                    host=self.host,
                    binary=self.binary,
                    topic=self.topic,
                    rpc_version=getattr(manager_class, 'RPC_API_VERSION', '1.0'),
                    disabled=False,
                    report_count=0
                )
                session.add(new_service)
                session.commit()
                Service.service_id = new_service.id
                LOG.info(f"Created new service record: {new_service.id}")

        except Exception as e:
            if session:
                session.rollback()
            LOG.error(f"Failed to create service record: {e}", exc_info=True)
            raise
        finally:
            if session:
                session.close()

        self.report_interval = report_interval or self.CONF.report_interval
        self.periodic_interval = periodic_interval or self.CONF.periodic_interval
        self.periodic_fuzzy_delay = periodic_fuzzy_delay or self.CONF.periodic_fuzzy_delay

        self.rpcserver: Optional[messaging.rpc.RPCServer] = None
        self.model_disconnected = False

    def start(self) -> None:
        """Start the service."""
        version_string = "1.0.0"  # TODO: Import from vigie.version
        LOG.info(f'Starting {self.topic} node (version {version_string})')

        self.manager.init_host()

        # Create RPC server
        LOG.debug(f"Creating RPC server for service {self.topic}")
        endpoints = [self.manager]
        target = messaging.Target(topic=self.topic, server=self.host)

        self.rpcserver = rpc.get_server(target, endpoints)
        self.rpcserver.start()

        # Add periodic timers
        if self.report_interval:
            self.tg.add_timer(self.report_interval, self.report_state,
                             initial_delay=self.report_interval)

        if self.periodic_interval:
            initial_delay = None
            if self.periodic_fuzzy_delay:
                initial_delay = random.randint(0, self.periodic_fuzzy_delay)

            # FIX: Wrap the call to inject the admin context
            def _run_periodic_tasks():
                ctxt = context.get_admin_context()
                self.manager.run_periodic_tasks(ctxt, raise_on_error=False)

            self.tg.add_timer(self.periodic_interval, _run_periodic_tasks,
                             initial_delay=initial_delay)

        # --- systemd notify: READY ---
        # Send READY=1 ONLY after RPC server is started and listening.
        # This tells systemd the service is fully initialized and ready for traffic.
        _sd_notify("READY=1")

    def stop(self) -> None:
        """Stop the service."""
        try:
            if self.rpcserver is not None:
                self.rpcserver.stop()
        except Exception:
            pass

        # Mark service as disabled in database
        self._update_service_status()

        super(Service, self).stop(graceful=True)

    def wait(self) -> None:
        """Wait for the service to stop."""
        if self.rpcserver:
            self.rpcserver.wait()
        super(Service, self).wait()

    def report_state(self) -> None:
        """Update the state of this service in the datastore."""
        if not self.manager.is_working():
            LOG.error(f'Manager for service {self.binary} {self.host} is '
                      f'reporting problems, not sending heartbeat.')
            return

        self._update_service_status()

    def _update_service_status(self):
        """Update service status in database."""
        session = None
        try:
            session = get_session()
            service_ref = session.query(ServiceModel).get(Service.service_id)
            if service_ref:
                service_ref.report_count += 1
                service_ref.last_seen_up = datetime.utcnow()
                session.commit()
        except db_exc.DBConnectionError:
            if not self.model_disconnected:
                self.model_disconnected = True
                LOG.exception('Database connection lost')
        except Exception:
            if session:
                session.rollback()
            LOG.exception('Error updating service status')
        finally:
            if session:
                session.close()

    def reset(self) -> None:
        """Reset the service."""
        self.manager.reset()
        super(Service, self).reset()

    @classmethod
    def create(cls,
               host: Optional[str] = None,
               binary: Optional[str] = None,
               topic: Optional[str] = None,
               manager: Optional[str] = None,
               report_interval: Optional[int] = None,
               periodic_interval: Optional[int] = None,
               periodic_fuzzy_delay: Optional[int] = None,
               **kwargs) -> 'Service':
        """Instantiates class and passes back application object."""
        CONF = get_config()

        if not host:
            host = CONF.worker.my_hostname or CONF.host
        if not binary:
            binary = os.path.basename(inspect.stack()[-1][1])
        if not topic:
            topic = binary.replace('.py', '').replace('-', '_')
        if not manager:
            manager = CONF.get(f'{topic}_manager', None)

        service_obj = cls(host, binary, topic, manager,
                          report_interval=report_interval,
                          periodic_interval=periodic_interval,
                          periodic_fuzzy_delay=periodic_fuzzy_delay,
                          **kwargs)
        return service_obj


def process_launcher() -> service.ProcessLauncher:
    return service.ProcessLauncher(get_config(), restart_method='mutate')


_launcher = None


def serve(server, workers=1):
    """Start the service."""
    global _launcher
    if _launcher:
        raise RuntimeError('serve() can only be called once')
    _launcher = service.launch(get_config(), server, workers=workers,
                               restart_method='mutate')


def wait() -> None:
    """Wait for the service to stop."""
    CONF = get_config()
    CONF.log_opt_values(LOG, logging.DEBUG)
    try:
        _launcher.wait()
    except KeyboardInterrupt:
        _launcher.stop()

    rpc.cleanup()
