"""PM Agent router - routes discussion between participant Agents."""

from __future__ import annotations

import json
import re
from typing import TYPE_CHECKING

from pydantic import ValidationError

from src.agent import format_scratchpad_summary
from src.models import AgentConfig, MeetingState, PMDecision

if TYPE_CHECKING:
    from src.context_manager import ContextManager
    from src.llm_client import LLMClient

_MAX_RETRIES = 5


def build_pm_system_prompt(available_agents: dict[str, AgentConfig]) -> str:
    """Build the PM Agent's system prompt with available agents listed.

    Args:
        available_agents: Mapping of agent name to AgentConfig.

    Returns:
        A fully-formed system prompt string for the PM LLM call.
    """
    agent_list = "\n".join(
        f"- {name}: {config.role}" for name, config in available_agents.items()
    )

    return f"""You are a meeting facilitator (PM). Your job is to:
1. Analyze the current whiteboard state
2. Decide which Agent should speak next, or if the meeting should end
3. Output ONLY valid JSON matching the schema below

Available Agents:
{agent_list}

You MUST output ONLY a JSON object with these fields:
- "analysis": brief analysis of current whiteboard state
- "next_action": either "CALL_AGENT" or "FINISH"
- "target_agent": name of the agent to call (required if CALL_AGENT)
- "prompt_for_agent": specific question for that agent (required if CALL_AGENT)
- "final_report": complete Markdown meeting report (required if FINISH)

Rules:
- DO NOT wrap up prematurely. Explore the topic deeply before concluding.
- Ask follow-up questions when an agent gives a surface-level answer.
- Call the same agent multiple times if their area needs deeper exploration.
- Challenge assumptions: ask agents to justify their reasoning with specifics.
- Cross-pollinate: share one agent's perspective with another for counterpoints.
- Ensure at least 2-3 rounds of back-and-forth before considering FINISH.
- Call each agent at least once before finishing.
- When finishing, produce a comprehensive report that reflects the depth of discussion.
- The target_agent MUST be one of the available agents listed above.
- Output ONLY the JSON object, no other text.
- If you see entries from [HUMAN], treat them as high-priority stakeholder feedback.
  Address their concerns by calling relevant agents to respond."""


def run_pm(
    state: MeetingState,
    available_agents: dict[str, AgentConfig],
    client: LLMClient,
    context_manager: ContextManager | None = None,
) -> PMDecision:
    """Run PM Agent to get the next routing decision.

    Calls the LLM with the current whiteboard state. Retries up to
    _MAX_RETRIES times when the response is invalid JSON, fails Pydantic
    validation, or references a non-existent agent. Each retry appends an
    error-feedback message so the LLM can self-correct.

    Args:
        state: Current MeetingState containing topic, scratchpad, and round.
        available_agents: Mapping of agent name to AgentConfig.
        client: Any object satisfying the LLMClient Protocol.
        context_manager: Optional ContextManager for whiteboard compression.

    Returns:
        A validated PMDecision instance.

    Raises:
        RuntimeError: When all retry attempts are exhausted without a valid decision.
    """
    system_prompt = build_pm_system_prompt(available_agents)

    if context_manager is not None:
        user_content = context_manager.build_pm_context(state, client)
    else:
        scratchpad_text = format_scratchpad_summary(state.scratchpad)
        user_content = f"## Meeting Topic\n\n{state.topic}\n\n"
        if state.context:
            user_content += f"## Background Context\n\n{state.context}\n\n"
        user_content += f"## Current Whiteboard (Round {state.current_round})\n\n{scratchpad_text}"

    messages: list[dict[str, str]] = [{"role": "user", "content": user_content}]
    last_error = ""

    for attempt in range(_MAX_RETRIES):
        if last_error:
            messages.append({
                "role": "user",
                "content": f"Your previous response was invalid: {last_error}. Please output ONLY valid JSON.",
            })

        raw_response = client.chat(system=system_prompt, messages=messages)

        try:
            # Strip potential markdown code fences
            cleaned = raw_response.strip()
            if cleaned.startswith("```"):
                cleaned = cleaned.split("\n", 1)[1] if "\n" in cleaned else cleaned
                if cleaned.endswith("```"):
                    cleaned = cleaned[:-3]
                cleaned = cleaned.strip()

            # Try to extract JSON object if there's extra text
            if not cleaned.startswith("{"):
                match = re.search(r"\{.*\}", cleaned, re.DOTALL)
                if match:
                    cleaned = match.group()

            # Handle extra data after JSON by finding the matching closing brace
            if cleaned.startswith("{"):
                depth = 0
                in_string = False
                escape_next = False
                for i, ch in enumerate(cleaned):
                    if escape_next:
                        escape_next = False
                        continue
                    if ch == "\\" and in_string:
                        escape_next = True
                        continue
                    if ch == '"':
                        in_string = not in_string
                        continue
                    if in_string:
                        continue
                    if ch == "{":
                        depth += 1
                    elif ch == "}":
                        depth -= 1
                        if depth == 0:
                            cleaned = cleaned[: i + 1]
                            break

            data = json.loads(cleaned)
            decision = PMDecision(**data)

            # Validate target_agent exists in available_agents
            if (
                decision.next_action == "CALL_AGENT"
                and decision.target_agent not in available_agents
            ):
                last_error = (
                    f"target_agent '{decision.target_agent}' is not available. "
                    f"Choose from: {', '.join(available_agents.keys())}"
                )
                continue

            return decision

        except (json.JSONDecodeError, ValidationError) as exc:
            last_error = str(exc)
            continue

    raise RuntimeError(
        f"Failed to get valid PM decision after {_MAX_RETRIES} attempts. "
        f"Last error: {last_error}"
    )
