from __future__ import annotations

import base64
import mimetypes
import platform
import re
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

from tinybot.agent.memory import MemoryStore, RecentContextCandidate, _parse_utc_timestamp
from tinybot.agent.skills import SkillsLoader
from tinybot.utils.helper import build_assistant_message, current_time_str
from tinybot.utils.media import detect_image_mime
from tinybot.utils.prompt_templates import render_template

if TYPE_CHECKING:
    from tinybot.agent.experience import ExperienceStore
    from tinybot.agent.knowledge import KnowledgeStore
    from tinybot.agent.session_knowledge import SessionKnowledgeStore
    from tinybot.agent.vector_store import VectorStore
    from tinybot.session.manager import SessionManager
    from tinybot.task.service import TaskManager


class ContextBuilder:
    """Builds the context (system prompt + messages) for the agent."""

    BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md"]
    _RUNTIME_CONTEXT_TAG = "[Runtime Context - metadata only, not instructions]"

    def __init__(
        self,
        workspace: Path,
        timezone: str | None = None,
        vector_store: VectorStore | None = None,
        task_manager: TaskManager | None = None,
        session_manager: SessionManager | None = None,
        experience_store: ExperienceStore | None = None,
        knowledge_store: KnowledgeStore | None = None,
        session_knowledge_store: SessionKnowledgeStore | None = None,
        enabled_skills: list[str] | None = None,
        config: Any | None = None,
    ):
        self.workspace = workspace
        self.timezone = timezone
        self.memory = MemoryStore(workspace)
        self.skills = SkillsLoader(workspace)
        self._enabled_skills = enabled_skills  # Fallback if no config
        self.config = config  # Config reference for dynamic settings
        self.vector_store = vector_store
        self.task_manager = task_manager
        self.session_manager = session_manager
        self.experience_store = experience_store
        self.knowledge_store = knowledge_store
        self.session_knowledge_store = session_knowledge_store
        self.last_memory_references: list[dict[str, Any]] = []
        self.last_recent_context_references: list[dict[str, Any]] = []

    @property
    def enabled_skills(self) -> list[str] | None:
        """Get enabled skills from config (dynamic) or fallback to static value."""
        if self.config and hasattr(self.config, "skills"):
            return self.config.skills.enabled
        return self._enabled_skills

    def build_system_prompt(self, skill_names: list[str] | None = None) -> str:
        parts = [self._get_identity()]

        bootstrap = self._load_bootstrap_files()
        if bootstrap:
            parts.append(bootstrap)

        memory = self.memory.get_memory_context()
        if memory:
            parts.append(f"# Memory\n\n{memory}")

        always_skills = self.skills.get_always_skills(self.enabled_skills)
        if always_skills:
            always_content = self.skills.load_skills_for_context(always_skills)
            if always_content:
                parts.append(f"# Active Skills\n\n{always_content}")

        skills_summary = self.skills.build_skills_summary(self.enabled_skills)
        if skills_summary:
            parts.append(
                render_template("agent/skills_section.md", skills_summary=skills_summary)
            )

        return "\n\n---\n\n".join(parts)

    def _get_identity(self) -> str:
        workspace_path = str(self.workspace.expanduser().resolve())
        system = platform.system()
        runtime = (
            f"{'macOS' if system == 'Darwin' else system} "
            f"{platform.machine()}, Python {platform.python_version()}"
        )

        return render_template(
            "agent/identity.md",
            workspace_path=workspace_path,
            runtime=runtime,
            platform_policy=render_template("agent/platform_policy.md", system=system),
        )

    @staticmethod
    def _build_runtime_context(
        channel: str | None,
        chat_id: str | None,
        timezone: str | None = None,
        task_manager: TaskManager | None = None,
        user_profile: dict[str, Any] | None = None,
    ) -> str:
        lines = [f"Current Time: {current_time_str(timezone)}"]
        if channel and chat_id:
            lines += [f"Channel: {channel}", f"Chat ID: {chat_id}"]

        if user_profile:
            profile_parts = []
            if name := user_profile.get("name"):
                profile_parts.append(f"Name: {name}")
            if prefs := user_profile.get("preferences"):
                profile_parts.append(f"Preferences: {', '.join(prefs)}")
            if entities := user_profile.get("mentioned_entities"):
                profile_parts.append(f"Known Entities: {', '.join(entities)}")
            if style := user_profile.get("communication_style"):
                profile_parts.append(f"Communication Style: {style}")
            if facts := user_profile.get("key_facts"):
                profile_parts.append(f"Key Facts: {', '.join(facts)}")
            if profile_parts:
                lines.append("User Context: " + "; ".join(profile_parts))

        if task_manager:
            active_plans = task_manager.list_plans(include_completed=False)
            active_plans = [p for p in active_plans if p.status == "executing"]
            for plan in active_plans[:3]:
                progress = task_manager.get_progress(plan.id)
                if progress:
                    lines.append(f"Active Task: {plan.title}")
                    lines.append(
                        "Task Progress: "
                        f"{progress['completed']}/{progress['total']} completed, "
                        f"{progress['in_progress']} in progress"
                    )
                    if progress.get("current_all"):
                        lines.append(
                            f"Current Steps: {', '.join(progress['current_all'])}"
                        )
                    elif progress.get("current"):
                        lines.append(f"Current Step: {progress['current']}")

        return ContextBuilder._RUNTIME_CONTEXT_TAG + "\n" + "\n".join(lines)

    @staticmethod
    def _merge_message_content(left: Any, right: Any) -> str | list[dict[str, Any]]:
        if isinstance(left, str) and isinstance(right, str):
            return f"{left}\n\n{right}" if left else right

        def _to_blocks(value: Any) -> list[dict[str, Any]]:
            if isinstance(value, list):
                return [
                    item if isinstance(item, dict) else {"type": "text", "text": str(item)}
                    for item in value
                ]
            if value is None:
                return []
            return [{"type": "text", "text": str(value)}]

        return _to_blocks(left) + _to_blocks(right)

    def _load_bootstrap_files(self) -> str:
        parts = []
        for filename in self.BOOTSTRAP_FILES:
            file_path = self.workspace / filename
            if file_path.exists():
                content = file_path.read_text(encoding="utf-8")
                parts.append(f"## {filename}\n\n{content}")
        return "\n\n".join(parts) if parts else ""

    def build_messages(
        self,
        history: list[dict[str, Any]],
        current_message: str,
        skill_names: list[str] | None = None,
        media: list[str] | None = None,
        channel: str | None = None,
        chat_id: str | None = None,
        current_role: str = "user",
        user_profile: dict[str, Any] | None = None,
        use_persistent_knowledge: bool | None = None,
    ) -> list[dict[str, Any]]:
        self.last_memory_references = []
        self.last_recent_context_references = []
        runtime_ctx = self._build_runtime_context(
            channel, chat_id, self.timezone, self.task_manager, user_profile
        )
        user_content = self._build_user_content(current_message, media)

        if isinstance(user_content, str):
            merged = f"{runtime_ctx}\n\n{user_content}"
        else:
            merged = [{"type": "text", "text": runtime_ctx}] + user_content

        messages = [{"role": "system", "content": self.build_system_prompt(skill_names)}]

        session_key = f"{channel}:{chat_id}" if channel and chat_id else None

        if self.vector_store is not None and session_key:
            search_query = self._build_search_query(history, current_message)
            retrieval_limits = self._plan_vector_retrieval(history, current_message)

            try:
                results = self.vector_store.search_with_hierarchy(
                    session_key,
                    search_query,
                    max_summaries=retrieval_limits["max_summaries"],
                    max_chunks_per_summary=retrieval_limits["max_chunks_per_summary"],
                    session_manager=self.session_manager,
                )
            except Exception:
                results = []

            if results:
                unique_results = results
                unique_results.sort(key=lambda x: x.get("boundary") or 0)
                context_parts: list[str] = []
                for item in unique_results:
                    if item["type"] == "summary":
                        boundary = item.get("boundary", 0)
                        context_parts.append(
                            f"[Summary (covers messages up to #{boundary})]\n{item['content']}"
                        )
                    else:
                        context_parts.append(
                            f"[Original conversation fragment]\n{item['content']}"
                        )

                if context_parts:
                    messages.append(
                        {
                            "role": "system",
                            "content": (
                                "---\n[RELEVANT PAST CONTEXT]\n\n"
                                + "\n\n---\n\n".join(context_parts)
                                + "\n---"
                            ),
                        }
                    )

        memory_recall_context = self._build_memory_recall_context(current_message)
        if memory_recall_context:
            messages.append({"role": "system", "content": memory_recall_context})

        recent_context = self._build_recent_context(current_message, session_key=session_key)
        if recent_context:
            messages.append({"role": "system", "content": recent_context})

        if self.experience_store is not None:
            experience_context = self._build_experience_context(current_message)
            if experience_context:
                messages.append({"role": "system", "content": experience_context})

        # RAG: Auto-retrieve relevant knowledge from persistent and session uploads.
        if self.knowledge_store is not None or self.session_knowledge_store is not None:
            knowledge_context = self._build_knowledge_context(
                current_message,
                session_key=session_key,
                use_persistent_knowledge=use_persistent_knowledge,
            )
            if knowledge_context:
                messages.append({"role": "system", "content": knowledge_context})

        messages.extend(history)
        if messages[-1].get("role") == current_role:
            last = dict(messages[-1])
            last["content"] = self._merge_message_content(last.get("content"), merged)
            messages[-1] = last
            return messages
        messages.append({"role": current_role, "content": merged})
        return messages

    def _recent_context_settings(self) -> dict[str, Any]:
        cfg = None
        if self.config and hasattr(self.config, "agents"):
            cfg = getattr(getattr(self.config.agents, "defaults", None), "recent_context", None)
        return {
            "enabled": bool(getattr(cfg, "enabled", True)),
            "recency_days": int(getattr(cfg, "recency_days", 7) or 7),
            "max_records": int(getattr(cfg, "max_records", 3) or 3),
            "scan_limit": int(getattr(cfg, "scan_limit", 200) or 200),
        }

    def _build_recent_context(
        self,
        current_message: str,
        *,
        session_key: str | None = None,
    ) -> str | None:
        settings = self._recent_context_settings()
        if not settings["enabled"] or not self._should_retrieve_recent_context(current_message):
            return None

        try:
            raw_records = self.memory.read_recent_conversation_evidence(
                max_age_days=settings["recency_days"],
                max_records=settings["scan_limit"],
                roles={"user", "assistant"},
                exclude_session_key=session_key,
            )
            candidates = [
                RecentContextCandidate.from_evidence(record, source_location=source)
                for record, source in raw_records
            ]
            ranked = self._rank_recent_context_candidates(
                current_message,
                candidates,
                max_records=settings["max_records"],
            )
        except Exception:
            self.last_recent_context_references = []
            return None

        if not ranked:
            self.last_recent_context_references = []
            return None

        self.last_recent_context_references = [
            candidate.reference_metadata() for candidate in ranked
        ]
        return self._format_recent_context(ranked)

    @classmethod
    def _rank_recent_context_candidates(
        cls,
        current_message: str,
        candidates: list[RecentContextCandidate],
        *,
        max_records: int = 3,
    ) -> list[RecentContextCandidate]:
        query_terms = cls._recent_context_terms(current_message)
        planning_prompt = cls._is_recent_context_planning_prompt(current_message)
        ranked: list[RecentContextCandidate] = []

        for candidate in candidates:
            candidate_terms = cls._recent_context_terms(candidate.excerpt)
            overlap = len(query_terms & candidate_terms)
            recency_score = cls._recent_context_recency_score(candidate.timestamp)
            role_score = 1.0 if candidate.role == "user" else 0.25
            plan_score = 0.0
            if planning_prompt and cls._has_recent_context_future_marker(candidate.excerpt):
                plan_score = 3.0
            score = (overlap * 2.0) + recency_score + role_score + plan_score
            candidate.score = score
            candidate.score_inputs = {
                "overlap": overlap,
                "recency": round(recency_score, 3),
                "role": candidate.role,
                "future_plan_marker": plan_score > 0,
            }
            if score >= 2.0:
                ranked.append(candidate)

        ranked.sort(
            key=lambda item: (
                item.score,
                1 if item.role == "user" else 0,
                item.cursor or 0,
                item.timestamp,
            ),
            reverse=True,
        )
        return ranked[: max(max_records, 0)]

    @staticmethod
    def _format_recent_context(candidates: list[RecentContextCandidate]) -> str:
        lines = [
            "---",
            "[RECENT CONTEXT]",
            "Recent conversation evidence from prior sessions. This is short-term context, not Durable Memory.",
            "",
        ]
        for idx, candidate in enumerate(candidates, 1):
            source = ""
            if candidate.source_location is not None:
                source = candidate.source_location.file
                if candidate.source_location.line is not None:
                    source += f":{candidate.source_location.line}"
            meta_parts = [
                f"evidence_id={candidate.evidence_id}",
                f"timestamp={candidate.timestamp}",
                f"session={candidate.session_key}",
                f"role={candidate.role}",
                f"turn_id={candidate.turn_id}",
            ]
            if candidate.cursor is not None:
                meta_parts.append(f"cursor={candidate.cursor}")
            if source:
                meta_parts.append(f"source={source}")
            lines.append(f"[{idx}] " + " | ".join(meta_parts))
            lines.append(f"Excerpt: {candidate.excerpt}")
            lines.append("")
        lines.append("---")
        return "\n".join(lines)

    @classmethod
    def _should_retrieve_recent_context(cls, text: str) -> bool:
        normalized = " ".join(text.strip().casefold().split())
        if not normalized or cls._is_simple_conversation(normalized):
            return False
        if cls._is_recent_context_planning_prompt(normalized):
            return True
        explicit_markers = (
            "recent",
            "recently",
            "earlier",
            "previous",
            "last time",
            "we talked",
            "you said",
            "i mentioned",
            "that trip",
            "that plan",
            "that meeting",
            "this weekend",
            "tomorrow",
            "next week",
            "the thing",
        )
        if any(marker in normalized for marker in explicit_markers):
            return True
        pronouns = set(re.findall(r"[a-z0-9][a-z0-9_-]*", normalized))
        if pronouns & {"that", "those", "it"}:
            return True
        ambiguous_questions = (
            "what should i do",
            "what do i need",
            "do i need anything",
            "what should i bring",
            "what should i prepare",
            "how should i arrange",
        )
        return any(marker in normalized for marker in ambiguous_questions)

    @staticmethod
    def _is_recent_context_planning_prompt(text: str) -> bool:
        normalized = text.strip().casefold()
        markers = (
            "prepare",
            "bring",
            "pack",
            "packing",
            "schedule",
            "arrange",
            "reservation",
            "book",
            "booking",
            "trip",
            "travel",
            "flight",
            "hotel",
            "meeting",
            "appointment",
            "plan",
            "weekend",
        )
        return any(marker in normalized for marker in markers)

    @staticmethod
    def _has_recent_context_future_marker(text: str) -> bool:
        normalized = text.strip().casefold()
        markers = (
            "tomorrow",
            "tonight",
            "this weekend",
            "next week",
            "next month",
            "flight",
            "train",
            "hotel",
            "meeting",
            "appointment",
            "trip",
            "travel",
            "conference",
            "deadline",
            "reservation",
            "plan to",
            "going to",
            "will ",
        )
        return any(marker in normalized for marker in markers)

    @staticmethod
    def _recent_context_terms(text: str) -> set[str]:
        stopwords = {
            "about",
            "after",
            "again",
            "anything",
            "before",
            "could",
            "should",
            "that",
            "this",
            "what",
            "when",
            "where",
            "which",
            "with",
            "would",
            "you",
            "your",
        }
        return {
            term
            for term in re.findall(r"[a-z0-9][a-z0-9_-]{2,}", text.casefold())
            if term not in stopwords
        }

    @staticmethod
    def _recent_context_recency_score(timestamp: str) -> float:
        parsed = _parse_utc_timestamp(timestamp)
        if parsed is None:
            return 0.0
        age_seconds = max((datetime.utcnow() - parsed).total_seconds(), 0.0)
        age_days = age_seconds / 86400
        return max(0.0, 3.0 - min(age_days, 7.0) * (3.0 / 7.0))

    def _build_memory_recall_context(
        self,
        current_message: str,
        max_notes: int = 6,
        max_chars: int = 1_600,
    ) -> str | None:
        memory_lookup = self._is_memory_lookup_question(current_message)
        if self._is_simple_conversation(current_message) and not memory_lookup:
            return None
        if memory_lookup:
            notes = self._select_memory_lookup_notes(current_message, max_notes=max_notes)
        else:
            notes = self.memory.select_memory_recall(
                current_message,
                max_notes=max_notes,
                max_chars=max_chars,
            )
        self.last_memory_references = self.memory.memory_reference_metadata(notes)
        context = self.memory.format_memory_recall_notes_context(notes)
        return context or None

    def _select_memory_lookup_notes(self, current_message: str, *, max_notes: int) -> list[Any]:
        text = current_message.casefold()
        notes = [
            note for note in self.memory.read_notes()
            if note.status.value == "active" and note.content
        ]
        if not notes:
            return []

        wants_user_memory = any(marker in text for marker in (
            "我", "我的", "用户", "user", "my ", "remember about me",
        ))
        wants_food = any(marker in text for marker in (
            "吃", "食物", "喜欢吃", "food", "eat", "eating", "like to eat",
        ))

        candidates = []
        for note in notes:
            score = 0
            note_text = " ".join([note.content, note.type.value, note.scope.value, " ".join(note.tags)]).casefold()
            if wants_user_memory and (note.scope.value == "user" or note.type.value == "preference"):
                score += 4
            if wants_food and any(marker in note_text for marker in ("吃", "食物", "food", "eat", "eating", "strawber", "草莓")):
                score += 4
            if any(marker in text for marker in ("喜欢", "prefer", "preference", "like")) and note.type.value == "preference":
                score += 2
            if note.scope.value == "user":
                score += 1
            if score > 0:
                candidates.append(((score, note.priority, note.confidence, note.content.casefold()), note))

        candidates.sort(key=lambda item: item[0], reverse=True)
        return [note for _, note in candidates[:max_notes]]

    def _build_experience_context(
        self,
        current_message: str,
        max_experiences: int = 3,
        min_confidence: float = 0.5,
    ) -> str | None:
        if not self.experience_store:
            return None

        if self._is_simple_conversation(current_message):
            return None

        workflow_exps = self.experience_store.search_workflows(
            query=current_message,
            limit=max_experiences,
            min_confidence=min_confidence,
        )
        reference_exps = self.experience_store.search_semantic(
            query=current_message,
            outcome="resolved",
            min_confidence=min_confidence,
            limit=max_experiences * 2,
        )

        all_exps = workflow_exps + reference_exps
        seen_ids: set[str] = set()
        unique_exps: list[Any] = []
        for exp in all_exps:
            if exp.id not in seen_ids:
                seen_ids.add(exp.id)
                unique_exps.append(exp)
        unique_exps = unique_exps[:max_experiences]

        if not unique_exps:
            return None

        lines = ["---\n"]
        workflow_section = [e for e in unique_exps if e.experience_type == "workflow"]
        reference_section = [e for e in unique_exps if e.experience_type != "workflow"]

        if workflow_section:
            lines.append("[RELEVANT WORKFLOWS]\n\n")
            for exp in workflow_section:
                conf = int(exp.confidence * 100)
                lines.append(
                    f"- {exp.context_summary or exp.tool_name or 'workflow'} ({conf}% confidence)\n"
                )
                if exp.action_hint:
                    lines.append(f"  Recommended action: {exp.action_hint}\n")
                if exp.applicability:
                    lines.append(f"  Applies when: {exp.applicability}\n")
                if exp.resolution:
                    lines.append(f"  Reference: {exp.resolution}\n")
                lines.append("\n")

        if reference_section:
            lines.append("[RELEVANT RECOVERIES / REFERENCES]\n\n")
            for exp in reference_section[:max_experiences]:
                tool_label = exp.tool_name or "general"
                conf = int(exp.confidence * 100)
                lines.append(f"- {tool_label} ({conf}% confidence)\n")
                if exp.action_hint:
                    lines.append(f"  Recommended action: {exp.action_hint}\n")
                if exp.resolution:
                    lines.append(f"  Solution: {exp.resolution}\n")
                if exp.category:
                    lines.append(f"  Category: {exp.category}\n")
                lines.append("\n")

        lines.append("---")

        for exp in unique_exps[:2]:
            try:
                self.experience_store.mark_used(exp.id)
            except Exception:
                pass

        return "".join(lines)

    def _build_knowledge_context(
        self,
        current_message: str,
        max_chunks: int = 3,
        session_key: str | None = None,
        use_persistent_knowledge: bool | None = None,
    ) -> str | None:
        """Build context from persistent RAG and current-session uploads."""
        if not self.knowledge_store and not self.session_knowledge_store:
            return None

        # Check if persistent knowledge feature is enabled in config. Session
        # uploads stay available even when global knowledge auto-retrieve is off.
        persistent_enabled = self.knowledge_store is not None
        if self.config and hasattr(self.config, "knowledge"):
            persistent_enabled = (
                persistent_enabled
                and self.config.knowledge.enabled
                and self.config.knowledge.auto_retrieve
            )
            max_chunks = self.config.knowledge.max_chunks
        if use_persistent_knowledge is False:
            persistent_enabled = False

        persistent_results: list[dict[str, Any]] = []
        if (
            persistent_enabled
            and self.knowledge_store is not None
            and not self._is_simple_conversation(current_message)
        ):
            try:
                persistent_results.extend(self.knowledge_store.query(
                    query_text=current_message,
                    top_k=max_chunks,
                ))
            except Exception:
                pass

        session_results: list[dict[str, Any]] = []
        if self.session_knowledge_store is not None and session_key:
            try:
                session_results.extend(self.session_knowledge_store.context_for_session(
                    session_key,
                    current_message,
                ))
            except Exception:
                pass

        if not persistent_results and not session_results:
            return None

        return self._format_retrieved_knowledge_context(
            persistent_results=persistent_results,
            session_results=session_results,
        )

    @classmethod
    def _format_retrieved_knowledge_context(
        cls,
        *,
        persistent_results: list[dict[str, Any]],
        session_results: list[dict[str, Any]],
    ) -> str:
        lines = ["---\n[RELEVANT KNOWLEDGE]\n\n"]
        if persistent_results:
            lines.append("[Persistent knowledge base]\n")
            lines.append(cls._format_knowledge_results(persistent_results))
            lines.append("\n")
        if session_results:
            lines.append("[Current session temporary files]\n")
            lines.append(
                "These snippets came from files uploaded in this chat session only. "
                "They are not persisted and are unavailable in other sessions.\n"
            )
            lines.append(cls._format_knowledge_results(session_results))
            lines.append("\n")

        lines.append(
            "Use retrieved knowledge only when it is relevant. When citing it, mention "
            "the document name plus page or line number when available.\n---"
        )
        return "".join(lines)

    @staticmethod
    def _format_knowledge_results(results: list[dict[str, Any]]) -> str:
        lines: list[str] = []
        for idx, result in enumerate(results, 1):
            doc_name = result.get("doc_name", "Unknown")
            content = result.get("content", "")
            summary = result.get("summary", "")
            meta_parts = [f"Document: {doc_name}"]
            if injection_mode := result.get("injection_mode"):
                meta_parts.append(f"Mode: {injection_mode}")
            if file_path := result.get("file_path", ""):
                meta_parts.append(f"Path: {file_path}")
            if category := result.get("category", ""):
                meta_parts.append(f"Category: {category}")
            if section_path := result.get("section_path", ""):
                meta_parts.append(f"Section: {section_path}")
            line_start = result.get("line_start", 0)
            line_end = result.get("line_end", 0)
            if line_start and line_end:
                meta_parts.append(f"Lines: {line_start}-{line_end}")
            if result.get("page") is not None:
                meta_parts.append(f"Page: {result.get('page')}")

            semantic_parts = []
            if matched_entities := result.get("matched_entities", []):
                semantic_parts.append("Entities: " + ", ".join(matched_entities[:5]))
            if matched_relations := result.get("matched_relations", []):
                semantic_parts.append("Relations: " + "; ".join(matched_relations[:3]))
            if matched_claims := result.get("matched_claims", []):
                semantic_parts.append("Claims: " + " | ".join(matched_claims[:3]))
            if matched_child_snippets := result.get("matched_child_snippets", []):
                semantic_parts.append("Matched snippets: " + " | ".join(matched_child_snippets[:2]))

            semantic_block = "\n" + "\n".join(semantic_parts) if semantic_parts else ""
            meta_str = " | ".join(meta_parts)
            if summary:
                lines.append(f"[{idx}] {meta_str}{semantic_block}\nSummary: {summary}\nContent: {content}\n\n")
            else:
                lines.append(f"[{idx}] {meta_str}{semantic_block}\n{content}\n\n")
        return "".join(lines)

    @staticmethod
    def _is_simple_conversation(text: str) -> bool:
        text = text.strip().lower()
        # Very short messages are simple
        if len(text) < 8:
            return True

        # English simple patterns (short messages only)
        simple_en_patterns = [
            "hello",
            "hi",
            "thanks",
            "thank you",
            "ok",
            "okay",
            "bye",
            "goodbye",
            "how are you",
            "yes",
            "no",
            "sure",
        ]
        for pattern in simple_en_patterns:
            if pattern in text and len(text) < 25:
                return True

        # Chinese simple patterns (short greetings only)
        simple_cn_patterns = [
            "你好",
            "您好",
            "谢谢",
            "好的",
            "再见",
            "拜拜",
            "嗨",
            "嗨嗨",
            "没问题",
            "收到",
        ]
        for pattern in simple_cn_patterns:
            if pattern in text and len(text) < 15:
                return True

        return False

    @staticmethod
    def _is_memory_lookup_question(text: str) -> bool:
        text = text.strip().casefold()
        if not text:
            return False
        markers = [
            "我喜欢",
            "我的偏好",
            "我爱",
            "我常",
            "你记得",
            "还记得",
            "记得我",
            "关于我",
            "what do i like",
            "what's my",
            "what is my",
            "do you remember",
            "remember about me",
            "my preference",
        ]
        return any(marker in text for marker in markers)

    @staticmethod
    def _build_search_query(
        history: list[dict[str, Any]],
        current_message: str,
        max_recent: int = 2,
    ) -> str:
        recent_user_msgs = [
            m.get("content", "")[:200]
            for m in history[-6:]
            if m.get("role") == "user" and isinstance(m.get("content"), str)
        ]
        recent_assistant_msgs = [
            m.get("content", "")[:160]
            for m in history[-4:]
            if m.get("role") == "assistant" and isinstance(m.get("content"), str)
        ]
        current_text = current_message[:500].strip()
        focus_lines = [
            line.strip(" -*0123456789.")[:120]
            for line in current_message.splitlines()
            if line.strip()
        ]
        focus_lines = [line for line in focus_lines if len(line) >= 6][:3]

        parts: list[str] = []
        if recent_user_msgs:
            parts.append("Recent user context:\n" + "\n".join(recent_user_msgs[-max_recent:]))
        if recent_assistant_msgs and any(
            token in current_text.lower()
            for token in ("earlier", "previous", "continue", "summary", "recap")
        ):
            parts.append(
                "Recent assistant context:\n" + "\n".join(recent_assistant_msgs[-1:])
            )
        if focus_lines:
            parts.append("Current focus:\n" + "\n".join(focus_lines))
        if current_text:
            parts.append("Current message:\n" + current_text)
        return "\n\n".join(p for p in parts if p.strip())

    @staticmethod
    def _plan_vector_retrieval(
        history: list[dict[str, Any]],
        current_message: str,
    ) -> dict[str, int]:
        text = current_message.strip()
        complexity = 0
        if len(text) >= 120:
            complexity += 1
        if len(text) >= 260:
            complexity += 1
        if text.count("\n") >= 2 or any(
            marker in text
            for marker in ("1.", "2.", "- ", "* ", "compare", "vs", "summary", "recap")
        ):
            complexity += 1
        if sum(1 for item in history[-8:] if item.get("role") == "user") >= 4:
            complexity += 1

        max_summaries = min(4, 2 + complexity)
        max_chunks = 2 if complexity <= 1 else 3
        return {
            "max_summaries": max_summaries,
            "max_chunks_per_summary": max_chunks,
        }

    def _build_user_content(
        self, text: str, media: list[str] | None
    ) -> str | list[dict[str, Any]]:
        if not media:
            return text

        images = []
        for path in media:
            p = Path(path)
            if not p.is_file():
                continue
            raw = p.read_bytes()
            mime = detect_image_mime(raw) or mimetypes.guess_type(path)[0]
            if not mime or not mime.startswith("image/"):
                continue
            b64 = base64.b64encode(raw).decode()
            images.append(
                {
                    "type": "image_url",
                    "image_url": {"url": f"data:{mime};base64,{b64}"},
                    "_meta": {"path": str(p)},
                }
            )

        if not images:
            return text
        return images + [{"type": "text", "text": text}]

    def add_tool_result(
        self,
        messages: list[dict[str, Any]],
        tool_call_id: str,
        tool_name: str,
        result: Any,
    ) -> list[dict[str, Any]]:
        messages.append(
            {
                "role": "tool",
                "tool_call_id": tool_call_id,
                "name": tool_name,
                "content": result,
            }
        )
        return messages

    def add_assistant_message(
        self,
        messages: list[dict[str, Any]],
        content: str | None,
        tool_calls: list[dict[str, Any]] | None = None,
        reasoning_content: str | None = None,
        thinking_blocks: list[dict] | None = None,
    ) -> list[dict[str, Any]]:
        messages.append(
            build_assistant_message(
                content,
                tool_calls=tool_calls,
                reasoning_content=reasoning_content,
                thinking_blocks=thinking_blocks,
            )
        )
        return messages
