# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Licensed under the MIT License

"""
Mesh Skill Endpoint — Phase 4 (v2.7.3)

SkillEndpoint subclass that routes skill invocations to remote
mesh nodes via the gateway. When registered with SkillBus,
remote skills appear in list_skills() and can be invoked with
bus.invoke() or included in Workflow DAG steps.

Integration:
  - Auto-registered on SkillBus when peer gateway connects
  - Auto-unregistered when peer disconnects
  - Workflow engine can use remote skill steps
  - Planner includes remote skills in tool schemas
"""

from __future__ import annotations

import asyncio
import logging
from typing import Any, Callable, Dict, List

logger = logging.getLogger(__name__)


class MeshSkillEndpoint:
    """
    A skill endpoint that routes invocations to a remote mesh node.

    Wraps gateway.request_peer_tool() behind the same interface
    as a local SkillEndpoint, so SkillBus treats it identically.

    Usage:
        endpoint = MeshSkillEndpoint(
            skill_name="meal_planner",
            node_id="peer_abc",
            node_name="Kitchen Pi",
            gateway=mesh_gateway,
        )
        bus.register_skill(
            f"remote:{node_id}:{skill_name}",
            actions={"invoke": endpoint.invoke},
        )
    """

    def __init__(
        self,
        skill_name: str,
        node_id: str,
        node_name: str,
        gateway: Any = None,
        trust_manager: Any = None,
        description: str = "",
    ):
        self.skill_name = skill_name
        self.node_id = node_id
        self.node_name = node_name
        self._gateway = gateway
        self._trust_manager = trust_manager
        self.description = description or f"Remote skill '{skill_name}' on {node_name}"

        # For SkillEndpoint compatibility
        self.actions: Dict[str, Callable] = {"invoke": self.invoke}
        self.metadata: Dict[str, Any] = {
            "remote": True,
            "node_id": node_id,
            "node_name": node_name,
            "skill_name": skill_name,
        }

    @property
    def is_available(self) -> bool:
        """Check if the remote node is connected."""
        if not self._gateway:
            return False
        return self.node_id in self._gateway.peer_gateways

    def invoke(self, payload: dict, context: dict = None) -> Any:
        """
        Invoke this skill on the remote node.

        Args:
            payload: Input data for the skill
            context: Additional context

        Returns:
            Result from remote execution
        """
        if not self.is_available:
            raise ConnectionError(f"Remote node '{self.node_name}' is not connected")

        # Check permission
        if self._trust_manager and not self._trust_manager.check_permission(
            self.node_id, "invoke_skills"
        ):
            raise PermissionError(f"No invoke_skills permission for '{self.node_name}'")

        # Send via gateway (sync wrapper around async)
        try:
            loop = asyncio.get_event_loop()
            if loop.is_running():
                # Already in an async context — use ensure_future
                future = asyncio.ensure_future(
                    self._gateway.request_peer_tool(
                        peer_id=self.node_id,
                        tool_name=self.skill_name,
                        tool_input=payload,
                    )
                )
                # Can't await in sync context, return the future
                return future
            else:
                return loop.run_until_complete(
                    self._gateway.request_peer_tool(
                        peer_id=self.node_id,
                        tool_name=self.skill_name,
                        tool_input=payload,
                    )
                )
        except RuntimeError:
            return asyncio.run(
                self._gateway.request_peer_tool(
                    peer_id=self.node_id,
                    tool_name=self.skill_name,
                    tool_input=payload,
                )
            )

    def get_schema(self) -> dict:
        """Get tool schema for this remote skill (for planner)."""
        return {
            "name": f"remote:{self.node_id[:8]}:{self.skill_name}",
            "description": self.description,
            "_source": f"peer:{self.node_id}",
            "_remote": True,
            "_node_name": self.node_name,
        }

    def __repr__(self) -> str:
        status = "connected" if self.is_available else "disconnected"
        return f"MeshSkillEndpoint({self.skill_name}@{self.node_name} [{status}])"


# ============================================================
# HELPER: Register/unregister remote skills on bus
# ============================================================


def register_remote_skills(
    bus: Any,
    node_id: str,
    node_name: str,
    skills: List[dict],
    gateway: Any,
    trust_manager: Any = None,
) -> List[MeshSkillEndpoint]:
    """
    Register all remote skills from a peer gateway on the SkillBus.

    Called when a peer gateway connects and shares its tool registry.

    Args:
        bus: SkillBus instance
        node_id: Peer's node ID
        node_name: Peer's display name
        skills: List of skill dicts (with 'name' and 'description')
        gateway: MeshGateway instance
        trust_manager: MeshTrustManager instance

    Returns:
        List of created MeshSkillEndpoint instances
    """
    endpoints = []
    for skill in skills:
        skill_name = skill.get("name", "")
        if not skill_name:
            continue

        endpoint = MeshSkillEndpoint(
            skill_name=skill_name,
            node_id=node_id,
            node_name=node_name,
            gateway=gateway,
            trust_manager=trust_manager,
            description=skill.get("description", ""),
        )

        # Register on bus with namespaced name
        bus_name = f"remote:{node_id[:8]}:{skill_name}"
        try:
            bus.register_skill(bus_name, actions={"invoke": endpoint.invoke})
            endpoints.append(endpoint)
        except Exception as e:
            logger.warning(f"Failed to register remote skill {bus_name}: {e}")

    if endpoints:
        logger.info(
            f"Registered {len(endpoints)} remote skills from {node_name}: "
            f"{[e.skill_name for e in endpoints[:5]]}"
        )

    return endpoints


def unregister_remote_skills(bus: Any, node_id: str):
    """
    Unregister all remote skills from a disconnected peer.

    Called when a peer gateway disconnects.
    """
    prefix = f"remote:{node_id[:8]}:"
    removed = 0

    # SkillBus stores endpoints in _endpoints dict
    if hasattr(bus, "_endpoints"):
        to_remove = [name for name in bus._endpoints if name.startswith(prefix)]
        for name in to_remove:
            try:
                del bus._endpoints[name]
                removed += 1
            except (KeyError, AttributeError):
                pass

    if removed:
        logger.info(f"Unregistered {removed} remote skills for node {node_id[:8]}")
