"""Session Engine — bidirectional dialogue between external agents and the network.

Presentation (Cursor -> Network):
  Cursor presents a proposal, all agents react, back-and-forth follows.

Request (Network -> Cursor):
  A Vastian agent identifies something they need from Cursor (code changes,
  architecture decisions, tool implementations), creates a request. Cursor
  sees pending requests and responds. Other agents can weigh in.
"""

from __future__ import annotations

import asyncio
import contextlib
import logging
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

from vastian.bridges.cursor_bridge.sessions.models import (
    SessionDirection,
    SessionMessage,
    SessionPhase,
    VastianSession,
)
from vastian.storage.file_store import FileStore

if TYPE_CHECKING:
    from vastian.agents.base import BaseAgent
    from vastian.agents.registry import AgentRegistry
    from vastian.bus import MessageBus
    from vastian.knowledge import KnowledgeStore

logger = logging.getLogger(__name__)


class SessionEngine:
    """
    Orchestrates bidirectional sessions between Cursor and the Vastian Network.

    Presentation flow (Cursor -> Network):
      START -> SAY / ASK / DISCUSS -> WRAP

    Request flow (Network -> Cursor):
      CREATE_REQUEST -> RESPOND -> DISCUSS -> RESOLVE

    Wave 6: Type-aware sessions loaded from config/sessions.yaml.
    """

    def __init__(
        self,
        registry: AgentRegistry,
        bus: MessageBus,
        knowledge: KnowledgeStore | None = None,
        state_dir: Path | None = None,
    ) -> None:
        self.registry = registry
        self.bus = bus
        self.knowledge = knowledge

        # Persistent store for session records
        sd = state_dir or Path("./state")
        self._store = FileStore(sd / "sessions.json", id_field="id")

        # In-memory cache of active sessions
        self._sessions: dict[str, VastianSession] = {}

        # Optional progress callback: async callable(event, agent_name, content)
        self.progress_callback: Any = None

        # Wave 6: Session type config (loaded lazily)
        self._session_config: dict | None = None

    # ── Wave 6: Session type config ──────────────────────

    def _load_session_config(self) -> dict:
        """Load and cache config/sessions.yaml."""
        if self._session_config is not None:
            return self._session_config
        try:
            import os

            import yaml

            from vastian.access_tiers import _find_project_root
            from vastian.storage.data_root import get_config_dir

            root = os.environ.get("VASTIAN_PROJECT_ROOT")
            proj = Path(root).resolve() if root else _find_project_root()
            config_path = get_config_dir(proj) / "sessions.yaml"
            if config_path.exists():
                raw = config_path.read_text(encoding="utf-8")
                self._session_config = yaml.safe_load(raw) or {}
            else:
                self._session_config = {}
        except Exception as e:
            logger.warning("Could not load sessions.yaml: %s", e)
            self._session_config = {}
        return self._session_config

    def get_session_types(self, user_tier: str = "open") -> list[dict]:
        """Return ALL session type configs, marking which are locked at the user's tier.

        Each dict includes: type_id, label, description, lead, default_members,
        selection_strategy, tier_required, max_rounds, icon, locked.
        """
        from vastian.access_tiers import VALID_TIERS

        cfg = self._load_session_config()
        types = cfg.get("session_types", {})

        # Build tier ordering for comparison
        tier_order = {t: i for i, t in enumerate(VALID_TIERS)}
        user_rank = tier_order.get(user_tier, 0)

        result = []
        for type_id, tc in types.items():
            required_rank = tier_order.get(tc.get("tier_required", "open"), 0)
            locked = user_rank < required_rank
            result.append(
                {
                    "type_id": type_id,
                    "label": tc.get("label", type_id),
                    "description": tc.get("description", ""),
                    "lead": tc.get("lead", "charles"),
                    "default_members": tc.get("default_members", []),
                    "selection_strategy": tc.get("selection_strategy", "curated"),
                    "tier_required": tc.get("tier_required", "open"),
                    "max_rounds": tc.get("max_rounds", 4),
                    "icon": tc.get("icon", ""),
                    "locked": locked,
                }
            )
        return result

    def get_session_type_config(self, session_type: str) -> dict | None:
        """Return the config dict for a specific session type."""
        cfg = self._load_session_config()
        types = cfg.get("session_types", {})
        tc = types.get(session_type)
        if not tc:
            return None
        return {
            "type_id": session_type,
            "label": tc.get("label", session_type),
            "description": tc.get("description", ""),
            "lead": tc.get("lead", "charles"),
            "default_members": tc.get("default_members", []),
            "selection_strategy": tc.get("selection_strategy", "curated"),
            "tier_required": tc.get("tier_required", "open"),
            "max_rounds": tc.get("max_rounds", 4),
            "icon": tc.get("icon", ""),
        }

    async def start_typed_session(
        self,
        session_type: str,
        topic: str = "",
        orchestrator: Any = None,
        display_name: str | None = None,
        theme: str | None = None,
        extra_agents: list[str] | None = None,
    ) -> VastianSession:
        """Start a session of a specific type (Wave 6).

        Loads the type config from sessions.yaml, resolves agents,
        and creates a PRESENTATION-direction session with the lead agent
        as the initiator.
        """
        tc = self.get_session_type_config(session_type)
        if not tc:
            raise ValueError(f"Unknown session type: {session_type}")

        lead = tc["lead"]
        members = list(tc["default_members"])
        if extra_agents:
            for aid in extra_agents:
                if aid not in members:
                    members.append(aid)

        # Validate agents exist
        valid_ids = [aid for aid in members if self.registry.get_agent(aid)]
        if not valid_ids:
            raise ValueError(f"No valid agents for session type {session_type}")

        # Use topic or type label as fallback
        session_topic = topic or tc["label"]

        # Generate Noah session name if not provided
        if not display_name:
            display_name, theme = await self.generate_session_name(
                session_type,
                session_topic,
                orchestrator,
            )

        session = VastianSession(
            direction=SessionDirection.PRESENTATION,
            topic=session_topic,
            session_type=session_type,
            display_name=display_name or tc["label"],
            theme=theme,
            lead_agent=lead,
            presenter_name=lead,
            agent_ids=valid_ids,
            phase=SessionPhase.OPEN,
        )
        self._sessions[session.id] = session

        logger.info(
            "Typed session started [%s] type=%s: %s | Lead: %s | Agents: %s",
            session.id,
            session_type,
            session_topic,
            lead,
            ", ".join(valid_ids),
        )

        # Build the opening prompt based on session type
        opening = self._build_typed_opening(session_type, tc, session_topic, theme)

        lead_msg = SessionMessage(
            sender=lead,
            sender_name=self.registry.get_agent(lead).config.name
            if self.registry.get_agent(lead)
            else lead,
            content=opening,
            round_number=1,
            message_type="broadcast",
        )

        await self._emit_progress("start", lead, f"Starting {tc['label']} session...")

        responses = await self._broadcast_to_agents(
            session=session,
            initiator_message=lead_msg,
            orchestrator=orchestrator,
        )

        session.add_round(
            initiator_msg=lead_msg,
            responses=responses,
            round_type="typed_session",
        )

        self._persist_session(session)
        return session

    def _build_typed_opening(
        self, session_type: str, config: dict, topic: str, theme: str | None
    ) -> str:
        """Build the opening prompt for a typed session."""
        label = config.get("label", session_type)
        desc = config.get("description", "")
        theme_line = f"\n\nTheme: {theme}" if theme else ""

        prompts = {
            "onboarding": (
                f"# {label}: {topic}\n\n"
                f"{desc}\n\n"
                f"Welcome! Let's introduce ourselves and walk through the system. "
                f"Each agent: share your role, what you can help with, and one thing "
                f"the user should know about working with you.{theme_line}"
            ),
            "daily_briefing": (
                f"# {label}: {topic}\n\n"
                f"Good morning. Let's cover today's priorities.\n\n"
                f"Each agent: share your top priority for today, any blockers, "
                f"and anything the user needs to know.{theme_line}"
            ),
            "cycle_planning": (
                f"# {label}: {topic}\n\n"
                f"{desc}\n\n"
                f"Let's plan the next cycle. Review open tickets, propose priorities, "
                f"and estimate capacity. Liam will coordinate the sprint.{theme_line}"
            ),
            "wisdom_scenarios": (
                f"# {label}: {topic}\n\n"
                f"{desc}\n\n"
                f"Let's explore scenarios and run what-if analyses. "
                f"Each agent: bring your perspective on the topic.{theme_line}"
            ),
            "network_pulse": (
                f"# {label}: {topic}\n\n"
                f"Network health check. Felix will lead.\n\n"
                f"Each agent: report your current status, any issues, "
                f"and system health metrics relevant to your domain.{theme_line}"
            ),
            "backend_ops": (
                f"# {label}: {topic}\n\n"
                f"Operations review. Orion will lead.\n\n"
                f"Each agent: report on infrastructure, quality issues, "
                f"prompt health, and operational concerns.{theme_line}"
            ),
            "research_insights": (
                f"# {label}: {topic}\n\n"
                f"{desc}\n\n"
                f"Let's synthesize recent learnings. Elias will lead. "
                f"Share patterns, insights, and knowledge worth promoting.{theme_line}"
            ),
            "recalibration": (
                f"# {label}: {topic}\n\n"
                f"Periodic recalibration session. The digital twin leads.\n\n"
                f"Each agent: report what has changed since last recalibration "
                f"in your domain. Focus on goals, habits, priorities, and any drift "
                f"from the user's stated direction.{theme_line}"
            ),
            "themed": (
                f"# {label}: {topic}\n\n"
                f"Creative session. Noah picks the theme.\n\n"
                f"Let's explore something new together. "
                f"Be creative, bring fresh perspectives.{theme_line}"
            ),
        }
        if session_type in prompts:
            return prompts[session_type]
        if session_type == "persona_roleplay":
            from vastian.scenario_loader import format_composed_for_prompt, load_composed_prompt

            composed = load_composed_prompt(style="labyrinth")
            components_block = format_composed_for_prompt(composed)
            return (
                f"# {label}: {topic}\n\n"
                f"{desc}\n\n"
                f"Persona exploration. Adrian leads. Present the persona, allies, "
                f"challenges with emojis. Use numbered options for decision points."
                f"{theme_line}{components_block}"
            )
        if session_type == "spiritual_plan_showcase":
            from vastian.plans_loader import load_spiritual_plan_showcase_candidates

            candidates = load_spiritual_plan_showcase_candidates()
            plans_block = ""
            if candidates:
                lines = [
                    f"- {p.get('archetype', p.get('id', ''))}: {p.get('dimension', '')}"
                    for p in candidates[:5]
                ]
                plans_block = "\n\n**Plans to present**:\n" + "\n".join(lines)
            return (
                f"# {label}: {topic}\n\n"
                f"{desc}\n\n"
                f"Sage leads. Analyze user metrics, career profile, therapy insights. "
                f"Present 3-5 spiritual plan choices (archetype cards with brief descriptions). "
                f"User selects one or refines through dialogue."
                f"{theme_line}{plans_block}"
            )
        return f"# {label}: {topic}\n\n{desc}{theme_line}"

    def get_sessions(
        self,
        session_type: str | None = None,
        phase: str | None = None,
    ) -> list[dict]:
        """Return session summaries, optionally filtered by type and/or phase."""
        all_sessions = self._store.load_all()
        results = []
        for s in all_sessions:
            if session_type and s.get("session_type") != session_type:
                continue
            if phase and s.get("phase") != phase:
                continue
            results.append(s)
        results.sort(key=lambda s: s.get("created_at", ""), reverse=True)
        return results

    async def generate_session_name(
        self,
        session_type: str,
        topic: str = "",
        orchestrator: Any = None,
    ) -> tuple[str, str]:
        """Ask Noah to generate a creative session name and theme.

        Returns (display_name, theme). Falls back to the session type label
        if Noah is unavailable or the orchestrator is not provided.
        """
        tc = self.get_session_type_config(session_type)
        fallback_name = tc["label"] if tc else session_type.replace("_", " ").title()

        if not orchestrator:
            return fallback_name, ""

        noah = self.registry.get_agent("noah")
        if not noah:
            return fallback_name, ""

        prompt = (
            f"Generate a creative, evocative name for a '{session_type}' session "
            f"about '{topic or fallback_name}'. Also generate a short theme or flavor "
            f"line (1 sentence max).\n\n"
            f"Respond in EXACTLY this format (2 lines, no extra text):\n"
            f"NAME: <creative session name>\n"
            f"THEME: <short theme or vibe>\n\n"
            f"Be creative but concise. The name should feel like an episode title."
        )

        try:
            from vastian.access_tiers import is_agent_enabled

            # Check if Noah is enabled at current tier
            user_tier = getattr(orchestrator, "user_tier", "open")
            if not is_agent_enabled("noah", user_tier):
                return fallback_name, ""

            llm = orchestrator._get_llm_for_agent("noah") or orchestrator.llm
            if not llm:
                return fallback_name, ""

            model = orchestrator._resolve_model_for_agent("noah")
            result = await llm.generate(
                system_prompt="You are Noah, the Creative Director. Generate session names.",
                messages=[{"role": "user", "content": prompt}],
                model=model,
                temperature=0.9,
            )

            # Parse NAME: and THEME: from response
            name = fallback_name
            theme = ""
            for line in result.strip().split("\n"):
                line = line.strip()
                if line.upper().startswith("NAME:"):
                    name = line.split(":", 1)[1].strip().strip('"')
                elif line.upper().startswith("THEME:"):
                    theme = line.split(":", 1)[1].strip().strip('"')
            return name or fallback_name, theme
        except Exception as e:
            logger.debug("Noah session naming failed, using fallback: %s", e)
            return fallback_name, ""

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

    # ══════════════════════════════════════════════════
    # PRESENTATION — Cursor presents TO the network
    # ══════════════════════════════════════════════════

    async def start_presentation(
        self,
        topic: str,
        brief_content: str = "",
        brief_path: str | None = None,
        agent_ids: list[str] | None = None,
        presenter_name: str = "Cursor AI",
        orchestrator: Any = None,
    ) -> VastianSession:
        """Start a presentation — Cursor presents to all agents, they react.

        Args:
            topic: The presentation topic.
            brief_content: Full text of the brief document.
            brief_path: Path to the brief file (for reference).
            agent_ids: Agents to include. None = all registered agents.
            presenter_name: Display name of the presenter.
            orchestrator: The Orchestrator instance for full tool-calling support.
        """
        # Resolve agent list
        if agent_ids is None or agent_ids == ["all"]:
            agent_ids = list(self.registry.agent_ids)

        valid_ids = [aid for aid in agent_ids if self.registry.get_agent(aid)]
        if not valid_ids:
            raise ValueError("No valid agents found for presentation")

        session = VastianSession(
            direction=SessionDirection.PRESENTATION,
            topic=topic,
            presenter_name=presenter_name,
            brief_path=brief_path,
            brief_content=brief_content,
            agent_ids=valid_ids,
            phase=SessionPhase.OPEN,
        )
        self._sessions[session.id] = session

        logger.info(
            "Presentation started [%s]: %s | Agents: %s",
            session.id,
            topic,
            ", ".join(valid_ids),
        )

        # Build the opening message
        opening_content = f"# Presentation: {topic}\n\n"
        if brief_content:
            opening_content += brief_content
        else:
            opening_content += f"Topic for discussion: {topic}"

        presenter_msg = SessionMessage(
            sender="presenter",
            sender_name=presenter_name,
            content=opening_content,
            round_number=1,
            message_type="broadcast",
        )

        await self._emit_progress("start", "", f"Presenting to {len(valid_ids)} agents...")

        responses = await self._broadcast_to_agents(
            session=session,
            initiator_message=presenter_msg,
            orchestrator=orchestrator,
        )

        session.add_round(
            initiator_msg=presenter_msg,
            responses=responses,
            round_type="presentation",
        )

        self._persist_session(session)
        return session

    async def presentation_say(
        self,
        session_id: str,
        message: str,
        orchestrator: Any = None,
    ) -> VastianSession:
        """Presenter broadcasts a follow-up to all agents."""
        session = self._get_session(session_id)
        self._assert_direction(session, SessionDirection.PRESENTATION)

        presenter_msg = SessionMessage(
            sender="presenter",
            sender_name=session.presenter_name,
            content=message,
            round_number=session.current_round + 1,
            message_type="broadcast",
        )

        await self._emit_progress("say", session.presenter_name, message[:200])

        responses = await self._broadcast_to_agents(
            session=session,
            initiator_message=presenter_msg,
            orchestrator=orchestrator,
        )

        session.add_round(
            initiator_msg=presenter_msg,
            responses=responses,
            round_type="broadcast",
        )

        self._persist_session(session)
        return session

    async def presentation_ask(
        self,
        session_id: str,
        agent_id: str,
        message: str,
        orchestrator: Any = None,
    ) -> VastianSession:
        """Presenter asks a specific agent a question."""
        session = self._get_session(session_id)
        self._assert_direction(session, SessionDirection.PRESENTATION)

        if agent_id not in session.agent_ids:
            raise ValueError(f"Agent '{agent_id}' is not in this session")

        agent = self.registry.get_agent(agent_id)
        if agent is None:
            raise ValueError(f"Agent '{agent_id}' not found in registry")

        presenter_msg = SessionMessage(
            sender="presenter",
            sender_name=session.presenter_name,
            content=message,
            round_number=session.current_round + 1,
            message_type="targeted",
            target_agent=agent_id,
        )

        await self._emit_progress("ask", agent.config.name, message[:200])

        response = await self._get_agent_response(
            session=session,
            agent=agent,
            initiator_message=presenter_msg,
            orchestrator=orchestrator,
        )

        session.add_round(
            initiator_msg=presenter_msg,
            responses=[response],
            round_type="targeted",
        )

        self._persist_session(session)
        return session

    # ══════════════════════════════════════════════════
    # REQUEST — Network agents request FROM Cursor
    # ══════════════════════════════════════════════════

    async def create_request(
        self,
        agent_id: str,
        topic: str,
        details: str,
        notify_agents: list[str] | None = None,
        orchestrator: Any = None,
    ) -> VastianSession:
        """A Vastian agent creates a request for the external architect (Cursor).

        Args:
            agent_id: The agent making the request.
            topic: Short title for the request.
            details: Full description of what the agent needs.
            notify_agents: Other agents to loop in (they'll see the request).
            orchestrator: The Orchestrator instance.
        """
        agent = self.registry.get_agent(agent_id)
        if agent is None:
            raise ValueError(f"Agent '{agent_id}' not found")

        # The requesting agent + any notified agents are participants
        participant_ids = [agent_id]
        if notify_agents:
            for nid in notify_agents:
                if nid not in participant_ids and self.registry.get_agent(nid):
                    participant_ids.append(nid)

        session = VastianSession(
            direction=SessionDirection.REQUEST,
            topic=topic,
            requesting_agent=agent_id,
            requesting_agent_name=agent.config.name,
            agent_ids=participant_ids,
            phase=SessionPhase.OPEN,
        )
        self._sessions[session.id] = session

        logger.info(
            "Request created [%s] by %s: %s",
            session.id,
            agent.config.name,
            topic,
        )

        # Record the initial request message
        request_msg = SessionMessage(
            sender=agent_id,
            sender_name=agent.config.name,
            content=details,
            round_number=1,
            message_type="request",
        )

        session.add_round(
            initiator_msg=request_msg,
            responses=[],
            round_type="request",
        )

        self._persist_session(session)
        return session

    async def respond_to_request(
        self,
        session_id: str,
        message: str,
        responder_name: str = "Cursor AI",
        notify_agents: bool = True,
        orchestrator: Any = None,
    ) -> VastianSession:
        """External agent (Cursor) responds to a network request.

        If notify_agents is True, all session agents also see the response
        and can react.
        """
        session = self._get_session(session_id)
        self._assert_direction(session, SessionDirection.REQUEST)

        response_msg = SessionMessage(
            sender="presenter",
            sender_name=responder_name,
            content=message,
            round_number=session.current_round + 1,
            message_type="response",
        )

        agent_responses: list[SessionMessage] = []
        if notify_agents and orchestrator:
            # Let session agents react to the response
            agent_responses = await self._broadcast_to_agents(
                session=session,
                initiator_message=response_msg,
                orchestrator=orchestrator,
            )

        session.add_round(
            initiator_msg=response_msg,
            responses=agent_responses,
            round_type="response",
        )

        self._persist_session(session)
        return session

    def get_pending_requests(self) -> list[dict]:
        """Get all open requests from the network (for Cursor to review)."""
        records = self._store.query(
            lambda r: r.get("direction") == "request" and r.get("phase") == "open"
        )
        return [
            {
                "id": r.get("id", "?"),
                "topic": r.get("topic", "?"),
                "requested_by": r.get("requesting_agent_name", r.get("requesting_agent", "?")),
                "rounds": r.get("current_round", 0),
                "created_at": r.get("created_at", "?"),
            }
            for r in records
        ]

    # ══════════════════════════════════════════════════
    # SHARED — Works for both directions
    # ══════════════════════════════════════════════════

    async def discuss(
        self,
        session_id: str,
        orchestrator: Any = None,
    ) -> VastianSession:
        """Open the floor — agents respond to each other sequentially.

        Works for both presentations and requests.
        """
        session = self._get_session(session_id)

        await self._emit_progress("discuss", "", "Opening the floor for agent discussion...")

        responses: list[SessionMessage] = []
        prior_discussion: list[dict[str, str]] = []

        for agent_id in session.agent_ids:
            agent = self.registry.get_agent(agent_id)
            if agent is None:
                continue

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

            context = self._build_session_context(session)
            if prior_discussion:
                context += "\n\n## Current Discussion Round\n"
                for pd in prior_discussion:
                    context += f"\n**{pd['name']}**: {pd['content']}\n"

            context += (
                "\n\nThis is an open discussion round. Build on, agree with, or "
                "challenge what others have said. Be specific from your role's perspective."
            )

            response_text = await self._call_agent(
                agent=agent,
                context=context,
                session=session,
                orchestrator=orchestrator,
            )

            msg = SessionMessage(
                sender=agent_id,
                sender_name=agent.config.name,
                content=response_text,
                round_number=session.current_round + 1,
                message_type="discuss",
            )
            responses.append(msg)
            prior_discussion.append({"name": agent.config.name, "content": response_text})

            await self._emit_progress("contribution", agent.config.name, response_text[:500])

        session.add_round(
            initiator_msg=None,
            responses=responses,
            round_type="discuss",
        )

        self._persist_session(session)
        return session

    async def wrap(
        self,
        session_id: str,
        orchestrator: Any = None,
    ) -> VastianSession:
        """Conclude the session with a summary. Works for both directions."""
        session = self._get_session(session_id)
        session.phase = SessionPhase.WRAPPING

        await self._emit_progress("wrap", "", "Generating session summary...")

        # Use Charles as summarizer, or first agent
        summarizer_id = "charles" if "charles" in session.agent_ids else session.agent_ids[0]
        summarizer = self.registry.get_agent(summarizer_id)

        if summarizer is None:
            session.summary = self._manual_summary(session)
        else:
            direction_context = (
                f"a {session.direction_label} session"
                if session.direction == SessionDirection.PRESENTATION
                else f"a Request session initiated by {session.requesting_agent_name}"
            )

            summary_prompt = (
                f"You are summarizing {direction_context} between an external "
                "consultant and the Vastian Network.\n\n"
                f"## Session Transcript\n{session.transcript}\n\n"
                "## Your Task\n"
                "Produce a comprehensive session summary with these sections:\n\n"
                "### Decisions Made\nList any decisions agreed upon.\n\n"
                "### Key Concerns Raised\nList significant concerns from any participant.\n\n"
                "### Open Questions\nList unresolved questions needing follow-up.\n\n"
                "### Action Items\n"
                "List specific action items with assignments. Format each as:\n"
                "- [ACTION] assigned to [AGENT_ID]: description\n\n"
                "### Participant Sentiment\n"
                "Brief note on each participant's overall position.\n\n"
                "Be thorough but concise."
            )

            summary_text = await self._call_agent(
                agent=summarizer,
                context=summary_prompt,
                session=session,
                orchestrator=orchestrator,
            )
            session.summary = summary_text
            self._extract_action_items(session)

        session.phase = SessionPhase.COMPLETED
        session.completed_at = datetime.now(UTC)
        self._persist_session(session)

        # Persist session knowledge to each participating agent's docs folder
        # so they retain it across future sessions and conversations.
        self._persist_to_agent_docs(session)

        logger.info(
            "Session wrapped [%s]: %s — %d rounds, %d messages",
            session.id,
            session.direction.value,
            session.current_round,
            len(session.all_messages),
        )
        return session

    async def add_human_feedback(
        self,
        session_id: str,
        feedback: str,
    ) -> VastianSession:
        """Record human feedback that agents will see in the next round context.

        This is a lightweight way for the human reviewer to provide direction
        between rounds without triggering a full broadcast.
        """
        session = self._get_session(session_id)
        session.human_feedback.append(
            {
                "feedback": feedback,
                "timestamp": datetime.now(UTC).isoformat(),
            }
        )
        self._persist_session(session)
        logger.info("Human feedback recorded for session %s", session_id)
        return session

    async def analyze_round(
        self,
        session_id: str,
        round_number: int | None = None,
        orchestrator: Any = None,
    ) -> dict:
        """Analyze a round's responses for sentiment, consensus, and concerns.

        Uses a single LLM call (gpt-4o-mini) to produce structured analysis.

        Returns:
            {
                "per_agent_sentiment": {agent_id: {sentiment, confidence, key_concern}},
                "consensus_level": int (0-100),
                "top_concerns": [{agent, concern, severity}],
                "suggested_rebuttals": [{agent, topic, suggested_response}],
            }
        """
        session = self._get_session(session_id)

        # Find the target round
        target_round = round_number or session.current_round
        matching = [r for r in session.rounds if r.round_number == target_round]
        if not matching:
            return {"error": f"Round {target_round} not found"}

        rnd = matching[0]
        if not rnd.responses:
            return {"error": f"Round {target_round} has no responses"}

        # Build a transcript of just this round's responses
        responses_text = ""
        for resp in rnd.responses:
            responses_text += f"\n## {resp.sender_name} ({resp.sender})\n{resp.content}\n"

        analysis_prompt = (
            "You are analyzing responses from a presentation round in the Vastian Network.\n"
            f"Topic: {session.topic}\n\n"
            "## Agent Responses\n"
            f"{responses_text}\n\n"
            "## Your Task\n"
            "Analyze ALL agent responses and produce a JSON object with this exact structure:\n"
            "```json\n"
            "{\n"
            '  "per_agent_sentiment": {\n'
            '    "agent_id": {\n'
            '      "sentiment": "supportive|cautious|concerned|opposed|misaligned",\n'
            '      "confidence": 0-100,\n'
            '      "key_concern": "one-line summary of their main concern or position"\n'
            "    }\n"
            "  },\n"
            '  "consensus_level": 0-100,\n'
            '  "top_concerns": [\n'
            '    {"agent": "agent_id", "concern": "description", "severity": "high|medium|low"}\n'
            "  ],\n"
            '  "suggested_rebuttals": [\n'
            '    {"agent": "agent_id", "topic": "what to address", "suggested_response": "how to respond"}\n'
            "  ]\n"
            "}\n"
            "```\n\n"
            "Sentiment categories:\n"
            "- supportive: Agrees with the proposal, constructive feedback\n"
            "- cautious: Generally agrees but has significant concerns\n"
            "- concerned: Has reservations that need addressing\n"
            "- opposed: Disagrees with the approach\n"
            "- misaligned: Misunderstands their role or the proposal\n\n"
            "Return ONLY the JSON object, no other text."
        )

        import json

        # Use gpt-4o-mini for cost efficiency
        try:
            if orchestrator is not None:
                from vastian.llm.provider import OpenAIProvider

                provider = OpenAIProvider(orchestrator.settings)
                result = await provider.generate(
                    system_prompt="You are an analyst. Return only valid JSON.",
                    messages=[{"role": "user", "content": analysis_prompt}],
                    model="gpt-4o-mini",
                    temperature=0.3,
                )
            else:
                # Fallback: use Charles agent
                charles = self.registry.get_agent("charles")
                if charles:
                    result = await charles._generate(analysis_prompt)
                else:
                    return {"error": "No orchestrator or agent available for analysis"}

            # Parse JSON from response
            result = result.strip()
            # Strip markdown code fences if present
            if result.startswith("```"):
                lines = result.split("\n")
                result = (
                    "\n".join(lines[1:-1]) if lines[-1].strip() == "```" else "\n".join(lines[1:])
                )
                result = result.strip()

            analysis = json.loads(result)

            # Store analysis in session
            analysis["round_number"] = target_round
            analysis["analyzed_at"] = datetime.now(UTC).isoformat()
            session.round_analyses.append(analysis)
            self._persist_session(session)

            return analysis

        except json.JSONDecodeError as e:
            logger.warning("Failed to parse analysis JSON: %s", e)
            return {"error": f"JSON parse error: {e}", "raw": result[:500] if result else ""}
        except Exception as e:
            logger.warning("Analysis failed: %s", e)
            return {"error": str(e)}

    def get_status(self, session_id: str) -> dict:
        """Return current session state."""
        session = self._get_session(session_id)
        return session.to_summary()

    def list_sessions(self, direction: str | None = None) -> list[dict]:
        """List sessions, optionally filtered by direction."""
        records = self._store.load_all()
        if direction:
            records = [r for r in records if r.get("direction") == direction]
        return [
            {
                "id": r.get("id", "?"),
                "direction": r.get("direction", "?"),
                "topic": r.get("topic", "?"),
                "phase": r.get("phase", "?"),
                "rounds": r.get("current_round", 0),
                "created_at": r.get("created_at", "?"),
            }
            for r in records
        ]

    # ──────────────────────────────────────────────────
    # Internal: Agent communication
    # ──────────────────────────────────────────────────

    async def _broadcast_to_agents(
        self,
        session: VastianSession,
        initiator_message: SessionMessage,
        orchestrator: Any = None,
    ) -> list[SessionMessage]:
        """Send a message to all session agents in parallel, collect responses."""
        tasks = []
        for agent_id in session.agent_ids:
            agent = self.registry.get_agent(agent_id)
            if agent is None:
                continue
            tasks.append(self._get_agent_response(session, agent, initiator_message, orchestrator))

        if tasks:
            responses = await asyncio.gather(*tasks, return_exceptions=True)
            valid: list[SessionMessage] = []
            for resp in responses:
                if isinstance(resp, Exception):
                    logger.warning("Agent response failed: %s", resp)
                else:
                    valid.append(resp)
            return valid
        return []

    async def _get_agent_response(
        self,
        session: VastianSession,
        agent: BaseAgent,
        initiator_message: SessionMessage,
        orchestrator: Any = None,
    ) -> SessionMessage:
        """Get a single agent's response to a message."""
        await self._emit_progress(
            "thinking", agent.config.name, f"{agent.config.name} is thinking..."
        )

        context = self._build_session_context(session)
        context += (
            f"\n\n## Latest from {initiator_message.sender_name}\n{initiator_message.content}"
        )

        if initiator_message.target_agent == agent.agent_id:
            context += (
                "\n\n**This message is directed at you specifically. Provide a detailed response.**"
            )

            # Inject per-agent analysis assessment for targeted rebuttals
            agent_assessment = self._get_agent_assessment(session, agent.agent_id)
            if agent_assessment:
                context += f"\n\n## Assessment of Your Previous Response\n{agent_assessment}"

        response_text = await self._call_agent(
            agent=agent,
            context=context,
            session=session,
            orchestrator=orchestrator,
        )

        await self._emit_progress("contribution", agent.config.name, response_text[:500])

        return SessionMessage(
            sender=agent.agent_id,
            sender_name=agent.config.name,
            content=response_text,
            round_number=session.current_round + 1,
            message_type=initiator_message.message_type,
        )

    async def _call_agent(
        self,
        agent: BaseAgent,
        context: str,
        session: VastianSession,
        orchestrator: Any = None,
    ) -> str:
        """Call an agent with session context. Uses tool-calling if orchestrator available."""
        if session.direction == SessionDirection.PRESENTATION:
            direction_instruction = (
                f"{session.presenter_name} is presenting a proposal. "
                "Respond from your role's perspective with specific concerns, "
                "suggestions, and commitments. If you need something built or "
                "changed, say so — you can also create a Request back to the "
                "presenter for specific needs."
            )
        else:
            direction_instruction = (
                f"{session.requesting_agent_name} has made a request to the external "
                "architect. Provide your perspective on the request — whether it "
                "aligns with your domain, any dependencies or conflicts you see, "
                "and any additional requirements from your role."
            )

        session_prompt = (
            f"\n\n--- {session.direction_label.upper()} SESSION: {session.topic} ---\n"
            f"Direction: {session.direction_label}\n"
            f"Participants: {', '.join(session.agent_ids)}\n"
            f"Phase: {session.phase.value} | Round: {session.current_round}\n\n"
            f"INSTRUCTIONS:\n"
            f"- {direction_instruction}\n"
            "- Reference your owned documents and real data when possible.\n"
            "- If you have concerns, state them clearly. If you agree, say why.\n"
            "- Be concise but substantive — this is a working session.\n"
            "- You have access to your tools. Use read_document, write_document, "
            "search_knowledge, etc. as needed to ground your response in real data.\n"
        )

        if orchestrator is not None:
            # Use the full tool-calling pipeline
            original_extra = agent.config.system_prompt_extra
            agent.config.system_prompt_extra = original_extra + session_prompt

            try:
                response = await orchestrator.handle_user_message_direct(
                    content=context,
                    agent_id=agent.agent_id,
                )
            finally:
                agent.config.system_prompt_extra = original_extra

            return response
        else:
            full_prompt = context + session_prompt
            return await agent._generate(full_prompt)

    def _get_agent_assessment(self, session: VastianSession, agent_id: str) -> str:
        """Extract the per-agent analysis assessment for targeted rebuttals.

        If the agent was flagged as misaligned, include correction framing.
        Also includes any human feedback that mentions this agent.
        """
        parts: list[str] = []

        # Check round analyses for this agent's sentiment
        for analysis in session.round_analyses:
            per_agent = analysis.get("per_agent_sentiment", {})
            if agent_id in per_agent:
                info = per_agent[agent_id]
                sentiment = info.get("sentiment", "unknown")
                confidence = info.get("confidence", "?")
                concern = info.get("key_concern", "")
                round_num = analysis.get("round_number", "?")

                parts.append(
                    f"Round {round_num} analysis rated your response as "
                    f"**{sentiment}** (confidence: {confidence}%). "
                    f"Key concern noted: {concern}"
                )

                if sentiment == "misaligned":
                    parts.append(
                        "**IMPORTANT: Your previous response showed a misunderstanding "
                        "of your role or the proposal. The presenter is now correcting "
                        "this. Please carefully re-read the correction and respond with "
                        "an updated understanding.**"
                    )

                # Include suggested rebuttal for this agent if any
                rebuttals = analysis.get("suggested_rebuttals", [])
                for r in rebuttals:
                    if r.get("agent") == agent_id:
                        parts.append(f"Topic to address: {r.get('topic', '')}")

        # Include human feedback that mentions this agent
        for fb in session.human_feedback:
            feedback_text = fb.get("feedback", "")
            if agent_id.lower() in feedback_text.lower():
                parts.append(f"**Human reviewer specifically noted about you:** {feedback_text}")

        return "\n".join(parts) if parts else ""

    def _build_session_context(self, session: VastianSession) -> str:
        """Build a context string from session history for agent consumption."""
        parts = [
            f"# {session.direction_label}: {session.topic}",
        ]

        if session.direction == SessionDirection.PRESENTATION:
            parts.append(f"Presenter: {session.presenter_name}")
        else:
            parts.append(f"Requested by: {session.requesting_agent_name}")

        parts.append("")

        if session.brief_content:
            brief_preview = session.brief_content[:3000]
            if len(session.brief_content) > 3000:
                brief_preview += "\n\n[... brief truncated for context window ...]"
            parts.append(f"## Brief\n{brief_preview}")

        # Include prior rounds (most recent 3 to manage context window)
        recent_rounds = session.rounds[-3:]
        if recent_rounds:
            parts.append("\n## Recent Discussion")
            for r in recent_rounds:
                parts.append(f"\n### Round {r.round_number} ({r.round_type})")
                if r.initiator_message:
                    parts.append(
                        f"**{r.initiator_message.sender_name}**: "
                        f"{r.initiator_message.content[:800]}"
                    )
                for resp in r.responses:
                    parts.append(f"**{resp.sender_name}**: {resp.content[:500]}")

        # Include human feedback if any
        if session.human_feedback:
            parts.append("\n## Human Reviewer Feedback")
            for fb in session.human_feedback:
                parts.append(f"- **Human reviewer noted**: {fb['feedback']}")

        # Include latest round analysis if available
        if session.round_analyses:
            latest = session.round_analyses[-1]
            concerns = latest.get("top_concerns", [])
            if concerns:
                parts.append("\n## Analysis Highlights")
                for c in concerns[:5]:
                    parts.append(
                        f"- [{c.get('severity', '?').upper()}] {c.get('agent', '?')}: "
                        f"{c.get('concern', '')}"
                    )

        return "\n".join(parts)

    # ──────────────────────────────────────────────────
    # Agent Knowledge Persistence
    # ──────────────────────────────────────────────────

    def _persist_to_agent_docs(self, session: VastianSession) -> None:
        """Write session brief + summary to each participating agent's docs/briefs/ folder.

        This is the key mechanism for agent knowledge retention across sessions.
        After a session wraps, each participant gets a markdown file in their
        ``docs/{agent_id}/briefs/`` folder containing:

        - The original brief (if any)
        - Their own response(s)
        - The session summary and action items
        - Key takeaways relevant to their role

        The ``briefs/`` folder is auto-discovered by the agent's document
        scanner (not in ``archive/``), so agents see these files in their
        context.  To archive old briefs, move them to
        ``docs/{agent_id}/archive/``.
        """
        import re

        project_root = (
            Path(__file__).resolve().parents[5]
        )  # src/vastian/bridges/cursor_bridge/sessions/ -> project root
        docs_root = project_root / "docs"

        # Build a filename-safe slug from the topic
        topic_slug = re.sub(r"[^a-z0-9]+", "-", session.topic.lower()).strip("-")[:50]
        date_str = (session.completed_at or datetime.now(UTC)).strftime("%Y%m%d")
        filename = f"{date_str}_{topic_slug}.md"

        # Collect each agent's own responses from all rounds
        agent_responses: dict[str, list[str]] = {}
        for rnd in session.rounds:
            for msg in rnd.responses:
                agent_responses.setdefault(msg.sender, []).append(msg.content)

        for agent_id in session.agent_ids:
            briefs_dir = docs_root / agent_id / "briefs"
            briefs_dir.mkdir(parents=True, exist_ok=True)

            lines: list[str] = []
            lines.append(f"# Session Brief: {session.topic}")
            lines.append(f"\n**Date**: {date_str}")
            lines.append(f"**Session ID**: {session.id}")
            lines.append(f"**Presenter**: {session.presenter_name or 'N/A'}")
            lines.append(f"**Participants**: {', '.join(session.agent_ids)}")
            lines.append("")

            # Include the original brief (trimmed to keep file manageable)
            if session.brief_content:
                brief_text = session.brief_content[:5000]
                lines.append("---")
                lines.append("\n## Original Brief\n")
                lines.append(brief_text)
                if len(session.brief_content) > 5000:
                    lines.append("\n*(brief truncated for brevity)*")
                lines.append("")

            # Include this agent's own responses
            my_responses = agent_responses.get(agent_id, [])
            if my_responses:
                lines.append("---")
                lines.append("\n## Your Responses\n")
                for i, resp in enumerate(my_responses, 1):
                    lines.append(f"### Round {i}\n")
                    lines.append(resp[:2000])
                    if len(resp) > 2000:
                        lines.append("\n*(response truncated)*")
                    lines.append("")

            # Include the session summary
            if session.summary:
                lines.append("---")
                lines.append("\n## Session Summary\n")
                lines.append(session.summary[:3000])
                lines.append("")

            # Include action items relevant to this agent
            my_actions = [a for a in session.action_items if a.get("assigned_to") == agent_id]
            if my_actions:
                lines.append("---")
                lines.append("\n## Action Items Assigned to You\n")
                for a in my_actions:
                    lines.append(f"- {a.get('description', '(no description)')}")
                lines.append("")

            filepath = briefs_dir / filename
            try:
                filepath.write_text("\n".join(lines), encoding="utf-8")
                logger.info(
                    "Session brief written for %s: %s",
                    agent_id,
                    filepath.name,
                )
            except Exception as e:
                logger.error(
                    "Failed to write session brief for %s: %s",
                    agent_id,
                    e,
                )

    # ──────────────────────────────────────────────────
    # Persistence
    # ──────────────────────────────────────────────────

    def _persist_session(self, session: VastianSession) -> None:
        """Save session state to the FileStore."""
        record = session.model_dump(mode="json")
        self._store.upsert(record)

    def _get_session(self, session_id: str) -> VastianSession:
        """Retrieve a session from memory or store."""
        if session_id in self._sessions:
            return self._sessions[session_id]

        record = self._store.get(session_id)
        if record is None:
            raise ValueError(f"Session not found: {session_id}")

        session = VastianSession.model_validate(record)
        self._sessions[session.id] = session
        return session

    @staticmethod
    def _assert_direction(session: VastianSession, expected: SessionDirection) -> None:
        """Raise if session direction doesn't match expected."""
        if session.direction != expected:
            raise ValueError(
                f"Session '{session.id}' is a {session.direction.value}, not a {expected.value}"
            )

    # ──────────────────────────────────────────────────
    # Summary helpers
    # ──────────────────────────────────────────────────

    def _manual_summary(self, session: VastianSession) -> str:
        """Fallback summary without LLM."""
        lines = [f"## Session Summary: {session.topic}\n"]
        lines.append(f"Type: {session.direction_label}")
        lines.append(f"Rounds: {session.current_round}")
        lines.append(f"Participants: {', '.join(session.agent_ids)}")
        lines.append("")

        for r in session.rounds:
            lines.append(f"### Round {r.round_number}")
            for resp in r.responses:
                lines.append(f"**{resp.sender_name}**: {resp.content[:300]}...")
            lines.append("")

        return "\n".join(lines)

    def _extract_action_items(self, session: VastianSession) -> None:
        """Extract action items from the session summary."""
        import re

        if not session.summary:
            return

        for line in session.summary.split("\n"):
            clean = line.strip()
            clean = re.sub(r"\*\*", "", clean)
            clean = re.sub(r"^\s*[-*]\s*", "", clean)
            clean = clean.strip()

            if not re.search(r"\[?ACTION\]?", clean, re.IGNORECASE):
                continue

            assigned_to = "charles"
            for aid in self.registry.agent_ids:
                if re.search(
                    rf"\[{re.escape(aid)}\]|(?<!\w){re.escape(aid)}(?!\w)",
                    clean,
                    re.IGNORECASE,
                ):
                    assigned_to = aid
                    break

            parts = clean.split(":", 1)
            if len(parts) == 2:
                description = parts[1].strip()
            else:
                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(
                    {
                        "assigned_to": assigned_to,
                        "description": description,
                    }
                )
