"""Project discovery agent for generating PROJECT.md."""

import json
from typing import Optional

from rich.console import Console
from rich.panel import Panel

from emdash.agent.toolkit import AgentToolkit
from emdash.agent.runner import SafeJSONEncoder
from emdash.agent.providers import get_provider
from emdash.agent.providers.factory import DEFAULT_MODEL
from emdash.templates import load_template_for_agent
from emdash.agent.events import AgentEventEmitter, EventType, NullEmitter


def get_discovery_prompt() -> str:
    """Load the project discovery system prompt from template."""
    return load_template_for_agent("project")


# Legacy constant - kept for backwards compatibility but not used
_DISCOVERY_SYSTEM_PROMPT_LEGACY = """You are a senior engineer who deeply understands codebases. Your job is to explore this codebase and write a PROJECT.md that captures its ESSENCE - not a list of everything, but the key mental models someone needs to work here effectively.

Think of it like explaining the codebase to a new teammate over coffee. What would you tell them? What's the "aha" moment when you finally get how this thing works?

EXPLORATION STRATEGY (in this order):
1. FIRST: Use get_top_commit_importance to find where the team actually spends their time
2. SECOND: Use get_recent_prs to see what's currently being worked on (open PRs) and recently completed (merged PRs)
3. THIRD: Use get_communities to see how the code naturally groups together
4. FOURTH: Use get_top_pagerank to find the technically central pieces
5. FIFTH: Use semantic_search for concepts like "main", "config", "api", "stream"
6. SIXTH: Use expand_node on the busiest/most central entities to understand their role

WHAT YOU'RE TRYING TO UNDERSTAND:
- What is this project really trying to do? (not features, the core purpose)
- What's the central idea that everything else serves?
- Where does the team spend their time and why? (from commit activity)
- What is the team working on RIGHT NOW? (from open PRs)
- What did they recently finish? (from merged PRs)
- What would confuse a new developer?
- What are the natural "neighborhoods" of code? (from communities)

HOW TO USE THE DATA:
DON'T just dump metrics. Use them to INFORM your writing:
- If a file has 65 commits from 17 authors, don't say that. Say "The team spends most of their time on X because that's where the complexity lives"
- If you see 3 communities, don't list them. Say "The code naturally groups into three areas: the core engine, the API layer, and the config/utilities"
- If something has high PageRank, explain WHY it's central: "Everything eventually flows through X because..."
- If there are open PRs, mention what the team is actively working on: "Right now the team is focused on improving X (based on open PRs)"
- If recent merges show a pattern, note it: "The team recently shipped Y - look for the new patterns there"

OUTPUT FORMAT:
Write a PROJECT.md that reads like a senior engineer explaining the codebase:

```markdown
# {Project Name}

## What This Is

2-3 sentences capturing the essence. Not a feature list - what IS this thing?

## The Main Idea

The core mental model. What's the central concept that everything revolves around?
Write this as a narrative paragraph, not bullet points.

Example: "Everything in this codebase serves one purpose: orchestrating AI agents. 
The Mastra Server is the brain - it loads recipes, connects to providers, and 
coordinates execution. When you understand that flow, everything else makes sense."

## How It Works

A narrative explanation of the main flow. Walk through what happens when the 
system does its primary job. Include a simple mermaid diagram:

```mermaid
graph LR
    A[Input] --> B[Core]
    B --> C[Output]
```

## Where The Complexity Lives

What does the team actually work on? Why is it complex? This should come from 
commit-importance data but written as insight, not statistics.

Example: "The streaming logic (`src/workflows/`) gets the most attention because 
real-time AI responses are tricky - partial responses, mid-stream errors, keeping 
the UI responsive. If you're debugging something slow, start there."

## What's Happening Now

What is the team currently working on? What did they recently ship? Look at open 
PRs and recent merges to understand the current focus.

Example: "Right now the team is adding support for multi-model conversations 
(there's an open PR for it). They recently shipped the new provider factory 
pattern - if you're adding a new LLM, that's the pattern to follow."

## The Key Players

The 3-5 most important things to understand. Not a comprehensive list - just 
the ones that matter. Explain each in context:

**The Server** (`src/server/`) - The orchestrator. Loads recipes, manages providers, 
coordinates everything. When you need to change how workflows run, start here.

**Providers** (`src/providers/`) - How we talk to LLMs. Each provider (Anthropic, 
OpenAI) has the same interface. Adding a new LLM? Create a new provider here.

## Before You Start Coding

What does a new developer need to know? The "aha" moments, the gotchas, the 
things that aren't obvious from just reading code.

- The config system is file-based, discovered at runtime
- Provider factories are the integration point - don't call SDKs directly
- Tests use mock providers, not real API calls
```

CRITICAL RULES:
- Write like you're talking to a teammate, not writing documentation
- NO comprehensive lists. Only include what matters.
- Use "we" and "you" language
- Explain WHY things are the way they are
- Specific file paths and names, but in context, not in tables
- If you can't explain something simply, explore more until you understand it

BE SELECTIVE: Better to deeply explain 5 things than shallowly list 50.
BE NARRATIVE: Paragraphs that flow, not bullet points.
BE HONEST: If something is confusing or complex, say so."""


FINALIZE_PROMPT = """Now write the PROJECT.md. Remember: you're a senior engineer explaining this codebase to a new teammate. Write with understanding, not comprehensiveness.

Structure your document like this:

# {Project Name}

## What This Is
(2-3 sentences - the essence, not a feature list)

## The Main Idea  
(The core mental model in 1-2 paragraphs. What's the central concept?)

## How It Works
(Narrative walkthrough of the main flow + a simple mermaid diagram)

## Where The Complexity Lives
(Where does the team spend time? Why? Write as insight, not statistics)

## What's Happening Now
(What's the team working on? Recent PRs, current focus areas)

## The Key Players
(Only 3-5 things that really matter, explained in context with file paths)

## Before You Start Coding
(The "aha" moments, gotchas, things that aren't obvious)

REMEMBER:
- Write like you're talking to a teammate
- NO tables or comprehensive lists - only what matters
- Use "we" and "you" language
- Explain WHY, not just WHAT

Start writing the PROJECT.md now:"""


class ProjectDiscoveryAgent:
    """Agent that discovers project structure and generates PROJECT.md."""

    # Maximum characters for tool result data to avoid context overflow
    MAX_TOOL_RESULT_SIZE = 8000

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

        Args:
            model: LLM model to use (claude-* for Anthropic, gpt-* for OpenAI)
            verbose: Whether to print progress
            max_iterations: Maximum tool call iterations
            context_threshold: Fraction of context limit to trigger summarization
            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.context_limit = self.provider.get_context_limit()
        self.console = Console()
        self.messages: list[dict] = []
        self.discoveries: list[str] = []  # Track key findings
        self.emitter = emitter or NullEmitter(agent_name="ProjectDiscoveryAgent")

    def discover(self) -> str:
        """Run discovery and generate PROJECT.md content.

        Returns:
            The generated PROJECT.md content
        """
        # Emit session start
        self.emitter.emit(EventType.SESSION_START, {
            "agent_name": "Project Discovery",
            "model": self.model,
        })
        
        if self.verbose:
            self.console.print(
                Panel(
                    "[cyan]Starting project discovery...[/cyan]\n"
                    "[dim]The agent will explore the codebase using graph analysis[/dim]",
                    title="[bold]Project Discovery[/bold]",
                    border_style="cyan",
                )
            )

        # Initialize conversation
        self.messages = [
            {"role": "system", "content": get_discovery_prompt()},
            {"role": "user", "content": "Explore this codebase and write a PROJECT.md that captures its essence. Start by finding where the team spends their time (commit importance), then understand how the code is organized (communities). Focus on understanding, not listing."},
        ]

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

        # Run exploration loop
        iterations = 0
        while iterations < self.max_iterations:
            iterations += 1

            # Check if context needs summarization
            if self._should_summarize():
                self._summarize_context()

            response = self.provider.chat(self.messages, tools=tools)
            self.messages.append(self.provider.format_assistant_message(response))

            if response.tool_calls:
                # Execute tool calls
                for tool_call in response.tool_calls:
                    result = self._execute_tool_call(tool_call)
                    result_json = json.dumps(result, cls=SafeJSONEncoder)
                    self.messages.append(
                        self.provider.format_tool_result(tool_call.id, result_json)
                    )
            else:
                # Agent finished exploring, check if we have a valid document
                content = response.content or ""
                extracted = self._extract_markdown(content)
                if self._is_valid_project_md(extracted):
                    # Emit response and session end
                    self.emitter.emit_response(extracted)
                    self.emitter.emit(EventType.SESSION_END, {"success": True})
                    return extracted
                else:
                    # Ask agent to generate the full document
                    self.messages.append({
                        "role": "user",
                        "content": FINALIZE_PROMPT,
                    })

        # Max iterations - generate final output with fresh context
        if self.verbose:
            self.console.print("[yellow]Max iterations reached, generating final document...[/yellow]")

        # Build a summary of discoveries for a clean generation context
        summary = self._build_discovery_summary()

        for attempt in range(3):
            # Use fresh context to avoid bloated conversation
            generation_messages = [
                {"role": "system", "content": get_discovery_prompt()},
                {"role": "user", "content": f"""Based on the following codebase analysis, generate a complete PROJECT.md.

## DISCOVERED INFORMATION:
{summary}

---

{FINALIZE_PROMPT}"""},
            ]

            response = self.provider.chat(generation_messages)
            content = response.content or ""
            extracted = self._extract_markdown(content)

            if self.verbose:
                # Debug: show what was generated
                preview = extracted[:200] if extracted else "(empty)"
                self.console.print(f"[dim]Raw output preview: {preview}...[/dim]")

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

            if self.verbose:
                self.console.print(f"[yellow]Attempt {attempt + 1}: Output incomplete, retrying...[/yellow]")

            # Add more explicit guidance for next attempt
            generation_messages.append({
                "role": "assistant",
                "content": content,
            })
            generation_messages.append({
                "role": "user",
                "content": """Your output was incomplete. Write the full PROJECT.md.

Start with "# {Project Name}" and include these sections:
- ## What This Is (2-3 sentences - the essence)
- ## The Main Idea (the core mental model)
- ## How It Works (narrative + mermaid diagram)
- ## Where The Complexity Lives (where does the team work?)
- ## What's Happening Now (current PRs, recent focus)
- ## The Key Players (3-5 important things explained in context)
- ## Before You Start Coding (the gotchas and "aha" moments)

Write like you're explaining to a new teammate. No tables or lists - narrative only.

Start now:""",
            })

        # Return whatever we have
        return extracted

    def _build_discovery_summary(self) -> str:
        """Build a summary of discoveries from the conversation."""
        summary_parts = []

        for msg in self.messages:
            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_result":
                            text_parts.append(block.get("content", ""))
                content = " ".join(text_parts)

            if role == "tool" and content:
                # Extract key findings from tool results
                try:
                    result = json.loads(content) if isinstance(content, str) else content
                    if isinstance(result, dict) and result.get("success"):
                        data = result.get("data", {})

                        # Extract useful summaries
                        if "results" in data:
                            results = data["results"][:5]  # Top 5
                            if results:
                                items = [f"- {r.get('identifier', r.get('name', 'unknown'))}: {r.get('type', '')}" for r in results]
                                summary_parts.append("Found entities:\n" + "\n".join(items))

                        if "communities" in data:
                            for comm in data["communities"][:3]:
                                summary_parts.append(f"Community {comm.get('id', '?')}: {comm.get('size', '?')} members")

                        if "summary" in data:
                            s = data["summary"]
                            summary_parts.append(f"File summary: {s.get('function_count', 0)} functions, {s.get('class_count', 0)} classes")

                except (json.JSONDecodeError, TypeError):
                    pass

            elif role == "assistant" and content and not msg.get("tool_calls"):
                # Extract assistant's analysis (not tool call messages)
                if len(content) > 100:
                    summary_parts.append(f"Analysis: {content[:500]}...")

        return "\n\n".join(summary_parts[:20])  # Limit to 20 summaries

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

    def _get_context_size(self) -> int:
        """Get current context size in estimated tokens."""
        total = 0
        for msg in self.messages:
            content = msg.get("content") or ""
            if isinstance(content, str):
                total += self._estimate_tokens(content)
            elif isinstance(content, list):
                for block in content:
                    if isinstance(block, dict):
                        total += self._estimate_tokens(str(block))
            if "tool_calls" in msg and msg["tool_calls"]:
                total += self._estimate_tokens(json.dumps(msg["tool_calls"], cls=SafeJSONEncoder))
        return total

    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 _summarize_context(self) -> None:
        """Summarize the conversation to reduce context size."""
        if self.verbose:
            self.console.print(
                f"  [yellow]⚡ Context compaction ({self._get_context_size()} tokens)...[/yellow]"
            )

        # Build summary of discoveries so far
        summary_messages = [
            {"role": "user", "content": self._build_conversation_text()},
        ]

        response = self.provider.chat(
            summary_messages,
            system="Summarize the key discoveries from this exploration into a concise list of findings."
        )
        summary = response.content

        # Reset messages with summary
        self.messages = [
            {"role": "system", "content": get_discovery_prompt()},
            {"role": "user", "content": "Continue exploring to understand the codebase deeply. Here's what you've learned so far:"},
            {"role": "assistant", "content": f"## What I've Learned So Far\n\n{summary}\n\nI'll keep exploring to understand the essence of this codebase before writing the PROJECT.md."},
        ]

        if self.verbose:
            self.console.print(f"  [green]✓ Compacted to {self._get_context_size()} tokens[/green]")

    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", ""))
                content = " ".join(text_parts)

            if role == "tool":
                # Summarize tool results
                lines.append(f"TOOL RESULT: {content[:500]}...")
            elif role == "assistant" and content:
                lines.append(f"DISCOVERY: {content[:300]}")

        return "\n".join(lines)

    def _truncate_data(self, data: dict) -> dict:
        """Truncate data to fit within size limits."""
        serialized = json.dumps(data, cls=SafeJSONEncoder)
        if len(serialized) <= self.MAX_TOOL_RESULT_SIZE:
            return data

        # Truncate lists in the data
        truncated = {}
        for key, value in data.items():
            if isinstance(value, list) and len(value) > 10:
                truncated[key] = value[:10]
                truncated[f"{key}_truncated"] = True
                truncated[f"{key}_total"] = len(value)
            elif isinstance(value, dict):
                truncated[key] = self._truncate_data(value)
            else:
                truncated[key] = value

        return truncated

    def _execute_tool_call(self, tool_call) -> dict:
        """Execute a tool call and return the result."""
        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 "prs" in result.data:
                summary = f"{len(result.data['prs'])} PRs"
            elif "communities" in result.data:
                summary = f"{len(result.data['communities'])} communities"
        elif not result.success:
            summary = result.error

        # Emit tool result event
        self.emitter.emit_tool_result(
            name=name,
            success=result.success,
            summary=summary,
        )

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

        # Format result for LLM with truncation
        if result.success:
            data = self._truncate_data(result.data)
            return {
                "success": True,
                "data": data,
                "suggestions": result.suggestions,
            }
        else:
            return {
                "success": False,
                "error": result.error,
                "suggestions": result.suggestions,
            }

    def _print_tool_call(self, name: str, args: dict, result):
        """Print concise tool call info."""
        status = "[green]✓[/green]" if result.success else "[red]✗[/red]"

        # Summarize args
        args_str = ""
        if args:
            key_args = []
            for k, v in list(args.items())[:2]:
                if isinstance(v, str) and len(v) > 30:
                    v = v[:30] + "..."
                key_args.append(f"{k}={v}")
            args_str = f" ({', '.join(key_args)})"

        # Summarize result
        result_str = ""
        if result.success and result.data:
            if "results" in result.data:
                result_str = f" → {len(result.data['results'])} results"
            elif "communities" in result.data:
                result_str = f" → {len(result.data['communities'])} communities"
            elif "callers" in result.data:
                result_str = f" → {result.data.get('count', len(result.data['callers']))} callers"
            elif "summary" in result.data:
                s = result.data["summary"]
                result_str = f" → {s.get('function_count', 0)} functions, {s.get('class_count', 0)} classes"

        self.console.print(f"  {status} [cyan]{name}[/cyan]{args_str}{result_str}")

    def _is_valid_project_md(self, content: str) -> bool:
        """Check if content looks like a valid PROJECT.md."""
        # Must have key sections (new structure)
        has_title = content.startswith("#") or "# " in content[:100]
        has_essence = "## What This Is" in content or "## The Main Idea" in content
        has_substance = (
            "## How It Works" in content or 
            "## Where" in content or
            "## What's Happening" in content
        )
        # Should be substantial (at least 500 chars)
        is_substantial = len(content) > 500
        return has_title and (has_essence or has_substance) and is_substantial

    def _extract_markdown(self, content: str) -> str:
        """Extract markdown content from response.

        We intentionally avoid aggressive code block extraction because the
        content may contain mermaid diagrams or other code blocks that would
        be incorrectly captured. Instead, we just return the raw content,
        letting the LLM format it properly.
        """
        # If the LLM wrapped the ENTIRE output in ```markdown ... ```, extract it
        # But only if ```markdown is at the very start (after optional whitespace)
        stripped = content.strip()
        if stripped.startswith("```markdown"):
            # Find the final closing ```
            last_fence = stripped.rfind("```")
            if last_fence > len("```markdown"):
                inner = stripped[len("```markdown"):last_fence].strip()
                # Sanity check: the inner content should look like a project doc
                if "# Project" in inner:
                    return inner

        # Otherwise return as-is - don't try to parse code blocks
        # The LLM output may contain mermaid diagrams, code examples, etc.
        return content.strip()
