# Copyright 2026 Guildhouse Dev
# SPDX-License-Identifier: Apache-2.0

"""DeviceRouter — automatic connector selection based on target device.

The DeviceRouter inspects the target device's characteristics (OS,
management channel, enrollment status) and routes operations to the
appropriate connector.

Routing decision logic:
  1. If the operation is API-mediated (compliance check, inventory
     query), route to the Intune connector regardless of device OS.
  2. If the device is Windows and the operation is session-based
     (execute, configure), route to PowerShell connector.
  3. If the device is Linux/macOS and the operation is session-based,
     route to Bascule connector.
  4. If the operation is multi-host (fleet-wide playbook), route to
     Ansible connector.
  5. If the device is unknown, raise ``UnknownDevice``.

The device inventory comes from:
  - Intune device cache (Windows/iOS/Android managed devices)
  - Bascule fleet registry (Linux governed endpoints) — future
  - Manual registration (for unmanaged devices) — future

Rust port note:
  ``DeviceRouter`` maps to a struct with connector registry and
  device cache references.  The routing logic is a match statement
  on ``(operation_type, device_os)``.
"""

from __future__ import annotations

import logging
from typing import Any, Optional

from gsap_broker.connectors.base import ConnectorContext, ConnectorResult
from gsap_broker.connectors.registry import ConnectorRegistry
from gsap_broker.intune.device_cache import DeviceComplianceCache

logger = logging.getLogger(__name__)

# Operations that are API-mediated (go through Intune Graph API
# regardless of device OS).
_API_OPERATIONS = frozenset({
    "get_compliance", "list_devices", "get_device",
    "sync_device", "retire_device", "wipe_device",
})

# Operations that span multiple hosts (go through Ansible).
_FLEET_OPERATIONS = frozenset({
    "playbook", "adhoc", "role", "collect",
})

# OS → preferred session connector mapping.
_OS_CONNECTOR_MAP = {
    "windows": "powershell",
    "linux": "bascule",
    "macos": "bascule",
    "ios": "intune",  # mobile — API-only
    "android": "intune",  # mobile — API-only
}


class UnknownDevice(Exception):
    """Target device is not in any inventory."""

    def __init__(self, target: str):
        self.target = target
        super().__init__(f"Unknown device: {target}")


class DeviceRouter:
    """Routes operations to the appropriate connector based on target.

    Combines the device inventory (Intune cache) with the connector
    registry to select the best management channel for each operation.
    """

    def __init__(
        self,
        connector_registry: ConnectorRegistry,
        device_cache: Optional[DeviceComplianceCache] = None,
    ):
        self._connectors = connector_registry
        self._devices = device_cache

    async def route(
        self, operation: str, target: str, context: ConnectorContext
    ) -> tuple[str, str]:
        """Determine which connector handles this operation.

        Returns:
            (connector_id, mapped_operation) — the connector to use
            and the operation name to pass to it (may differ from the
            input if the router translates operations).

        Raises:
            UnknownDevice: if the target is not in any inventory and
                cannot be routed.
        """
        # API-mediated operations always go to Intune
        if operation in _API_OPERATIONS:
            return ("intune", operation)

        # Fleet operations go to Ansible
        if operation in _FLEET_OPERATIONS:
            return ("ansible", operation)

        # Session operations: route based on device OS
        device_os = await self._detect_os(target)
        if device_os is None:
            raise UnknownDevice(target)

        connector_id = _OS_CONNECTOR_MAP.get(device_os.lower(), "bascule")
        return (connector_id, operation)

    async def invoke(
        self,
        operation: str,
        target: str,
        parameters: dict[str, Any],
        context: ConnectorContext,
    ) -> ConnectorResult:
        """Route and invoke in one call."""
        try:
            connector_id, mapped_op = await self.route(operation, target, context)
        except UnknownDevice as e:
            return ConnectorResult(success=False, error=str(e))

        connector = self._connectors.get(connector_id)
        if connector is None:
            return ConnectorResult(
                success=False,
                error=f"Connector '{connector_id}' not registered",
            )

        parameters["target"] = target
        return await connector.invoke(mapped_op, parameters, context)

    async def _detect_os(self, target: str) -> Optional[str]:
        """Look up the device OS from the Intune cache.

        Future: also check Bascule fleet registry, manual
        registration, DNS-based hints, etc.
        """
        if self._devices is None:
            return None

        cached = await self._devices.get(target)
        if cached is not None:
            # ComplianceState doesn't carry OS — the cache is keyed
            # by device_id but the Intune connector's list_devices
            # response has os_type.  For now, return None and let the
            # caller handle it.  A proper DeviceInventory cache (not
            # just compliance) is needed — tracked for next sprint.
            return None

        return None
