"""Agent runner for LLM-powered code exploration."""

import json
import os
from datetime import datetime, date
from typing import Optional, Any, Union

from rich.console import Console
from rich.panel import Panel
from rich.markdown import Markdown
from rich.table import Table

from emdash.agent.toolkit import AgentToolkit
from emdash.agent.tools.base import ToolResult
from emdash.agent.providers import get_provider, LLMProvider
from emdash.agent.providers.factory import DEFAULT_MODEL
from emdash.agent.providers.base import ImageContent
from emdash.agent.events import AgentEventEmitter, EventType, NullEmitter
from emdash.core.config import get_config
from emdash.utils.logger import log

# Maximum size for a single tool result (in characters, ~tokens * 4)
DEFAULT_TOOL_RESULT_MAX_CHARS = int(os.getenv("EMDASH_TOOL_RESULT_MAX_CHARS", "50000"))


class SafeJSONEncoder(json.JSONEncoder):
    """JSON encoder that handles Neo4j types and other non-serializable objects."""

    def default(self, obj: Any) -> Any:
        # Handle datetime objects
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()

        # Handle Neo4j DateTime
        if hasattr(obj, 'isoformat'):
            return obj.isoformat()

        # Handle Neo4j Date, Time, etc.
        if hasattr(obj, 'to_native'):
            return str(obj.to_native())

        # Handle sets
        if isinstance(obj, set):
            return list(obj)

        # Handle bytes
        if isinstance(obj, bytes):
            return obj.decode('utf-8', errors='replace')

        # Fallback to string representation
        try:
            return str(obj)
        except Exception:
            return f"<non-serializable: {type(obj).__name__}>"


SYSTEM_PROMPT = """You are a senior software architect with direct access to a knowledge graph of a codebase.

CRITICAL RULES:
1. You MUST use the tools to explore - do NOT just describe what to do. Actually CALL the tools and analyze results.
2. NEVER give time estimates (no "2-3 days", "1 week", etc.). Focus on WHAT needs to be done, not how long.
3. Act as a strict architect: identify what ALREADY EXISTS before suggesting anything new.

Your approach:
1. FIRST: Search for existing implementations related to the user's request
2. SECOND: Expand and trace what you find to understand current state
3. THIRD: Identify gaps - what's missing vs what already exists
4. FOURTH: Be STRICT - only recommend additions that are truly necessary

When planning features or changes:
- Point out existing code that can be reused or extended
- Be specific about which files/functions already handle parts of the requirement
- Clearly separate "already implemented" from "needs to be added"
- Question whether new code is needed - prefer extending existing patterns
- If something exists, say "This is already implemented in X" rather than suggesting to build it

Available tools:

## Graph Tools (Local Code Analysis)
- semantic_search: Find code by meaning
- text_search: Find code by exact name
- expand_node: Get full context of a function/class/file
- get_callers/get_callees: Trace relationships
- get_class_hierarchy: Get inheritance tree
- get_file_dependencies: Get imports/dependents
- get_impact_analysis: Assess change impact
- get_top_pagerank: Find important code
- get_communities: Find code clusters

## GitHub MCP Tools (Live GitHub Data)
For PR analysis, file history, and code search on GitHub, use the github_* tools:
- github_list_prs: List PRs (state=open/closed/all)
- github_pr_details: Get PR info, diff, comments, reviews
- github_search_prs: Search PRs by text
- github_search_code: Search code patterns on GitHub
- github_get_file_content: Read file from GitHub
- github_get_issue: Get issue details
- github_create_review: Submit PR review (APPROVE/REQUEST_CHANGES/COMMENT)

OUTPUT FORMAT for implementation plans:
1. **Already Exists**: List existing implementations found
2. **Gaps Identified**: What's specifically missing
3. **Recommended Changes**: Minimal additions needed (no time estimates)
4. **Files to Modify**: Specific paths

Be thorough in exploration. Be strict in recommendations."""


SUMMARIZE_PROMPT = """Summarize the exploration so far into a concise context block.

Include:
1. **Original Question**: What the user asked
2. **Key Findings**: Most important discoveries (entities, files, patterns)
3. **Entities Found**: List the most relevant qualified names (max 10)
4. **Files Involved**: Key file paths discovered
5. **Current Understanding**: What we know so far
6. **Open Questions**: What still needs investigation

Be concise but preserve critical information needed to continue exploration.
Do NOT include raw JSON data - summarize the insights."""


class AgentRunner:
    """Runs an LLM agent with access to code exploration tools."""

    # Tokens per image for context estimation
    TOKENS_PER_IMAGE = 500

    def __init__(
        self,
        model: str = DEFAULT_MODEL,
        verbose: bool = True,
        max_iterations: int = 20,
        context_threshold: float = 0.6,
        tool_result_max_chars: Optional[int] = None,
        emitter: Optional[AgentEventEmitter] = None,
    ):
        """Initialize the agent runner.

        Args:
            model: LLM model to use (claude-* for Anthropic, gpt-* for OpenAI)
            verbose: Whether to print tool calls
            max_iterations: Maximum tool call iterations per query
            context_threshold: Fraction of context limit to trigger summarization (0.0-1.0).
                              Default 0.6 (60%) to leave room for tool results.
            tool_result_max_chars: Max size for tool results; 0 disables truncation.
            emitter: Event emitter for unified message stream
        """
        self.provider = get_provider(model)
        self.toolkit = AgentToolkit(enable_session=True)
        self.model = model
        self.verbose = verbose
        self.max_iterations = max_iterations
        self.context_threshold = context_threshold
        self.tool_result_max_chars = (
            DEFAULT_TOOL_RESULT_MAX_CHARS
            if tool_result_max_chars is None
            else tool_result_max_chars
        )
        self.messages: list[dict] = []
        self.console = Console()
        self.emitter = emitter or NullEmitter(agent_name="AgentRunner")

        # Pending images for the next message
        self._pending_images: list[ImageContent] = []

        # Context management
        self.context_limit = self.provider.get_context_limit()
        self.summarization_count = 0
        self.original_question: str = ""

        # Session context service (for AST-based context)
        self._context_service = None
        self._terminal_id = None
        config = get_config()
        if config.context.enabled:
            try:
                from emdash.context import ContextService
                self._context_service = ContextService()
                self._terminal_id = ContextService.get_terminal_id()
                log.info(f"Session context enabled (terminal_id={self._terminal_id[:8]}...)")
            except Exception as e:
                log.warning(f"Failed to initialize context service: {e}")

        # Initialize with system prompt
        self.messages.append({
            "role": "system",
            "content": SYSTEM_PROMPT,
        })

    def add_images(self, images: list[ImageContent]) -> None:
        """Add images to be sent with the next message.

        Args:
            images: List of images to attach
        """
        self._pending_images.extend(images)

    def clear_images(self) -> None:
        """Clear pending images."""
        self._pending_images = []

    def _estimate_tokens(self, text: str) -> int:
        """Estimate token count from text (rough approximation: 1 token \u2248 4 chars)."""
        return len(text) // 4

    def _estimate_image_tokens(self, image: ImageContent) -> int:
        """Estimate token count for an image.

        Args:
            image: ImageContent object

        Returns:
            Estimated token count
        """
        # Base estimate per image
        base_tokens = self.TOKENS_PER_IMAGE

        # Adjust based on image size (larger images have more detail)
        size_mb = len(image.image_data) / (1024 * 1024)
        size_factor = int(base_tokens * size_mb * 0.5)

        return base_tokens + size_factor

    def _get_context_size(self) -> int:
        """Get current context size in estimated tokens."""
        total = 0

        # Count tokens in messages
        for msg in self.messages:
            content = msg.get("content") or ""
            if isinstance(content, str):
                total += self._estimate_tokens(content)
            elif isinstance(content, list):
                # Handle Anthropic-style content blocks
                for block in content:
                    if isinstance(block, dict):
                        total += self._estimate_tokens(str(block))
            # Account for tool calls
            if "tool_calls" in msg and msg["tool_calls"]:
                total += self._estimate_tokens(json.dumps(msg["tool_calls"], cls=SafeJSONEncoder))

        # Count pending image tokens
        total += sum(self._estimate_image_tokens(img) for img in self._pending_images)

        return total

    def _check_vision_support(self) -> tuple[bool, str]:
        """Check if the current model supports vision.

        Returns:
            Tuple of (supports_vision, message)
        """
        if self.provider.supports_vision():
            return True, ""

        # Model doesn't support vision
        if self._pending_images:
            warning = (
                f"\n[yellow]\u26a0 Warning: Model '{self.model}' does not support images.[/yellow]\n"
                f"[dim]Images attached but will be stripped. Use a vision-capable model like "
                f"Claude (haiku/sonnet/opus) or GPT-4o for image support.[/dim]"
            )
            if self.verbose:
                self.console.print(Panel(
                    warning,
                    title="[bold yellow]Vision Not Supported[/bold yellow]",
                    border_style="yellow",
                ))
            return False, warning

        return True, ""

    def _should_summarize(self) -> bool:
        """Check if context should be summarized."""
        current_size = self._get_context_size()
        threshold = int(self.context_limit * self.context_threshold)
        return current_size > threshold

    def _get_limited_messages(self) -> list[dict]:
        """Get messages limited to max_context_messages.

        Keeps the system prompt (first message) and the last N messages
        where N is configured via EMDASH_MAX_CONTEXT_MESSAGES.

        Returns:
            List of messages to send to LLM
        """
        config = get_config()
        max_messages = config.agent.max_context_messages

        if len(self.messages) <= max_messages + 1:  # +1 for system prompt
            return self.messages

        # Keep system prompt + last N messages
        system_prompt = self.messages[0]
        recent_messages = self.messages[-(max_messages):]

        log.info(
            "Limiting messages from {} to {} (max_context_messages={})",
            len(self.messages),
            len(recent_messages) + 1,
            max_messages,
        )

        return [system_prompt] + recent_messages

    def _truncate_tool_result(self, result_json: str) -> str:
        """Truncate large tool results to prevent context overflow.

        Args:
            result_json: JSON string of tool result

        Returns:
            Truncated JSON string
        """
        if self.tool_result_max_chars <= 0:
            return result_json

        if len(result_json) <= self.tool_result_max_chars:
            return result_json

        try:
            result = json.loads(result_json)

            # Truncate data arrays
            if isinstance(result.get("data"), dict):
                data = result["data"]
                for key in ["results", "prs", "callers", "callees", "commits", "files", "communities"]:
                    if key in data and isinstance(data[key], list) and len(data[key]) > 20:
                        original_count = len(data[key])
                        data[key] = data[key][:20]
                        data[f"_{key}_truncated"] = True
                        data[f"_{key}_original_count"] = original_count

                # Truncate nested data in each item
                for key in data:
                    if isinstance(data[key], list):
                        for item in data[key]:
                            if isinstance(item, dict):
                                # Truncate long string values
                                for k, v in list(item.items()):
                                    if isinstance(v, str) and len(v) > 500:
                                        item[k] = v[:500] + "... [truncated]"
                                    # Remove verbose nested objects
                                    if isinstance(v, dict) and len(json.dumps(v, cls=SafeJSONEncoder)) > 1000:
                                        item[k] = {"_truncated": True, "summary": str(v)[:200]}

            truncated_json = json.dumps(result, cls=SafeJSONEncoder)

            # If still too large, do aggressive truncation
            if len(truncated_json) > self.tool_result_max_chars:
                truncated_json = truncated_json[:self.tool_result_max_chars] + '...[TRUNCATED]"}'

            return truncated_json

        except (json.JSONDecodeError, TypeError):
            # Fallback: simple string truncation
            return result_json[:self.tool_result_max_chars] + "...[TRUNCATED]"

    def _summarize_context(self) -> None:
        """Summarize the conversation to reduce context size."""
        if self.verbose:
            self.console.print(
                Panel(
                    f"[yellow]Context size exceeded threshold. Summarizing exploration...[/yellow]\n"
                    f"[dim]Current: ~{self._get_context_size()} tokens | Limit: {self.context_limit}[/dim]",
                    title="[bold yellow]Context Compaction[/bold yellow]",
                    border_style="yellow",
                )
            )

        # Build context for summarization
        conversation_text = self._build_conversation_text()

        # Get session state for preservation
        session_context = self.toolkit.get_session_context() or {}

        # Call LLM to summarize
        summary_messages = [
            {"role": "user", "content": f"""Original question: {self.original_question}

Conversation to summarize:
{conversation_text}

Session state:
- Entities discovered: {session_context.get('entities_discovered', 0)}
- Files visited: {len(session_context.get('visited_files', []))}
- Tool calls made: {session_context.get('steps_taken', 0)}

Provide a concise summary that preserves the key findings."""}
        ]

        response = self.provider.chat(summary_messages, system=SUMMARIZE_PROMPT)
        summary = response.content

        # Replace messages with compacted version
        # Include a user follow-up to prompt the LLM to summarize rather than re-call tools
        self.messages = [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": self.original_question},
            {"role": "assistant", "content": f"""## Exploration Summary (Compacted)

{summary}

---
*Context was compacted. Discovered {session_context.get('entities_discovered', 0)} entities across {len(session_context.get('visited_files', []))} files.*"""},
            {"role": "user", "content": "Based on the exploration summary above, please provide your findings and answer my original question. Do not call more tools - summarize what you learned."},
        ]

        self.summarization_count += 1

        if self.verbose:
            new_size = self._get_context_size()
            self.console.print(
                f"[green]✓ Context compacted: ~{new_size} tokens[/green]\n"
            )

    def _build_conversation_text(self) -> str:
        """Build text representation of conversation for summarization."""
        lines = []
        for msg in self.messages[1:]:  # Skip system prompt
            role = msg.get("role", "")
            content = msg.get("content", "")
            
            # Handle list content (Anthropic format)
            if isinstance(content, list):
                text_parts = []
                for block in content:
                    if isinstance(block, dict):
                        if block.get("type") == "text":
                            text_parts.append(block.get("text", ""))
                        elif block.get("type") == "tool_use":
                            text_parts.append(f"[Tool: {block.get('name')}]")
                content = " ".join(text_parts)

            if role == "user":
                lines.append(f"USER: {content}")
            elif role == "assistant":
                if content:
                    lines.append(f"ASSISTANT: {content[:500]}...")  # Truncate long responses
                if msg.get("tool_calls"):
                    for tc in msg["tool_calls"]:
                        func = tc.get("function", {})
                        lines.append(f"TOOL CALL: {func.get('name')} with {func.get('arguments', '{}')[:200]}")
            elif role == "tool":
                # Truncate tool results for summary
                content_preview = content[:500] if content else ""
                lines.append(f"TOOL RESULT: {content_preview}...")

        return "\n".join(lines)

    def run(
            self,
            query: str,
            images: Optional[list[ImageContent]] = None
        ) -> str:
        """Run a single query through the agent loop.

        Args:
            query: User's question about the codebase
            images: Optional list of images to send with the query

        Returns:
            The agent's response
        """
        # Emit session start
        self.emitter.emit(EventType.SESSION_START, {
            "agent_name": "Code Explorer",
            "model": self.model,
            "query": query,
            "image_count": len(images) if images else 0,
        })

        # Build system prompt with session context (pass query for re-ranking)
        system_prompt = self._build_system_prompt(query=query)

        # Reset messages for a fresh query
        self.messages = [{"role": "system", "content": system_prompt}]
        self.original_question = query
        self.summarization_count = 0

        # Set pending images if provided
        if images:
            self._pending_images = images

        response = self.chat(query)

        # Update session context with any modified files
        self._update_session_context()

        # Emit response and session end
        self.emitter.emit_response(response)
        self.emitter.emit(EventType.SESSION_END, {"success": True})

        return response

    def chat(
        self,
        message: str,
        images: Optional[list[ImageContent]] = None
    ) -> str:
        """Continue multi-turn conversation.

        Args:
            message: User's message
            images: Optional list of images to send with the message

        Returns:
            The agent's response
        """
        # Store original question if this is the first message
        if not self.original_question:
            self.original_question = message

        # Add images to pending if provided
        if images:
            self._pending_images.extend(images)

        # Check vision support and handle non-vision models
        supports_vision, warning = self._check_vision_support()

        # Get content with or without images based on model support
        content = self.provider._format_content_with_images(
            message,
            self._pending_images if supports_vision else []
        )

        # Add user message (with or without images)
        if isinstance(content, str):
            self.messages.append({
                "role": "user",
                "content": content,
            })
        else:
            # Content blocks for vision models
            self.messages.append({
                "role": "user",
                "content": content,
            })

        # Clear pending images after adding to message
        pending_count = len(self._pending_images)
        self._pending_images = []

        # Log image status
        if pending_count > 0:
            log.info(
                "Added {} images to message (vision={})",
                pending_count,
                supports_vision,
            )

        # Get tool schemas
        tools = self.toolkit.get_all_schemas()

        # Run the agent loop
        iterations = 0
        retry_after_compact = False

        while iterations < self.max_iterations:
            iterations += 1

            # Check if we need to summarize context BEFORE calling LLM
            if self._should_summarize():
                self._summarize_context()
                # After compaction, we want the LLM to summarize, not call tools again
                # The compaction adds a user message asking for summary
                retry_after_compact = False

            # Call LLM via provider with error handling for context overflow
            try:
                # Limit messages to configured max to reduce token costs
                messages_to_send = self._get_limited_messages()
                provider_name = getattr(self.provider, "_provider", None)
                provider_model = getattr(self.provider, "model", None)
                log.info(
                    "Agent request start iteration={} provider={} model={} messages={} messages_sent={} tools={} images={}",
                    iterations,
                    provider_name,
                    provider_model,
                    len(self.messages),
                    len(messages_to_send),
                    len(tools),
                    pending_count,
                )
                response = self.provider.chat(messages_to_send, tools=tools)
            except Exception as e:
                error_str = str(e).lower()
                # Handle context window exceeded error
                if "context" in error_str or "token" in error_str or "too long" in error_str or "prompt is too long" in error_str:
                    if self.verbose:
                        self.console.print(
                            f"[yellow]⚠ Context limit exceeded, compacting...[/yellow]"
                        )
                    self._summarize_context()
                    retry_after_compact = True
                    continue
                else:
                    raise

            # Add assistant message to history
            self.messages.append(self.provider.format_assistant_message(response))
            log.info(
                "Agent response received iteration={} content_len={} tool_calls={}",
                iterations,
                len(response.content or ""),
                len(response.tool_calls or []),
            )

            # Check if the model wants to use tools
            if response.tool_calls:
                # Execute each tool call
                for tool_call in response.tool_calls:
                    result = self._execute_tool_call(tool_call)
                    result_json = json.dumps(result, cls=SafeJSONEncoder)

                    # Truncate large tool results to prevent context overflow
                    result_json = self._truncate_tool_result(result_json)

                    # Add tool result to messages
                    self.messages.append(
                        self.provider.format_tool_result(tool_call.id, result_json)
                    )

                # Update session context after each iteration with tool calls
                # This captures explored entities from the tools that just ran
                self._update_session_context()

                # Check context size AFTER adding tool results
                if self._should_summarize():
                    self._summarize_context()
            else:
                # No tool calls, return the response
                log.info(
                    "Agent response final iteration={} content_len={}",
                    iterations,
                    len(response.content or ""),
                )
                return response.content or ""

        # Max iterations reached
        return (
            "I've reached the maximum number of exploration steps. "
            "Here's what I found so far based on my exploration."
        )

    def _execute_tool_call(self, tool_call) -> dict:
        """Execute a tool call and return the result.

        Args:
            tool_call: ToolCall object from provider

        Returns:
            Result dictionary for the LLM
        """
        name = tool_call.name
        try:
            args = json.loads(tool_call.arguments)
        except json.JSONDecodeError:
            args = {}

        # Emit tool start event
        self.emitter.emit_tool_start(name, args)

        # Execute the tool
        result = self.toolkit.execute(name, **args)

        # Build summary for the event
        summary = None
        if result.success and result.data:
            if "results" in result.data:
                summary = f"{len(result.data['results'])} results"
            elif "callers" in result.data:
                summary = f"{result.data.get('count', len(result.data['callers']))} callers"
            elif "callees" in result.data:
                summary = f"{result.data.get('count', len(result.data['callees']))} callees"
            elif "summary" in result.data:
                s = result.data["summary"]
                summary = f"{s.get('function_count', 0)} functions, {s.get('class_count', 0)} classes"
        elif not result.success:
            summary = result.error

        # Emit tool result event
        self.emitter.emit_tool_result(
            name=name,
            success=result.success,
            summary=summary,
            data=None,  # Don't include full data in events by default
        )

        # Print verbose output
        if self.verbose:
            self._print_tool_call(name, args, result)

        # Format result for LLM
        if result.success:
            return {
                "success": True,
                "data": result.data,
                "suggestions": result.suggestions,
            }
        else:
            return {
                "success": False,
                "error": result.error,
                "suggestions": result.suggestions,
            }

    def _print_tool_call(self, name: str, args: dict, result: ToolResult):
        """Print verbose output for a tool call.

        Args:
            name: Tool name
            args: Tool arguments
            result: Tool execution result
        """
        # Build content for the panel
        lines = []

        # Arguments
        if args:
            for key, value in args.items():
                if isinstance(value, str) and len(value) > 50:
                    value = value[:50] + "..."
                lines.append(f"[dim]{key}:[/dim] {value}")

        # Results summary
        if result.success:
            data = result.data
            if "results" in data:
                count = len(data["results"])
                lines.append(f"\n[green]Found {count} results[/green]")
                # Show first few results
                for i, r in enumerate(data["results"][:3]):
                    if isinstance(r, dict):
                        rname = r.get("name") or r.get("qualified_name", "")
                        score = r.get("score", "")
                        if score:
                            lines.append(f"  {i+1}. {rname} ({score:.2f})")
                        else:
                            lines.append(f"  {i+1}. {rname}")
                    else:
                        lines.append(f"  {i+1}. {r}")
                if count > 3:
                    lines.append(f"  ... and {count - 3} more")
            elif "callers" in data:
                count = data.get("count", len(data["callers"]))
                lines.append(f"\n[green]Found {count} callers[/green]")
            elif "callees" in data:
                count = data.get("count", len(data["callees"]))
                lines.append(f"\n[green]Found {count} callees[/green]")
            elif "summary" in data:
                summary = data["summary"]
                lines.append(f"\n[green]Expanded:[/green]")
                lines.append(f"  Functions: {summary.get('function_count', 0)}")
                lines.append(f"  Classes: {summary.get('class_count', 0)}")
                lines.append(f"  Files: {summary.get('file_count', 0)}")
            elif "plan" in data:
                plan = data["plan"]
                lines.append(f"\n[green]Strategy: {plan.get('strategy', 'unknown')}[/green]")
                steps = plan.get("steps", [])
                for step in steps[:3]:
                    lines.append(f"  {step['step']}. {step['action']}")
            else:
                # Generic success
                lines.append(f"\n[green]Success[/green]")
        else:
            lines.append(f"\n[red]Error: {result.error}[/red]")

        # Suggestions
        if result.suggestions:
            lines.append(f"\n[dim]Suggestion: {result.suggestions[0]}[/dim]")

        content = "\n".join(lines) if lines else "Executed"

        # Determine panel style
        style = "green" if result.success else "red"
        title = f"[cyan bold]{name}[/cyan bold]"

        panel = Panel(
            content,
            title=title,
            title_align="left",
            border_style=style,
            padding=(0, 1),
        )
        self.console.print(panel)

    def reset(self):
        """Reset the conversation history."""
        self.messages = [{"role": "system", "content": SYSTEM_PROMPT}]
        self.toolkit = AgentToolkit(enable_session=True)
        self.original_question = ""
        self.summarization_count = 0

    def _build_system_prompt(self, query: str | None = None) -> str:
        """Build system prompt with optional session context and custom rules.

        Args:
            query: Optional query to re-rank context by relevance

        Returns:
            System prompt string, potentially with session context and rules appended
        """
        prompt = SYSTEM_PROMPT

        # Add custom rules if available (.emdash/rules/*.md)
        from emdash.agent.rules import load_rules
        custom_rules = load_rules()
        if custom_rules:
            prompt = f"{prompt}\n\n{custom_rules}"

        if not self._context_service or not self._terminal_id:
            log.info("Session context: disabled (no context service or terminal_id)")
            return prompt

        try:
            log.info(f"Session context: loading for terminal_id={self._terminal_id[:8]}...")
            context_items = self._context_service.get_context_items(self._terminal_id)

            if not context_items:
                log.info("Session context: no items found (empty context)")
                return prompt

            # Calculate statistics
            total_score = sum(item.score for item in context_items)
            avg_score = total_score / len(context_items) if context_items else 0
            high_relevance = [i for i in context_items if i.score >= 0.8]
            med_relevance = [i for i in context_items if 0.5 <= i.score < 0.8]
            low_relevance = [i for i in context_items if i.score < 0.5]

            # Log context summary
            log.info(
                "Session context loaded: items={} total_weight={:.2f} avg_score={:.2f} "
                "high={} med={} low={}",
                len(context_items),
                total_score,
                avg_score,
                len(high_relevance),
                len(med_relevance),
                len(low_relevance),
            )

            # Log individual items with scores
            for item in context_items:
                log.info(
                    "  Context item: {} type={} score={:.2f} file={} neighbors={}",
                    item.qualified_name,
                    item.entity_type,
                    item.score,
                    item.file_path or "N/A",
                    len(item.neighbors) if item.neighbors else 0,
                )

            context_prompt = self._context_service.get_context_prompt(self._terminal_id, query=query)
            if context_prompt:
                return f"{prompt}\n\n{context_prompt}"

        except Exception as e:
            log.warning(f"Failed to get session context: {e}")

        return prompt

    def _update_session_context(self):
        """Update session context with modified files and exploration steps after run completes."""
        if not self._context_service or not self._terminal_id:
            return

        try:
            # Get modified files (for TouchedAreasProvider)
            modified_files = self._context_service.detect_modified_files()

            # Get exploration steps (for ExploredAreasProvider)
            exploration_steps = self.toolkit.get_exploration_steps()

            # Log modified files
            if modified_files:
                log.info(f"Context update: {len(modified_files)} modified files")
                for f in modified_files[:5]:
                    log.info(f"  Modified: {f}")
                if len(modified_files) > 5:
                    log.info(f"  ... and {len(modified_files) - 5} more files")

            # Log exploration steps with tool-based relevance info
            if exploration_steps:
                # Import relevance scores for logging
                from emdash.context.providers.explored_areas import ExploredAreasProvider

                tool_counts = {}
                total_entities = 0
                for step in exploration_steps:
                    tool_name = step.tool_name if hasattr(step, "tool_name") else step.get("tool_name", "unknown")
                    entities = step.entities_discovered if hasattr(step, "entities_discovered") else step.get("entities_discovered", [])
                    tool_counts[tool_name] = tool_counts.get(tool_name, 0) + 1
                    total_entities += len(entities)

                log.info(
                    "Context update: {} exploration steps, {} entities discovered",
                    len(exploration_steps),
                    total_entities,
                )

                # Log tools used with their relevance weights
                for tool_name, count in sorted(tool_counts.items(), key=lambda x: -x[1]):
                    relevance = ExploredAreasProvider.TOOL_RELEVANCE.get(tool_name, 0.2)
                    log.info(f"  Tool: {tool_name} x{count} (relevance={relevance:.2f})")

            # Update context with both sources
            if modified_files or exploration_steps:
                self._context_service.update_context(
                    terminal_id=self._terminal_id,
                    modified_files=modified_files,
                    exploration_steps=exploration_steps,
                )
        except Exception as e:
            log.warning(f"Failed to update session context: {e}")

    def get_session_summary(self) -> dict:
        """Get a summary of the current exploration session."""
        context = self.toolkit.get_session_context() or {}
        context["context_size"] = self._get_context_size()
        context["context_limit"] = self.context_limit
        context["summarization_count"] = self.summarization_count
        return context
