"""
Agent v3.0 — ReAct reasoning, memory, planning, guardrails.
"""

from __future__ import annotations

import logging
import uuid
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING

from mangaba.core.types import AgentState, AgentStatus, LLMConfig, MemoryConfig
from mangaba.core.exceptions import AgentError
from mangaba.core.events import EventBus, Event, EventType
from mangaba.core.reasoning import ReActEngine

if TYPE_CHECKING:
    from mangaba.tools.base import BaseTool
    from mangaba.core.guardrails import BaseGuardrail
    from mangaba.core.output_parsers import BaseOutputParser
    from mangaba.memory.base import BaseMemory

log = logging.getLogger(__name__)


class Agent:
    """Intelligent agent with ReAct reasoning, memory, and tool use.

    Example::

        agent = Agent(
            role="Senior Data Analyst",
            goal="Analyze market trends and provide insights",
            backstory="Expert in financial markets with 15 years of experience",
            tools=[SearchTool(), CalculatorTool()],
            verbose=True,
        )
        result = agent.execute_task("Analyze Q4 revenue trends")
    """

    def __init__(
        self,
        role: str,
        goal: str,
        backstory: str,
        tools: Optional[List[BaseTool]] = None,
        llm: Optional[Any] = None,  # LLMClient or provider string
        llm_config: Optional[LLMConfig] = None,
        api_key: Optional[str] = None,
        verbose: bool = False,
        memory: Optional[BaseMemory] = None,
        memory_config: Optional[MemoryConfig] = None,
        max_iterations: int = 15,
        max_retry_on_error: int = 3,
        allow_delegation: bool = True,
        step_callback: Optional[Callable] = None,
        guardrails: Optional[List[BaseGuardrail]] = None,
        output_parser: Optional[BaseOutputParser] = None,
        agent_id: Optional[str] = None,
    ) -> None:
        if not role or not role.strip():
            raise ValueError("Role cannot be empty")
        if not goal or not goal.strip():
            raise ValueError("Goal cannot be empty")
        if not backstory or not backstory.strip():
            raise ValueError("Backstory cannot be empty")

        self.role = role.strip()
        self.goal = goal.strip()
        self.backstory = backstory.strip()
        self.tools: List[BaseTool] = list(tools or [])
        self.verbose = verbose
        self.max_iterations = max_iterations
        self.max_retry_on_error = max_retry_on_error
        self.allow_delegation = allow_delegation
        self.step_callback = step_callback
        self.guardrails = guardrails or []
        self.output_parser = output_parser
        self.memory = memory
        self.memory_config = memory_config or MemoryConfig()

        self.agent_id = agent_id or f"agent_{role.lower().replace(' ', '_')}_{uuid.uuid4().hex[:8]}"

        # ── LLM setup ────────────────────────────────────────────────
        if llm is not None and not isinstance(llm, str):
            # Already an LLMClient instance
            self.llm = llm
        else:
            self.llm = self._create_llm(llm, llm_config, api_key)

        # ── Memory (auto-create short-term if nothing provided) ──────
        if self.memory is None and self.memory_config.short_term:
            from mangaba.memory.short_term import ShortTermMemory
            self.memory = ShortTermMemory(max_items=self.memory_config.max_short_term_items)

        # ── ReAct engine ─────────────────────────────────────────────
        self._react = ReActEngine(
            llm=self.llm,
            tools=self.tools,
            max_iterations=self.max_iterations,
            verbose=self.verbose,
        )

        # ── State ────────────────────────────────────────────────────
        self.state = AgentState(agent_id=self.agent_id)

        # ── Connected agents (for delegation) ────────────────────────
        self._peers: Dict[str, Agent] = {}

        if self.verbose:
            log.info("Agent initialized — role=%s tools=%d", self.role, len(self.tools))

    # ── public API ─────────────────────────────────────────────────────

    def execute_task(self, task_description: str, context: Optional[str] = None) -> str:
        """Execute a task using the ReAct loop with tool/function calling."""
        EventBus.emit(Event(
            event_type=EventType.AGENT_START,
            source_id=self.agent_id,
            data={"role": self.role, "task": task_description[:200]},
        ))

        system_prompt = self._build_system_prompt()
        user_prompt = self._build_user_prompt(task_description, context)

        # Inject relevant memories
        memory_context = self._get_memory_context(task_description)

        last_error: Optional[Exception] = None
        for attempt in range(1, self.max_retry_on_error + 1):
            try:
                response = self._react.run(
                    system_prompt=system_prompt,
                    user_prompt=user_prompt,
                    context=memory_context or None,
                    state=self.state,
                )

                result_text = response.text

                # Guardrails
                result_text = self._apply_guardrails(result_text)

                # Output parser
                if self.output_parser:
                    result_text = self.output_parser.parse(result_text)

                # Store in memory
                if self.memory:
                    self.memory.add(
                        f"Task: {task_description}\nResult: {result_text[:500]}",
                        metadata={"agent": self.role, "type": "task_result"},
                    )

                EventBus.emit(Event(
                    event_type=EventType.AGENT_END,
                    source_id=self.agent_id,
                    data={"result_preview": str(result_text)[:200]},
                ))
                return str(result_text)

            except Exception as exc:
                last_error = exc
                if attempt < self.max_retry_on_error:
                    log.warning("Agent retry %d/%d: %s", attempt, self.max_retry_on_error, exc)
                    continue
                break

        EventBus.emit(Event(
            event_type=EventType.AGENT_ERROR,
            source_id=self.agent_id,
            data={"error": str(last_error)},
        ))
        raise AgentError(f"Task failed after {self.max_retry_on_error} attempts: {last_error}", cause=last_error)

    def connect_to(self, other: Agent) -> None:
        """Register another agent as a peer for delegation."""
        self._peers[other.agent_id] = other
        other._peers[self.agent_id] = self

    def delegate(self, peer_id: str, task_description: str, context: Optional[str] = None) -> str:
        """Delegate a task to a connected peer agent."""
        peer = self._peers.get(peer_id)
        if peer is None:
            raise AgentError(f"No peer agent with id '{peer_id}'")
        return peer.execute_task(task_description, context)

    # ── prompt building ────────────────────────────────────────────────

    def _build_system_prompt(self) -> str:
        parts = [
            f"You are: {self.role}",
            f"Your goal is: {self.goal}",
            f"Background: {self.backstory}",
        ]
        if self.allow_delegation and self._peers:
            peers_desc = ", ".join(f"{p.role}" for p in self._peers.values())
            parts.append(f"\nYou can delegate to these agents: {peers_desc}")
        return "\n\n".join(parts)

    def _build_user_prompt(self, task_description: str, context: Optional[str]) -> str:
        parts = []
        if context:
            parts.append(f"Context:\n{context}")
        parts.append(f"Task:\n{task_description}")
        parts.append("Complete this task according to your role and goal.")
        return "\n\n".join(parts)

    def _get_memory_context(self, query: str) -> str:
        if self.memory is None:
            return ""
        return self.memory.get_relevant(query, max_results=5)

    def _apply_guardrails(self, text: str) -> str:
        for g in self.guardrails:
            text = g.validate(text)
        return text

    # ── LLM factory ────────────────────────────────────────────────────

    @staticmethod
    def _create_llm(provider_str: Optional[str], llm_config: Optional[LLMConfig], api_key: Optional[str]) -> Any:
        """Build an LLMClient from config or fall back to global config."""
        from mangaba.core.llm import create_llm_client
        try:
            from config import config as global_cfg
        except ImportError:
            global_cfg = None  # type: ignore[assignment]

        cfg = llm_config or LLMConfig()

        prov = provider_str or cfg.provider
        if global_cfg:
            prov = prov or getattr(global_cfg, "provider", "google")
        key = api_key or cfg.api_key
        if not key and global_cfg:
            key = getattr(global_cfg, "api_key", None)
        model = cfg.model
        if not model and global_cfg:
            model = getattr(global_cfg, "model", None)
        if not model:
            model = "gemini-2.5-flash"

        return create_llm_client(
            provider=prov,
            api_key=key or "",
            model=model,
            temperature=cfg.temperature,
            max_output_tokens=cfg.max_tokens,
        )

    def __repr__(self) -> str:
        return f"Agent(role='{self.role}', tools={len(self.tools)}, id='{self.agent_id}')"
