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

"""
Remote Agent — Phase 4 (v2.7.3)

AgentBase subclass that delegates execute() to a remote mesh node
via the existing MeshGateway. The Orchestrator sees RemoteAgents as
normal agents — HIERARCHICAL, CONSENSUS, DEBATE, and all other
strategies work transparently with mixed local + remote agents.

Integration:
  - Auto-registered in AgentRegistry when a peer gateway connects
  - Auto-unregistered when peer disconnects
  - Planner sees remote skills in tool schemas
  - Self-correction classifies delegation errors
"""

from __future__ import annotations

import asyncio
import logging
import time
from typing import TYPE_CHECKING, Any, Dict, List, Optional

if TYPE_CHECKING:
    from ..orchestration import Task, TaskResult  # noqa: F811 - runtime import below supersedes

logger = logging.getLogger(__name__)


# Import orchestration types — needed at runtime for subclassing
from ..orchestration import (  # noqa: E402
    AgentBase,
    AgentRole,
    AgentStatus,
    Task,
    TaskResult,
)


class RemoteAgent(AgentBase):
    """
    AgentBase subclass that delegates to a remote mesh node.

    The Orchestrator treats this identically to a local FunctionAgent
    or LLMAgent. The difference: execute() sends a tool/task request
    over the mesh WebSocket instead of running locally.

    Usage:
        remote = RemoteAgent(
            node_id="peer_abc",
            node_name="Kitchen Pi",
            skills=["meal_planner", "recipe_search"],
            gateway=mesh_gateway,
        )
        orchestrator.registry.register(remote)

        # Now orchestrator.execute(task, strategy=HIERARCHICAL) can
        # delegate to this remote agent automatically.
    """

    def __init__(
        self,
        node_id: str,
        node_name: str,
        skills: Optional[List[str]] = None,
        gateway: Any = None,
        trust_manager: Any = None,
        role: AgentRole = AgentRole.SPECIALIST,
    ):
        super().__init__(
            agent_id=f"remote:{node_id}",
            name=f"{node_name} (remote)",
            role=role,
            capabilities=skills or [],
        )
        self.node_id = node_id
        self.node_name = node_name
        self.skills = skills or []
        self._gateway = gateway
        self._trust_manager = trust_manager
        self._metadata["remote"] = True
        self._metadata["node_id"] = node_id

    @property
    def is_connected(self) -> bool:
        """Check if the peer gateway is still connected."""
        if not self._gateway:
            return False
        return self.node_id in self._gateway.peer_gateways

    async def execute(self, task: Task) -> TaskResult:
        """
        Execute a task by delegating to the remote mesh node.

        Sends a TOOL_REQUEST over the peer gateway WebSocket and
        awaits the response. Falls back to error if disconnected
        or permission denied.
        """
        self.status = AgentStatus.BUSY
        self._current_task = task
        start_time = time.time()

        try:
            # Check connection
            if not self.is_connected:
                return TaskResult(
                    task_id=task.id,
                    success=False,
                    error=f"Remote node '{self.node_name}' is disconnected",
                    agent_id=self.id,
                    elapsed_ms=0,
                )

            # Check trust permission
            if self._trust_manager and not self._trust_manager.check_permission(
                self.node_id, "delegate_tasks"
            ):
                return TaskResult(
                    task_id=task.id,
                    success=False,
                    error=f"No delegate_tasks permission for '{self.node_name}'",
                    agent_id=self.id,
                    elapsed_ms=0,
                )

            # Determine which skill/tool to invoke
            skill_name = (
                task.input_data.get("skill", "") if isinstance(task.input_data, dict) else ""
            )
            if not skill_name:
                # Try to match task description to a remote skill
                skill_name = self._match_skill(task.description)

            # Send request via gateway
            result_text = await self._gateway.request_peer_tool(
                peer_id=self.node_id,
                tool_name=skill_name or "chat",
                tool_input={
                    "task_description": task.description,
                    "input_data": task.input_data,
                    "context": task.context,
                },
                timeout=int(task.timeout),
            )

            elapsed = (time.time() - start_time) * 1000

            return TaskResult(
                task_id=task.id,
                success=True,
                output=result_text,
                agent_id=self.id,
                elapsed_ms=elapsed,
            )

        except asyncio.TimeoutError:
            elapsed = (time.time() - start_time) * 1000
            return TaskResult(
                task_id=task.id,
                success=False,
                error=f"Remote execution timed out on '{self.node_name}'",
                agent_id=self.id,
                elapsed_ms=elapsed,
            )
        except Exception as e:
            elapsed = (time.time() - start_time) * 1000
            return TaskResult(
                task_id=task.id,
                success=False,
                error=f"Remote execution failed: {e}",
                agent_id=self.id,
                elapsed_ms=elapsed,
            )
        finally:
            self.status = AgentStatus.IDLE
            self._current_task = None

    def can_handle(self, task: Task) -> bool:
        """Check if this remote agent can handle a task."""
        if not self.is_connected:
            return False

        required = task.metadata.get("required_capabilities", [])
        if not required:
            return True
        return any(cap in self.capabilities for cap in required)

    def _match_skill(self, description: str) -> str:
        """Try to match a task description to a remote skill name."""
        desc_lower = description.lower()
        for skill in self.skills:
            if skill.lower() in desc_lower:
                return skill
        # Return first skill as fallback
        return self.skills[0] if self.skills else ""

    def get_info(self) -> Dict[str, Any]:
        info = super().get_info()
        info["remote"] = True
        info["node_id"] = self.node_id
        info["node_name"] = self.node_name
        info["connected"] = self.is_connected
        info["skills"] = self.skills
        return info
