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

"""RPC layer for Vigie.

This module handles the initialization and creation of RPC clients and servers
using oslo_messaging.
"""

from typing import Optional, List, Any

from oslo_config import cfg
from oslo_log import log as logging
import oslo_messaging
from oslo_messaging import Target

from vigie.config import get_config

# Ensure configuration is loaded and CONF is available
CONF = get_config()
LOG = logging.getLogger(__name__)

# Global variables to track RPC state and version pins (used by Manager.reset())
_TRANSPORT: Optional[oslo_messaging.Transport] = None
_NOTIFIER: Optional[oslo_messaging.Notifier] = None

# Version pins for rolling upgrades. 
# These are dictionaries mapping target identifiers to version strings.
LAST_RPC_VERSIONS: dict = {}
LAST_OBJ_VERSIONS: dict = {}

_initialized = False


def init(conf: Optional[cfg.CONF] = None) -> None:
    """Initialize the RPC layer.

    This must be called before any RPC clients or servers are created.
    It sets up the transport and notifier based on the configuration.

    :param conf: Optional CONF object. If not provided, uses the global CONF.
    """
    global _TRANSPORT, _NOTIFIER, _initialized

    if _initialized:
        LOG.debug("RPC already initialized, skipping.")
        return

    # Use the provided conf or the global CONF
    if conf is None:
        conf = CONF

    LOG.debug("Initializing RPC transport for Vigie...")

    # Determine the control exchange name. Defaults to 'vigie' if not set.
    control_exchange = getattr(conf, 'control_exchange', 'vigie')

    # Set defaults for oslo_messaging transport
    oslo_messaging.set_transport_defaults(control_exchange=control_exchange)

    try:
        # Get the transport using the configured URL (e.g., rabbit://, kafka://)
        # If no transport_url is set, oslo_messaging will use defaults (usually rabbit)
        _TRANSPORT = oslo_messaging.get_transport(conf)

        # Initialize the notifier for events (optional but good practice)
        _NOTIFIER = oslo_messaging.Notifier(_TRANSPORT, driver='messaging')

        _initialized = True
        LOG.info("RPC transport initialized successfully with exchange: %s", control_exchange)
        
    except Exception as e:
        LOG.error("Failed to initialize RPC transport: %s", e, exc_info=True)
        raise


def cleanup() -> None:
    """Clean up RPC resources.

    This should be called when the service shuts down to close connections.
    """
    global _TRANSPORT, _NOTIFIER, _initialized

    LOG.debug("Cleaning up RPC resources...")

    if _NOTIFIER:
        try:
            _NOTIFIER.cleanup()
        except Exception:
            pass
        _NOTIFIER = None

    if _TRANSPORT:
        try:
            _TRANSPORT.cleanup()
        except Exception:
            pass
        _TRANSPORT = None

    _initialized = False
    LOG.info("RPC cleanup completed.")


def get_transport() -> Optional[oslo_messaging.Transport]:
    """Return the current transport.

    :raises RuntimeError: If RPC has not been initialized yet.
    """
    if not _initialized or _TRANSPORT is None:
        # Attempt lazy init if called before explicit init()
        # This is a safety net, but explicit init() is preferred
        init(CONF)
    return _TRANSPORT


def get_client(target: Target, version: Optional[str] = None, 
               namespace: Optional[str] = None,
               timeout: Optional[int] = None,
               retry: Optional[int] = None,
               serializer: Optional[Any] = None) -> oslo_messaging.RPCClient:
    """Create an RPC client.

    :param target: The messaging target (topic, server, etc.).
    :param version: The RPC API version to target.
    :param namespace: The namespace for the client.
    :param timeout: Timeout for RPC calls in seconds.
    :param retry: Number of times to retry on failure.
    :param serializer: Optional serializer for data.
    :return: An RPCClient instance.
    """
    transport = get_transport()

    # If version is not specified, check for pinned versions (for rolling upgrades)
    if version is None:
        # Construct a key to look up pinned version
        key = f"{target.topic}:{target.server}" if target.server else target.topic
        version = LAST_RPC_VERSIONS.get(key)

    return oslo_messaging.RPCClient(
        transport,
        target,
        version_cap=version,
        timeout=timeout,
        retry=retry,
        serializer=serializer
    )


def get_server(target: Target, endpoints: List[Any], 
               serializer: Optional[Any] = None,
               executor: Optional[str] = None) -> oslo_messaging.rpc.server.RPCServer:
    """Create an RPC server.

    :param target: The messaging target to listen on.
    :param endpoints: List of endpoint objects containing RPC methods.
    :param serializer: Optional serializer for data.
    :param executor: Type of executor to use ('eventlet', 'threading', etc.).
    :return: An RPCServer instance.
    """
    transport = get_transport()

    return oslo_messaging.get_rpc_server(
        transport,
        target,
        endpoints,
        serializer=serializer,
        executor='threading'
    )


def get_notifier(service: Optional[str] = None, 
                 host: Optional[str] = None,
                 publisher_id: Optional[str] = None) -> Optional[oslo_messaging.Notifier]:
    """Get a notifier instance.

    :param service: Service name for the publisher ID.
    :param host: Host name for the publisher ID.
    :param publisher_id: Explicit publisher ID.
    :return: A Notifier instance or None if not initialized.
    """
    if not _NOTIFIER:
        return None

    if publisher_id is None:
        publisher_id = f"{service or 'vigie'}.{host or CONF.host}"

    return _NOTIFIER.prepare(publisher_id=publisher_id)


def initialized() -> bool:
    """Check if RPC is initialized."""
    return _initialized
