"""Mentor Council Engine — orchestrates MARL-inspired multi-agent deliberation."""

from __future__ import annotations

import contextlib
import logging
from datetime import UTC, datetime

from vastian.agents.registry import AgentRegistry
from vastian.bus import Message, MessageBus, MessageType
from vastian.council.session import (
    ActionItem,
    CouncilPhase,
    CouncilSession,
    CouncilType,
)
from vastian.knowledge import KnowledgeStore

logger = logging.getLogger(__name__)

# Default council compositions by type
DEFAULT_COUNCILS: dict[str, list[str]] = {
    "strategic_review": ["rowan", "victor", "dominic", "orion"],
    "operational_sync": ["liam", "elias", "charles"],
    "scenario_workshop": ["theo", "concord", "jake", "henry"],
    "full_council": [
        "rowan",
        "charles",
        "victor",
        "dominic",
        "orion",
        "liam",
        "theo",
        "elias",
        "henry",
        "finley",
    ],
    "ad_hoc": ["charles", "dominic", "liam"],  # default, but usually overridden
}

# Who synthesizes each council type
DEFAULT_SYNTHESIZERS: dict[str, str] = {
    "strategic_review": "dominic",
    "operational_sync": "charles",
    "scenario_workshop": "theo",
    "full_council": "dominic",
    "ad_hoc": "charles",
}


class MentorCouncilEngine:
    """
    Orchestrates Mentor Council meetings.

    Flow:
    1. CONVENE — Select members, assemble briefing context
    2. DELIBERATE — Multi-round discussion (each agent contributes in turn)
    3. SYNTHESIZE — Designated agent compiles unified recommendation
    4. DELIVER — Results archived and action items dispatched
    """

    def __init__(
        self,
        registry: AgentRegistry,
        bus: MessageBus,
        knowledge: KnowledgeStore | None = None,
        max_rounds: int = 5,
        consensus_threshold: float = 0.75,
    ) -> None:
        self.registry = registry
        self.bus = bus
        self.knowledge = knowledge
        self.max_rounds = max_rounds
        self.consensus_threshold = consensus_threshold
        self._sessions: dict[str, CouncilSession] = {}
        # Optional progress callback: async callable(phase, agent_name, detail)
        self.progress_callback = None
        # Optional delegation callback: async callable(agent_id, description, topic)
        # When set, council action items get delegated as real background tasks
        self._delegation_callback = None

    async def _emit_progress(self, phase: str, agent_name: str = "", detail: str = "") -> None:
        """Emit progress update if a callback is registered."""
        if self.progress_callback:
            with contextlib.suppress(Exception):
                await self.progress_callback(phase, agent_name, detail)

    async def convene(
        self,
        topic: str,
        council_type: str = "ad_hoc",
        member_ids: list[str] | None = None,
        context: str = "",
        max_rounds: int | None = None,
    ) -> dict:
        """
        Convene a Mentor Council meeting.

        Returns the full council session summary including synthesis and action items.
        """
        # Resolve council type
        try:
            c_type = CouncilType(council_type)
        except ValueError:
            c_type = CouncilType.AD_HOC

        # Determine members
        if member_ids is None:
            member_ids = DEFAULT_COUNCILS.get(council_type, DEFAULT_COUNCILS["ad_hoc"])

        # Filter to agents that actually exist in the registry
        valid_members = [mid for mid in member_ids if self.registry.get_agent(mid)]
        if not valid_members:
            return {"error": "No valid council members found"}

        # Determine synthesizer
        synthesizer_id = DEFAULT_SYNTHESIZERS.get(council_type, "charles")
        if synthesizer_id not in valid_members:
            synthesizer_id = valid_members[0]

        # Set sensible round limits by council type
        if max_rounds is not None:
            effective_rounds = max_rounds
        elif c_type in (CouncilType.AD_HOC, CouncilType.OPERATIONAL_SYNC):
            effective_rounds = 2  # keep fast councils fast
        else:
            effective_rounds = self.max_rounds

        # Create session
        session = CouncilSession(
            council_type=c_type,
            topic=topic,
            context=context,
            member_ids=valid_members,
            max_rounds=effective_rounds,
            synthesizer_id=synthesizer_id,
        )
        self._sessions[session.id] = session

        logger.info(
            "Council convened [%s]: %s | Members: %s",
            c_type.value,
            topic,
            ", ".join(valid_members),
        )

        # Persist session
        if self.knowledge:
            await self.knowledge.structured.create_council_session(
                session_id=session.id,
                council_type=c_type.value,
                topic=topic,
                members=valid_members,
            )

        # ── Phase 1: CONVENE — Gather briefing context ──
        session.phase = CouncilPhase.CONVENE
        await self._emit_progress(
            "convene", "", f"Assembling briefing for {len(valid_members)} members..."
        )
        briefing_context = await self._build_briefing(topic, context)
        session.context = briefing_context

        # ── Phase 2: DELIBERATE — Multi-round discussion ──
        session.phase = CouncilPhase.DELIBERATE
        await self._emit_progress(
            "deliberate", "", f"Starting deliberation ({session.max_rounds} rounds max)..."
        )
        await self._deliberate(session)

        # ── Phase 3: SYNTHESIZE — Build unified recommendation ──
        session.phase = CouncilPhase.SYNTHESIZE
        await self._emit_progress(
            "synthesize", synthesizer_id, "Synthesizing unified recommendation..."
        )
        await self._synthesize(session)

        # ── Phase 4: DELIVER — Archive and dispatch ──
        session.phase = CouncilPhase.DELIVER
        await self._emit_progress("deliver", "", "Archiving and dispatching action items...")
        await self._deliver(session)

        session.phase = CouncilPhase.COMPLETED
        session.completed_at = datetime.now(UTC)

        logger.info("Council completed [%s]: %s", session.id, topic)

        return session.to_summary()

    async def _build_briefing(self, topic: str, additional_context: str) -> str:
        """Assemble briefing context from knowledge store."""
        parts = [f"Meeting Topic: {topic}"]

        if additional_context:
            parts.append(f"\nAdditional Context:\n{additional_context}")

        # Search knowledge store for relevant context
        if self.knowledge:
            results = await self.knowledge.search(query=topic, top_k=5)
            if results:
                parts.append("\nRelevant Knowledge Bank Entries:")
                for entry in results:
                    parts.append(f"- {entry.get('content', '')[:300]}")

        return "\n".join(parts)

    async def _deliberate(self, session: CouncilSession) -> None:
        """Run multi-round deliberation."""
        for round_num in range(1, session.max_rounds + 1):
            session.current_round = round_num
            logger.info("Council %s: Round %d", session.id, round_num)

            # Collect prior contributions for context
            prior = [{"agent": c.agent_name, "content": c.content} for c in session.contributions]

            # Each member contributes in turn
            for member_id in session.member_ids:
                agent = self.registry.get_agent(member_id)
                if agent is None:
                    continue

                await self._emit_progress(
                    "thinking",
                    agent.config.name,
                    f"Round {round_num}: {agent.config.name} is thinking...",
                )

                # Build the council message
                council_msg = Message(
                    sender="council",
                    recipient=member_id,
                    type=MessageType.COUNCIL_BRIEFING,
                    payload={
                        "topic": session.topic,
                        "context": session.context,
                        "prior_contributions": prior,
                        "round": round_num,
                        "max_rounds": session.max_rounds,
                    },
                    metadata={"session_id": session.id},
                )

                # Get agent's contribution
                response = await agent.process_message(council_msg)
                if response:
                    content = response.payload.get("content", "")
                    session.add_contribution(
                        agent_id=member_id,
                        agent_name=agent.config.name,
                        content=content,
                    )
                    # Emit the actual contribution content so the user can follow along
                    await self._emit_progress(
                        "contribution",
                        agent.config.name,
                        content,
                    )
                    # Add to prior for next agent's context
                    prior.append({"agent": agent.config.name, "content": content})

            # Check if we should continue (simple heuristic: if round 2+ and
            # contributions are getting shorter, we may have reached consensus)
            if round_num >= 2:
                round_contributions = session.get_round_contributions(round_num)
                prev_contributions = session.get_round_contributions(round_num - 1)

                if round_contributions and prev_contributions:
                    avg_current = sum(len(c.content) for c in round_contributions) / len(
                        round_contributions
                    )
                    avg_prev = sum(len(c.content) for c in prev_contributions) / len(
                        prev_contributions
                    )

                    # If contributions are shrinking significantly, consensus may be near
                    if avg_current < avg_prev * 0.5:
                        logger.info(
                            "Council %s: Early convergence detected at round %d",
                            session.id,
                            round_num,
                        )
                        break

    async def _synthesize(self, session: CouncilSession) -> None:
        """Have the designated synthesizer compile a unified recommendation."""
        synthesizer = self.registry.get_agent(session.synthesizer_id)
        if synthesizer is None:
            # Fallback: compile from contributions without LLM
            session.synthesis = self._manual_synthesis(session)
            return

        # Build synthesis prompt
        all_contributions = "\n\n".join(
            f"**{c.agent_name}** (Round {c.round_number}):\n{c.content}"
            for c in session.contributions
        )

        synthesis_msg = Message(
            sender="council",
            recipient=session.synthesizer_id,
            type=MessageType.COUNCIL_BRIEFING,
            payload={
                "topic": session.topic,
                "context": (
                    f"You are the designated synthesizer for this Mentor Council meeting.\n\n"
                    f"## Topic\n{session.topic}\n\n"
                    f"## All Contributions\n{all_contributions}\n\n"
                    f"## Your Task\n"
                    f"1. Synthesize all perspectives into a unified recommendation.\n"
                    f"2. Note any significant areas of disagreement.\n"
                    f"3. Propose specific action items with agent assignments.\n"
                    f"4. Be concise but thorough.\n"
                    f"\nFormat your response as:\n"
                    f"### Unified Recommendation\n[synthesis]\n\n"
                    f"### Key Disagreements\n[if any]\n\n"
                    f"### Action Items\n- [ACTION] assigned to [AGENT_ID]: [description]"
                ),
                "prior_contributions": [],
            },
            metadata={"session_id": session.id, "phase": "synthesis"},
        )

        response = await synthesizer.process_message(synthesis_msg)
        if response:
            session.synthesis = response.payload.get("content", "")
            # Emit the synthesis so user can see it in real-time
            await self._emit_progress(
                "synthesis_result", synthesizer.config.name, session.synthesis
            )
            # Try to extract action items from the synthesis
            self._extract_action_items(session)

    def _manual_synthesis(self, session: CouncilSession) -> str:
        """Fallback synthesis without LLM — just compiles contributions."""
        lines = [f"## Council Summary: {session.topic}\n"]
        for member_id in session.member_ids:
            contribs = session.get_agent_contributions(member_id)
            if contribs:
                lines.append(f"### {contribs[0].agent_name}")
                lines.append(contribs[-1].content)  # Latest contribution
                lines.append("")
        return "\n".join(lines)

    def _extract_action_items(self, session: CouncilSession) -> None:
        """Extract action items from the synthesis text.

        Handles various markdown formats the LLM might use:
          - [ACTION] assigned to [ELIAS]: description
          - **[ACTION] assigned to [ELIAS]:** description
          - [ACTION] assigned to ELIAS: description
          - ACTION assigned to elias: description
        """
        import re

        if not session.synthesis:
            return

        for line in session.synthesis.split("\n"):
            # Strip markdown bold, list markers, leading whitespace
            clean = line.strip()
            clean = re.sub(r"\*\*", "", clean)  # remove **bold**
            clean = re.sub(r"^\s*[-*]\s*", "", clean)  # remove list marker
            clean = clean.strip()

            # Check for action item pattern (case-insensitive)
            if not re.search(r"\[?ACTION\]?", clean, re.IGNORECASE):
                continue

            # Extract the agent ID — look for [AGENT] or just agent name
            assigned_to = "charles"  # default
            for aid in self.registry.agent_ids:
                # Match [AGENT_ID] or just agent_id in the line
                if re.search(
                    rf"\[{re.escape(aid)}\]|(?<!\w){re.escape(aid)}(?!\w)", clean, re.IGNORECASE
                ):
                    assigned_to = aid
                    break

            # Extract description — everything after the last colon
            parts = clean.split(":", 1)
            if len(parts) == 2:
                description = parts[1].strip()
            else:
                # Fallback: use everything after "assigned to [AGENT]"
                match = re.search(r"assigned to \[?\w+\]?\s*[:\-]?\s*(.*)", clean, re.IGNORECASE)
                description = match.group(1).strip() if match else clean

            if description:
                session.action_items.append(
                    ActionItem(
                        description=description,
                        assigned_to=assigned_to,
                    )
                )

    async def _deliver(self, session: CouncilSession) -> None:
        """Archive results and dispatch action items."""
        # Archive the full transcript
        if self.knowledge:
            # Store transcript in knowledge bank
            await self.knowledge.store_entry(
                content=session.transcript,
                author="council",
                tags=["council", session.council_type.value, "transcript"],
                metadata={"session_id": session.id, "topic": session.topic},
            )

            # Update the council session in structured store
            await self.knowledge.structured.update_council_session(
                session_id=session.id,
                transcript=[c.model_dump(mode="json") for c in session.contributions],
                synthesis=session.synthesis,
                action_items=[a.model_dump(mode="json") for a in session.action_items],
                status="completed",
            )

        # Dispatch action items to assigned agents via bus
        for item in session.action_items:
            action_msg = Message(
                sender="council",
                recipient=item.assigned_to,
                type=MessageType.COUNCIL_ACTION,
                payload={
                    "content": item.description,
                    "council_session": session.id,
                    "priority": item.priority,
                    "topic": session.topic,
                },
            )
            await self.bus.send(action_msg)

        # Auto-delegate action items as real background tasks
        if self._delegation_callback and session.action_items:
            for item in session.action_items:
                try:
                    await self._delegation_callback(
                        item.assigned_to,
                        item.description,
                        session.topic,
                    )
                except Exception:
                    logger.debug("Council delegation failed for %s", item.assigned_to)

        logger.info(
            "Council %s delivered: %d action items dispatched",
            session.id,
            len(session.action_items),
        )

    # ── Session Access ───────────────────────────────

    def get_session(self, session_id: str) -> CouncilSession | None:
        return self._sessions.get(session_id)

    def list_sessions(self) -> list[dict]:
        return [s.to_summary() for s in self._sessions.values()]
