"""Base agent class — the building block of every Vastian Network agent."""

from __future__ import annotations

import asyncio
import logging
from enum import StrEnum

from pydantic import BaseModel, Field

from vastian.bus.message import Message, MessageType

logger = logging.getLogger(__name__)


# ── Configuration Model ──────────────────────────────────


class AgentConfig(BaseModel):
    """Declarative agent configuration — loaded from YAML."""

    model_config = {"protected_namespaces": ()}

    agent_id: str
    name: str
    title: str
    sigil: str = ""  # emoji identifier
    tone: str = "professional and clear"
    role_description: str
    responsibilities: list[str] = Field(default_factory=list)
    owned_documents: list[str] = Field(default_factory=list)
    reports_to: str | None = None
    direct_reports: list[str] = Field(default_factory=list)
    teams: list[str] = Field(default_factory=list)
    slot: str | None = None  # prompt_slot for baselines/drift (e.g. chief_of_staff, finance)
    model_override: str | None = None  # use a different model than default
    llm_provider_override: str | None = None  # use a different LLM provider (e.g. "anthropic")
    temperature_override: float | None = (
        None  # override default LLM temperature (e.g. 0.9 for creative)
    )
    tools: list[str] = Field(default_factory=list)  # tool names this agent can use
    system_prompt_extra: str = ""  # appended to the base system prompt
    interview_questions: list[dict] = Field(
        default_factory=list,
        description="Structured questions for Interview Me mode, each with 'question' and 'options'",
    )

    @property
    def display_name(self) -> str:
        prefix = f"{self.sigil} " if self.sigil else ""
        return f"{prefix}{self.name} ({self.title})"


class AgentState(StrEnum):
    IDLE = "idle"
    PROCESSING = "processing"
    IN_COUNCIL = "in_council"
    WAITING = "waiting"
    ERROR = "error"


# ── Base Agent ───────────────────────────────────────────


class BaseAgent:
    """
    Core agent runtime. Each agent:
    - Has identity and configuration
    - Owns a mailbox on the message bus
    - Maintains short-term memory (conversation context)
    - Can invoke tools
    - Can send/receive messages
    - Can participate in Mentor Council meetings
    """

    def __init__(
        self,
        config: AgentConfig,
        bus: MessageBus,  # noqa: F821 — forward ref
        knowledge_store: KnowledgeStore | None = None,  # noqa: F821
        llm_provider: BaseLLMProvider | None = None,  # noqa: F821
    ) -> None:
        self.config = config
        self.bus = bus
        self.knowledge_store = knowledge_store
        self.llm = llm_provider

        # State
        self.state = AgentState.IDLE
        self.mailbox: asyncio.Queue[Message] = bus.register_agent(config.agent_id)

        # Network context — injected by orchestrator after all agents are loaded
        self._network_roster: str = ""
        self._document_states: dict[str, str] = {}  # doc_name -> status/summary
        self._all_documents: list[str] = []  # all docs discovered on disk (excl. archive/)
        self._briefings: str = ""  # inlined content from briefs/ subfolder (auto-injected)
        self._recent_activity: str = ""  # cross-machine continuity context
        self._user_name: str = "User"  # configurable via VASTIAN_USER_NAME
        self._shared_plans: str = ""  # approved plans from docs/plans/ injected into all agents

        # Short-term conversation memory (most recent messages in context window)
        self.conversation_history: list[dict[str, str]] = []
        self.max_conversation_history = 50

        logger.info("Agent initialized: %s", config.display_name)

    @property
    def agent_id(self) -> str:
        return self.config.agent_id

    @property
    def name(self) -> str:
        return self.config.name

    # ── System Prompt Assembly ───────────────────────

    def build_system_prompt(self) -> str:
        """Assemble the full system prompt from config + templates + live network context."""
        from datetime import datetime

        today = datetime.now().strftime("%A, %B %d, %Y")
        sections = [
            f"You are {self.config.name}, {self.config.title} in the Vastian Network.",
            f"\n## TODAY'S DATE\n{today}",
            "\n## WHO IS THE USER\n"
            f"The person you are chatting with is **{self._user_name}**, the human founder and creator of "
            f"the Vastian Network. When the user says 'I' or 'me', they mean {self._user_name} — NOT you. "
            f"You are an agent serving {self._user_name}. Never confuse yourself with the user.",
            f"\n## Role\n{self.config.role_description}",
            f"\n## Tone\n{self.config.tone}",
        ]

        if self.config.responsibilities:
            items = "\n".join(f"- {r}" for r in self.config.responsibilities)
            sections.append(f"\n## Responsibilities\n{items}")

        if self.config.owned_documents:
            docs = "\n".join(f"- {d}" for d in self.config.owned_documents)
            sections.append(f"\n## Core Documents (your primary responsibilities)\n{docs}")

        # Show ALL documents in this agent's folder (auto-discovered from disk,
        # excludes archive/ subfolder). This includes the core docs above plus
        # any project-specific documents created during conversations.
        if self._all_documents:
            statuses = []
            for doc_name in self._all_documents:
                status = self._document_states.get(doc_name, "unknown")
                statuses.append(f"- `{self.agent_id}/{doc_name}`: {status}")
            sections.append(
                "\n## All Documents in Your Folder (read with `read_document`)\n"
                "Use path format: `{your_agent_id}/{filename}` when calling read_document.\n"
                + "\n".join(statuses)
            )
        elif self._document_states:
            # Fallback if all_documents wasn't populated
            statuses = []
            for doc_name, status in self._document_states.items():
                statuses.append(f"- `{self.agent_id}/{doc_name}`: {status}")
            sections.append("\n## Document Status (Live)\n" + "\n".join(statuses))

        if self.config.reports_to:
            sections.append(f"\n## Reporting\nYou report to: {self.config.reports_to}")

        if self.config.direct_reports:
            reports = ", ".join(self.config.direct_reports)
            sections.append(f"Your direct reports: {reports}")

        # Inject the real network roster
        if self._network_roster:
            sections.append(
                f"\n## Vastian Network Roster (REAL — Do Not Invent Others)\n{self._network_roster}"
            )

        # Inject shared approved plans that all agents must be aware of
        if self._shared_plans:
            sections.append(
                f"\n## Approved Plans & Announcements (READ CAREFULLY)\n{self._shared_plans}"
            )

        # Inject inlined briefing content from briefs/ subfolder — these are
        # session outcomes, council meeting results, and announcements that the
        # agent MUST know about without needing to call read_document.
        if self._briefings:
            sections.append(
                f"\n## Session Briefings (IMPORTANT — You attended these sessions)\n"
                f"The following are summaries of sessions you participated in. "
                f"You MUST remember this information.\n\n{self._briefings}"
            )

        # List the agent's available tools
        if self.config.tools:
            tool_list = "\n".join(f"- `{t}`" for t in self.config.tools)
            sections.append(f"\n## Your Available Tools\n{tool_list}")

        if self.config.system_prompt_extra:
            sections.append(f"\n## Additional Context\n{self.config.system_prompt_extra}")

        # Cross-machine continuity: recent activity from prior sessions
        if self._recent_activity:
            sections.append(f"\n{self._recent_activity}")

        sections.append(
            "\n## CRITICAL RULES — NEVER VIOLATE\n"
            "1. NEVER fabricate, hallucinate, or invent information. If you don't know "
            "something, say 'I don't have that information yet' or 'This document hasn't "
            "been populated yet.'\n"
            "2. ONLY reference agents listed in the Network Roster above. Do NOT invent "
            "agent names that don't exist.\n"
            "3. ONLY reference documents listed in the Network Roster. Every agent's owned "
            "documents are listed there. If a document hasn't been populated yet, say so.\n"
            "4. If asked for metrics, status, or data that hasn't been tracked yet, say "
            "'This hasn't been set up yet — would you like me to help establish this?'\n"
            f"5. The Vastian Network is NEW. Most documents are empty placeholders. Be "
            f"honest about the current state and help {self._user_name} build it up from here.\n"
            f"6. The user is ALWAYS {self._user_name}. Never confuse yourself with the user.\n"
            "7. Keep responses focused on your domain of responsibility.\n"
            "8. When collaborating with peers, be concise and structured.\n"
            "\n## DELEGATION & TOOLS\n"
            "Use your tools actively — they are how you get real work done:\n"
            "- `delegate_to_agent` — Use when a request involves another agent's domain, "
            f"you need specialist input, or {self._user_name} asks you to coordinate with someone.\n"
            "- `read_document` — Use to read real data from managed documents before answering.\n"
            "- Any SPECIAL tools listed above — these are unique to your role. Use them proactively.\n\n"
            "Do NOT try to answer on behalf of other agents. Delegate to them and "
            "incorporate their actual response. The roster above shows who owns what.\n\n"
            "## MANDATORY: WRITE YOUR WORK TO DOCUMENTS\n"
            "When you produce content that belongs in your owned documents — templates, "
            "plans, trackers, frameworks, logs, analyses — you MUST use `write_document` "
            "to save it. This is non-negotiable. Your documents are your persistent memory "
            "and the source of truth for your domain. If you create something valuable, "
            "write it down. If you are delegated a task that asks for templates, content, "
            "or document updates, call `write_document` before finishing your response. "
            "Never just describe what you would write — actually write it."
        )

        return "\n".join(sections)

    def set_network_context(
        self,
        roster: str,
        document_states: dict[str, str] | None = None,
        all_documents: list[str] | None = None,
        recent_activity: str = "",
        user_name: str = "User",
        shared_plans: str = "",
        briefings: str = "",
    ) -> None:
        """Inject live network context (called by the orchestrator after initialization)."""
        self._network_roster = roster
        if document_states:
            self._document_states = document_states
        if all_documents is not None:
            self._all_documents = all_documents
        self._recent_activity = recent_activity
        self._user_name = user_name
        self._shared_plans = shared_plans
        self._briefings = briefings

    # ── Message Handling ─────────────────────────────

    async def process_message(self, message: Message) -> Message | None:
        """
        Process an incoming message and optionally return a response.

        Override in subclasses for specialized behavior.
        """
        self.state = AgentState.PROCESSING
        logger.info("[%s] Processing %s from %s", self.agent_id, message.type.value, message.sender)

        try:
            if message.type == MessageType.ESCALATION:
                return await self._handle_escalation(message)
            elif message.type == MessageType.QUERY:
                return await self._handle_query(message)
            elif message.type == MessageType.TASK:
                return await self._handle_task(message)
            elif message.type in (
                MessageType.COUNCIL_BRIEFING,
                MessageType.COUNCIL_CONTRIBUTION,
            ):
                return await self._handle_council_message(message)
            else:
                return await self._handle_generic(message)
        except Exception:
            logger.exception("[%s] Error processing message %s", self.agent_id, message.id)
            self.state = AgentState.ERROR
            return message.reply(
                sender=self.agent_id,
                payload={"error": "Internal processing error"},
                msg_type=MessageType.RESPONSE,
            )
        finally:
            if self.state != AgentState.ERROR:
                self.state = AgentState.IDLE

    async def _handle_query(self, message: Message) -> Message | None:
        """Handle a query by generating an LLM response."""
        query = message.payload.get("content", "")
        response_text = await self._generate(query)
        return message.reply(
            sender=self.agent_id,
            payload={"content": response_text},
        )

    async def _handle_task(self, message: Message) -> Message | None:
        """Handle a task assignment."""
        task_description = message.payload.get("content", "")
        response_text = await self._generate(
            f"You have been assigned the following task:\n\n{task_description}\n\n"
            "Provide your plan and any immediate output."
        )
        return message.reply(
            sender=self.agent_id,
            payload={"content": response_text, "status": "in_progress"},
        )

    async def _handle_escalation(self, message: Message) -> Message | None:
        """Handle an escalation from a subordinate."""
        context = message.payload.get("content", "")
        reason = message.payload.get("reason", "unspecified")
        response_text = await self._generate(
            f"An escalation has been received from {message.sender}.\n"
            f"Reason: {reason}\n\nContext:\n{context}\n\n"
            "Provide guidance or take ownership of this issue."
        )
        return message.reply(
            sender=self.agent_id,
            payload={"content": response_text, "escalation_handled": True},
        )

    async def _handle_council_message(self, message: Message) -> Message | None:
        """Handle Mentor Council meeting messages."""
        self.state = AgentState.IN_COUNCIL
        topic = message.payload.get("topic", "")
        context = message.payload.get("context", "")
        prior_contributions = message.payload.get("prior_contributions", [])

        prompt_parts = [
            f"You are in a Mentor Council meeting.\n\nTopic: {topic}",
        ]
        if context:
            prompt_parts.append(f"\nBriefing Context:\n{context}")
        if prior_contributions:
            contrib_text = "\n\n".join(
                f"**{c['agent']}**: {c['content']}" for c in prior_contributions
            )
            prompt_parts.append(f"\nPrior Contributions:\n{contrib_text}")

        prompt_parts.append(
            "\nProvide your perspective based on your role and expertise. "
            "Be specific, cite relevant documents you own, and note any "
            "disagreements with prior contributions."
        )

        response_text = await self._generate("\n".join(prompt_parts))

        return message.reply(
            sender=self.agent_id,
            payload={"content": response_text, "agent": self.config.name},
            msg_type=MessageType.COUNCIL_CONTRIBUTION,
        )

    async def _handle_generic(self, message: Message) -> Message | None:
        """Fallback handler."""
        content = message.payload.get("content", str(message.payload))
        response_text = await self._generate(content)
        return message.reply(
            sender=self.agent_id,
            payload={"content": response_text},
        )

    # ── LLM Generation ───────────────────────────────

    async def _generate(self, user_message: str) -> str:
        """Generate an LLM response using this agent's persona."""
        self.conversation_history.append({"role": "user", "content": user_message})

        # Trim conversation history
        if len(self.conversation_history) > self.max_conversation_history:
            self.conversation_history = self.conversation_history[-self.max_conversation_history :]

        if self.llm is None:
            # Stub response when no LLM is connected (for testing)
            response = f"[{self.config.name}] Acknowledged: {user_message[:100]}..."
        else:
            response = await self.llm.generate(
                system_prompt=self.build_system_prompt(),
                messages=self.conversation_history,
                model=self.config.model_override,
            )

        self.conversation_history.append({"role": "assistant", "content": response})
        return response

    # ── Outbound Messaging ───────────────────────────

    async def send_message(
        self,
        recipient: str,
        content: str,
        msg_type: MessageType = MessageType.QUERY,
        **extra_payload: object,
    ) -> None:
        """Send a message to another agent or team."""
        message = Message(
            sender=self.agent_id,
            recipient=recipient,
            type=msg_type,
            payload={"content": content, **extra_payload},
        )
        await self.bus.send(message)

    async def escalate(self, content: str, reason: str = "requires higher authority") -> None:
        """Escalate an issue to the reporting superior."""
        if not self.config.reports_to:
            logger.warning("[%s] No superior to escalate to!", self.agent_id)
            return
        await self.send_message(
            recipient=self.config.reports_to,
            content=content,
            msg_type=MessageType.ESCALATION,
            reason=reason,
        )

    # ── Mailbox Processing Loop ──────────────────────

    async def run(self) -> None:
        """Main agent loop — continuously process incoming messages."""
        logger.info("[%s] Agent loop started", self.agent_id)
        while True:
            message = await self.mailbox.get()
            response = await self.process_message(message)
            if response:
                await self.bus.send(response)

    def __repr__(self) -> str:
        return f"<Agent {self.config.display_name} [{self.state.value}]>"
