"""Abstract base class for all LangGraph agent nodes.

Each specialist agent subclasses BaseDataBridgeAgent, declares its name,
tools, and system prompt, then implements ``run()`` which receives the
shared WorkflowState and returns an updated copy.

When ``langchain-anthropic`` is available the base class provides
``_invoke_llm()`` which binds tools to ChatAnthropic for agentic reasoning.
When the package is missing, agents fall back to deterministic execution
(directly calling tool functions without LLM mediation).
"""

from __future__ import annotations

import logging
import time
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional

from .state_schema import WorkflowState, PhaseResultDict
from .message_protocol import AgentMessage, create_response, create_error

logger = logging.getLogger(__name__)

# Optional LangChain imports
try:
    from langchain_anthropic import ChatAnthropic
    from langchain_core.messages import HumanMessage, SystemMessage
    LANGCHAIN_AVAILABLE = True
except ImportError:
    LANGCHAIN_AVAILABLE = False


class BaseDataBridgeAgent(ABC):
    """Abstract base for DataBridge LangGraph agent nodes."""

    name: str = "base_agent"
    description: str = "Base DataBridge agent"
    phase_name: str = ""  # maps to PHASE_ORDER entry

    def __init__(
        self,
        tools: Optional[List[Any]] = None,
        system_prompt: str = "",
        model: str = "claude-sonnet-4-20250514",
        temperature: float = 0.1,
        progress_callback: Optional[Callable] = None,
    ):
        self.tools = tools or []
        self.system_prompt = system_prompt or f"You are the {self.name} specialist agent for DataBridge AI."
        self.model = model
        self.temperature = temperature
        self._progress_callback = progress_callback

    # ------------------------------------------------------------------
    # Abstract interface
    # ------------------------------------------------------------------

    @abstractmethod
    async def run(self, state: WorkflowState) -> WorkflowState:
        """Execute this agent's logic on the shared state.

        Must return a (possibly updated) WorkflowState.
        """

    # ------------------------------------------------------------------
    # Graph-node wrapper (called by LangGraph)
    # ------------------------------------------------------------------

    async def __call__(self, state: WorkflowState) -> Dict[str, Any]:
        """LangGraph node entry point.

        Wraps ``run()`` with phase timing, error handling, and result recording.
        Returns a partial state update dict (LangGraph merge semantics).
        """
        phase = self.phase_name or self.name
        self._emit_progress(phase, "started")

        phase_result = PhaseResultDict(
            name=phase,
            status="running",
            started_at=datetime.utcnow().isoformat() + "Z",
        )
        t0 = time.time()

        try:
            updated = await self.run(state)
            phase_result["status"] = "completed"
            phase_result["output"] = updated.get("context", {}).get(phase, {})
            self._emit_progress(phase, "completed")
        except Exception as exc:
            logger.error("Agent %s failed: %s", self.name, exc)
            phase_result["status"] = "error"
            phase_result["error"] = str(exc)
            self._emit_progress(phase, "failed", str(exc))

            # Return error state
            errors = list(state.get("errors", []))
            errors.append({"phase": phase, "error": str(exc), "agent": self.name})
            return {
                "current_phase": phase,
                "errors": errors,
                "phase_results": {
                    **state.get("phase_results", {}),
                    phase: phase_result,
                },
            }

        phase_result["duration_seconds"] = round(time.time() - t0, 2)
        phase_result["ended_at"] = datetime.utcnow().isoformat() + "Z"

        # Build partial state update
        phase_results = dict(updated.get("phase_results", state.get("phase_results", {})))
        phase_results[phase] = phase_result

        completed = list(updated.get("completed_phases", state.get("completed_phases", [])))
        if phase not in completed:
            completed.append(phase)

        return {
            "current_phase": phase,
            "phase_results": phase_results,
            "completed_phases": completed,
            "context": updated.get("context", state.get("context", {})),
            "messages": updated.get("messages", state.get("messages", [])),
            "errors": updated.get("errors", state.get("errors", [])),
        }

    # ------------------------------------------------------------------
    # LLM helpers
    # ------------------------------------------------------------------

    async def _invoke_llm(
        self,
        prompt: str,
        tools: Optional[List] = None,
    ) -> str:
        """Call ChatAnthropic with optional tool binding.

        Falls back to returning the prompt itself when langchain-anthropic
        is not installed (deterministic mode).
        """
        if not LANGCHAIN_AVAILABLE:
            logger.debug("langchain-anthropic not available; skipping LLM call")
            return f"[deterministic] {prompt}"

        llm = ChatAnthropic(model=self.model, temperature=self.temperature)
        if tools:
            llm = llm.bind_tools(tools)

        messages = []
        if self.system_prompt:
            messages.append(SystemMessage(content=self.system_prompt))
        messages.append(HumanMessage(content=prompt))

        response = await llm.ainvoke(messages)
        return response.content if hasattr(response, "content") else str(response)

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _emit_progress(self, phase: str, status: str, detail: str = "") -> None:
        """Emit a progress event via the callback if configured."""
        if self._progress_callback:
            try:
                self._progress_callback({
                    "agent": self.name,
                    "phase": phase,
                    "status": status,
                    "detail": detail,
                    "timestamp": datetime.utcnow().isoformat() + "Z",
                })
            except Exception:
                pass  # never let progress emission break the pipeline

    def _update_context(
        self,
        state: WorkflowState,
        phase: str,
        data: Dict[str, Any],
    ) -> WorkflowState:
        """Return a copy of state with ``context[phase]`` set to data."""
        ctx = dict(state.get("context", {}))
        ctx[phase] = data
        return {**state, "context": ctx}

    def get_capabilities(self) -> Dict[str, Any]:
        """Return a description of this agent's capabilities."""
        return {
            "name": self.name,
            "description": self.description,
            "phase": self.phase_name,
            "tools": [getattr(t, "name", str(t)) for t in self.tools],
            "llm_available": LANGCHAIN_AVAILABLE,
        }
