"""LangGraph node primitives with actor-first semantics (ported from v2)."""

from __future__ import annotations

import asyncio
import logging
from copy import deepcopy
from enum import Enum
from typing import Any, cast

from pydantic import BaseModel, Field

from cleveragents.agents.base import Agent

try:
    from cleveragents.agents.tool import ToolAgent  # type: ignore
except Exception:  # pragma: no cover

    class ToolAgent(Agent):  # type: ignore
        async def process_message(self, message: Any, context: Any = None) -> str:
            return ""

        def get_capabilities(self) -> list[str]:
            return []


from cleveragents.langgraph.state import GraphState


class NodeType(Enum):
    AGENT = "agent"
    FUNCTION = "function"
    TOOL = "tool"
    CONDITIONAL = "conditional"
    SUBGRAPH = "subgraph"
    START = "start"
    END = "end"
    MESSAGE_ROUTER = "message_router"


class NodeConfig(BaseModel):  # pylint: disable=too-many-instance-attributes
    name: str
    type: NodeType
    agent: str | None = None
    function: str | None = None
    tools: list[str] = Field(default_factory=list)
    retry_policy: dict[str, Any] | None = None
    timeout: float | None = None
    parallel: bool = False
    condition: dict[str, Any] | None = None
    subgraph: str | None = None
    metadata: dict[str, Any] = Field(default_factory=dict)


class Edge(BaseModel):
    source: str
    target: str
    condition: dict[str, Any] | None = None
    metadata: dict[str, Any] = Field(default_factory=dict)


class Node:  # pylint: disable=too-many-instance-attributes
    MAX_HISTORY_MESSAGES = 20
    MAX_HISTORY_CHARS = 12000

    def __init__(self, config: NodeConfig, agents: dict[str, Agent] | None = None):
        self.config = config
        self.name = config.name
        self.type = config.type
        self.agents = agents or {}
        self.logger = logging.getLogger(__name__)
        self.execution_count = 0
        self.last_execution_time: float | None = None
        self.last_error: Exception | None = None

    def _prepare_conversation_history(
        self, messages: list[dict[str, Any]]
    ) -> tuple[list[dict[str, Any]], bool]:
        if not messages:
            return [], False
        raw_max_messages = self.config.metadata.get(
            "max_history_messages", self.MAX_HISTORY_MESSAGES
        )
        raw_max_chars = self.config.metadata.get(
            "max_history_chars", self.MAX_HISTORY_CHARS
        )
        try:
            max_messages = max(1, int(raw_max_messages))
        except (TypeError, ValueError):
            max_messages = self.MAX_HISTORY_MESSAGES
        try:
            max_chars = max(500, int(raw_max_chars))
        except (TypeError, ValueError):
            max_chars = self.MAX_HISTORY_CHARS
        collected: list[dict[str, Any]] = []
        total_chars = 0
        for msg in reversed(messages):
            content = str(msg.get("content", ""))
            total_chars += len(content)
            collected.append(deepcopy(msg))
            if len(collected) >= max_messages or total_chars >= max_chars:
                break
        collected.reverse()
        truncated = len(collected) < len(messages)
        return collected, truncated

    async def execute(self, state: GraphState) -> dict[str, Any]:  # pylint: disable=too-many-branches
        self.execution_count += 1
        loop = asyncio.get_event_loop()
        start_time = loop.time()
        try:
            state_updates = {"current_node": self.name}
            if self.type == NodeType.AGENT:
                result = await self._execute_agent(state)
            elif self.type == NodeType.FUNCTION:
                result = await self._execute_function(state)
            elif self.type == NodeType.TOOL:
                result = await self._execute_tool()
            elif self.type == NodeType.CONDITIONAL:
                result = await self._execute_conditional(state)
            elif self.type == NodeType.SUBGRAPH:
                result = await self._execute_subgraph(state)
            elif self.type == NodeType.MESSAGE_ROUTER:
                result = await self._execute_message_router(state)
            elif self.type == NodeType.START:
                result = {"started": True}
            elif self.type == NodeType.END:
                result = {"completed": True}
            else:
                result = {}
            if isinstance(result, dict):
                state_updates.update(cast(dict[str, Any], result))
            self.last_error = None
            return state_updates
        except Exception as exc:  # pylint: disable=broad-except
            self.last_error = exc
            self.logger.error("Node %s execution failed: %s", self.name, exc)
            if self.config.retry_policy:
                retry_count = self.config.retry_policy.get("max_retries", 3)
                retry_delay = self.config.retry_policy.get("delay", 1.0)
                for _ in range(retry_count):
                    await asyncio.sleep(retry_delay)
                    try:
                        return await self.execute(state)
                    except Exception:  # pylint: disable=broad-except
                        self.logger.warning("Node %s retry attempt failed", self.name)
                        continue
            return {"error": str(exc), "failed_node": self.name}
        finally:
            self.last_execution_time = loop.time() - start_time

    async def _execute_agent(self, state: GraphState) -> dict[str, Any]:
        if not self.config.agent:
            raise ValueError(f"Agent node {self.name} has no agent specified")
        agent = self.agents.get(self.config.agent)
        if not agent:
            # Fallback: synthesize a response when agent instance is unavailable
            return {
                "messages": [
                    {
                        "role": "assistant",
                        "content": f"Agent {self.config.agent} not found",
                        "node": self.name,
                        "agent": self.config.agent,
                    }
                ],
                "current_node": self.name,
            }

        if state.messages:
            if isinstance(agent, ToolAgent):
                current_msg = state.metadata.get("current_message", "")
                agent_input = (
                    str(current_msg)
                    if current_msg
                    else state.messages[-1].get("content", "")
                )
            else:
                current_msg = state.metadata.get("current_message")
                if current_msg is not None:
                    agent_input = str(current_msg)
                else:
                    last_user_message = None
                    for msg in reversed(state.messages):
                        if msg.get("role") == "user":
                            last_user_message = msg.get("content", "")
                            break
                    agent_input = last_user_message or state.messages[-1].get(
                        "content", ""
                    )
        else:
            agent_input = ""

        trimmed_history, history_truncated = self._prepare_conversation_history(
            state.messages
        )
        graph_state_dict = state.to_dict()
        graph_state_dict["messages"] = trimmed_history

        context = {
            "graph_state": graph_state_dict,
            "conversation_history": trimmed_history,
            "full_context": True,
        }
        if history_truncated:
            context["_history_truncated"] = True
            context["_history_original_length"] = len(state.messages)
        if state.metadata:
            context.update(state.metadata)
        nested_context = context.get("context")
        if isinstance(nested_context, dict):
            for key, value in nested_context.items():
                context.setdefault(key, value)
        context_snapshot = deepcopy(context) if context else None

        try:
            agent_response = await agent.process_message(agent_input, context)
        except Exception as exc:  # pylint: disable=broad-except
            self.logger.error("Agent %s execution failed: %s", agent.name, exc)
            agent_response = f"Error processing message: {exc!s}"

        state_updates = {
            "messages": [
                {
                    "role": "assistant",
                    "content": agent_response,
                    "node": self.name,
                    "agent": self.config.agent,
                }
            ],
            "current_node": self.name,
        }
        metadata_updates = state_updates.setdefault("metadata", {})
        metadata_updates["last_agent_node"] = self.name

        if context:
            ignore_keys = {
                "graph_state",
                "conversation_history",
                "full_context",
                "_history_truncated",
                "_history_original_length",
            }
            snapshot_keys = set(context_snapshot.keys()) if context_snapshot else set()
            current_keys = set(context.keys())
            all_keys = snapshot_keys | current_keys
            _missing = object()
            for key in all_keys:
                if key in ignore_keys:
                    continue
                before = (
                    context_snapshot.get(key, _missing)
                    if context_snapshot
                    else _missing
                )
                after = context.get(key, _missing)
                if after is not _missing and after != before:
                    metadata_updates[key] = after

        return state_updates

    async def _execute_function(self, state: GraphState) -> dict[str, Any]:
        func_name = self.config.function
        if not func_name:
            raise ValueError(f"Function node {self.name} has no function specified")
        fn = self.agents.get(func_name)
        if not fn:
            raise ValueError(f"Function {func_name} not found")
        if not callable(fn):
            raise ValueError(f"Function {func_name} is not callable")
        if asyncio.iscoroutinefunction(fn):
            result = await fn(state)
        else:
            loop = asyncio.get_event_loop()
            result = await loop.run_in_executor(None, lambda: fn(state))
            if asyncio.iscoroutine(result):
                result = await result
        return {
            "messages": [
                {
                    "role": "assistant",
                    "content": result,
                    "node": self.name,
                    "function": func_name,
                }
            ]
        }

    async def _execute_tool(self) -> dict[str, Any]:
        return {"tool_executed": True, "node": self.name}

    async def _execute_conditional(self, state: GraphState) -> dict[str, Any]:
        condition = self.config.condition or {}
        field = condition.get("field")
        expected = condition.get("equals")
        result = False
        if field:
            for msg in reversed(state.messages):
                if msg.get("role") == "assistant" and field in msg:
                    result = msg.get(field) == expected
                    break
        return {
            "condition_result": result,
            "metadata": {"conditional_edges_used": True},
        }

    async def _execute_subgraph(self, state: GraphState) -> dict[str, Any]:
        # Placeholder: could invoke nested LangGraph by name
        return {"subgraph_invoked": self.config.subgraph or ""}

    async def _execute_message_router(self, state: GraphState) -> dict[str, Any]:
        rules = self.config.metadata.get("rules", [])
        current_msg = state.metadata.get("current_message") if state.metadata else None
        routed_to = None
        for rule in rules:
            cond = rule.get("condition")
            target = rule.get("target")
            if target is None:
                continue
            if cond is None:
                routed_to = target
                break
            if self._eval_condition(current_msg, cond):
                routed_to = target
                break
        return {"metadata": {"routed_to": routed_to, "current_message": current_msg}}

    def _eval_condition(self, message: Any, condition: dict[str, Any]) -> bool:
        if "equals" in condition:
            return message == condition["equals"]
        if "field" in condition and isinstance(message, dict):
            return message.get(condition["field"]) == condition.get("value")
        return False

    def evaluate_edge_condition(self, edge: Edge, state: GraphState) -> bool:
        if not edge.condition:
            return True
        cond = edge.condition
        if "equals" in cond:
            field = cond.get("field", "content")
            if state.messages:
                return state.messages[-1].get(field) == cond["equals"]
        return True

    def can_execute_parallel(self) -> bool:
        return self.config.parallel
