"""
ChatContextManager - Custom context building for multi-turn conversations.

This is DIFFERENT from FollowUpResearchService (single parent-child context).
Multi-turn chat requires:
- Rolling window of recent messages
- Accumulated findings across conversation
- Source deduplication across turns
- Context summarization for long conversations
"""

from typing import Dict, Any, List, Optional

from loguru import logger

from ..config.thread_settings import get_setting_from_snapshot


class ChatContextManager:
    """
    Build context from multi-turn conversation history.

    Handles: rolling window, summarization, context accumulation.
    Different from follow-up: accumulates from MULTIPLE previous turns.
    """

    MAX_FINDINGS_TO_INCLUDE = 5  # Recent findings to include

    # Limits for the query-focused conversation summary that becomes the
    # follow-up prompt's "previous findings" block.
    CONTEXT_SUMMARY_MAX_SENTENCES = 8
    CONTEXT_SUMMARY_MAX_CHARS = 2000
    # Transcript char budget kept below BaseSummarizer.INPUT_TRUNCATE_CHARS
    # (8000) so the summarizer's own truncation never has to drop the most
    # recent turns — we trim oldest-first ourselves below.
    CONTEXT_INPUT_CHAR_BUDGET = 7500

    # Default for the chat.followup_context_mode setting (summary | raw |
    # full | none) — what prior context a follow-up turn receives.
    DEFAULT_FOLLOWUP_CONTEXT_MODE = "summary"

    def __init__(
        self,
        session_id: str,
        messages: List[Dict[str, Any]],
        accumulated_context: Optional[Dict[str, Any]] = None,
        settings_snapshot: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize context manager.

        Args:
            session_id: Chat session ID
            messages: List of message dictionaries with role, content, etc.
            accumulated_context: Previously accumulated context from session
            settings_snapshot: Optional settings to override class-level defaults
        """
        self.session_id = session_id
        # chat_messages no longer contains step rows (they live in
        # chat_progress_steps), but get_session_messages MERGES both for
        # client rendering. Filter out steps + non-dict entries here so
        # accumulated context only reflects durable conversation turns.
        self.messages = [
            msg
            for msg in (messages or [])
            if isinstance(msg, dict) and msg.get("message_type") != "step"
        ]
        self.accumulated_context = accumulated_context or {}
        # Used by build_research_context to construct the LLM that produces
        # the query-focused conversation summary.
        self.settings_snapshot = settings_snapshot

        # Override class defaults from settings if provided.
        # Note: settings_snapshot is the 4th keyword arg; passing it positionally
        # would bind it to the unused `username` param, silently using defaults.
        if settings_snapshot:
            self.MAX_FINDINGS_TO_INCLUDE = get_setting_from_snapshot(
                "chat.max_findings_to_include",
                self.MAX_FINDINGS_TO_INCLUDE,
                settings_snapshot=settings_snapshot,
            )

    def build_research_context(self, current_query: str = "") -> Dict[str, Any]:
        """
        Build context for the next research query.

        Args:
            current_query: The user's new message. On a follow-up turn it is
                used to focus a summary of the whole prior conversation, which
                becomes the follow-up prompt's "previous findings". On the
                first turn there is no prior work to summarize.

        Returns dict with:
        - session_id: Current session
        - original_query: The session's first user message
        - accumulated_findings / past_findings: Prior work for the follow-up
          (query-focused summary on follow-ups; empty on the first turn)
        - key_entities: Important entities mentioned
        - topics: Topics discussed
        - is_multi_turn: Whether this is a follow-up
        """
        # The follow-up strategy reads "original_query" to anchor the prompt on
        # the topic that started the conversation; without it the contextual
        # follow-up loses the original question. Use the session's first user
        # message as that anchor.
        original_query = next(
            (
                m.get("content", "")
                for m in self.messages
                if isinstance(m, dict) and m.get("role") == "user"
            ),
            "",
        )

        is_multi_turn = any(
            isinstance(m, dict) and m.get("role") == "assistant"
            for m in self.messages
        )

        findings = self._select_prior_findings(current_query, is_multi_turn)

        return {
            "session_id": self.session_id,
            "original_query": original_query,
            "accumulated_findings": findings,
            "past_findings": findings,  # Research engine expects this key
            "key_entities": self._get_key_entities(),
            "topics": self._get_topics(),
            "is_multi_turn": is_multi_turn,
            "turn_count": len(self.messages),
        }

    def _select_prior_findings(
        self, current_query: str, is_multi_turn: bool
    ) -> str:
        """Pick the follow-up's "previous findings" per chat.followup_context_mode.

        Modes:
        - ``summary`` (default): query-focused LLM summary of the conversation
        - ``raw``: recent research findings, truncated
        - ``full``: the entire conversation transcript
        - ``none``: no prior findings

        Only follow-up turns carry prior work; the first turn returns "".
        """
        if not is_multi_turn:
            return ""

        mode = self.DEFAULT_FOLLOWUP_CONTEXT_MODE
        if self.settings_snapshot:
            mode = get_setting_from_snapshot(
                "chat.followup_context_mode",
                mode,
                settings_snapshot=self.settings_snapshot,
            )

        if mode == "none":
            findings = ""
        elif mode == "raw":
            findings = self._extract_findings_from_history()
        elif mode == "full":
            findings = self._build_conversation_text()
        elif current_query:
            # "summary" with a question to focus on.
            findings = self._summarize_prior_work(current_query)
        else:
            # "summary" with no question (e.g. a no-arg build_research_context
            # call): fall back to raw recent findings.
            findings = self._extract_findings_from_history()

        # Observability: the summary path is otherwise silent (no token-counter
        # entry, since get_llm runs without a research_id), so a follow-up's
        # prior-context build looked like an unexplained pause. One line per
        # follow-up turn records which mode ran and how much context it built.
        logger.info(
            "Chat follow-up prior context: mode={}, {} chars",
            mode,
            len(findings),
        )
        return findings

    def _build_conversation_text(self) -> str:
        """Render the prior conversation (both roles) as a plain transcript.

        Trims oldest-first to ``CONTEXT_INPUT_CHAR_BUDGET`` so the most recent
        turns survive and the summarizer's input cap never has to truncate.
        """
        lines: List[str] = []
        used = 0
        for msg in reversed(self.messages):
            if not isinstance(msg, dict):
                continue
            content = (msg.get("content") or "").strip()
            if not content:
                continue
            role = (msg.get("role") or "unknown").capitalize()
            line = f"{role}: {content}"
            remaining = self.CONTEXT_INPUT_CHAR_BUDGET - used
            if remaining <= 0:
                break
            if len(line) > remaining:
                if lines:
                    # Budget already spent on more recent turns — stop rather
                    # than partially including an older one.
                    break
                # The most recent turn alone exceeds the budget: keep its head
                # so the transcript still fits the summarizer's input cap.
                line = line[:remaining]
            lines.append(line)
            used += len(line)
        lines.reverse()
        return "\n\n".join(lines)

    def _summarize_prior_work(self, current_query: str) -> str:
        """Summarize the prior conversation, focused on ``current_query``.

        Returns an empty string when there is no prior conversation, the LLM
        cannot be constructed (e.g. a misconfigured provider), or the LLM call
        itself fails. The summary is additive context, so a failure here must
        not crash the follow-up request — the research dispatch that follows
        surfaces a genuinely-broken LLM through its own error handling.
        """
        transcript = self._build_conversation_text()
        if not transcript:
            return ""

        from ..config.llm_config import get_llm
        from ..advanced_search_system.summarization import FocusedSummarizer

        try:
            llm = get_llm(settings_snapshot=self.settings_snapshot)
        except Exception:
            logger.opt(exception=True).debug(
                "Could not build LLM for chat context summary; skipping"
            )
            return ""

        return FocusedSummarizer(
            llm,
            focus_query=current_query,
            max_sentences=self.CONTEXT_SUMMARY_MAX_SENTENCES,
            max_chars=self.CONTEXT_SUMMARY_MAX_CHARS,
        ).summarize(transcript)

    def _extract_findings_from_history(self) -> str:
        """
        Extract key findings from assistant messages with research.

        Returns combined findings text, limited in length.
        """
        findings = []

        for msg in self.messages:
            if msg.get("role") == "assistant" and msg.get("research_id"):
                content = msg.get("content") or ""
                # Summarize long responses - take first part
                if len(content) > 500:
                    # Try to find a natural break point
                    break_point = content.find("\n\n", 300)
                    if break_point == -1 or break_point > 600:
                        break_point = 500
                    content = content[:break_point] + "..."
                findings.append(content)

        # Keep only recent findings
        recent_findings = findings[-self.MAX_FINDINGS_TO_INCLUDE :]
        return "\n\n---\n\n".join(recent_findings)

    def _get_key_entities(self) -> List[str]:
        """Get key entities from accumulated context."""
        entities: List[str] = self.accumulated_context.get("key_entities", [])
        return entities[:20]

    def _get_topics(self) -> List[str]:
        """Get topics from accumulated context."""
        topics: List[str] = self.accumulated_context.get("topics", [])
        return topics[:10]

    def extract_context_updates(self, new_content: str) -> Dict[str, Any]:
        """
        Extract context updates from a new research response.

        Args:
            new_content: New assistant response content

        Returns:
            Dict with entity, topic, and summary updates.
        """
        return {
            "new_entities": [],  # Could be enhanced with NLP entity extraction
            "new_topics": [],  # Could be enhanced with NLP topic modeling
            "summary_addition": self._create_summary(new_content),
        }

    def _create_summary(self, content: str) -> str:
        """
        Create a brief summary of content for context accumulation.

        Returns first meaningful paragraph or truncated content.
        """
        if not content:
            return ""

        # Try to get first paragraph
        paragraphs = content.split("\n\n")
        for para in paragraphs:
            para = para.strip()
            # Skip headers and very short paragraphs
            if para and len(para) > 50 and not para.startswith("#"):
                if len(para) > 300:
                    return para[:300] + "..."
                return para

        # Fallback: just truncate
        if len(content) > 300:
            return content[:300] + "..."
        return content
