import asyncio
import os
import subprocess

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

app = typer.Typer(name="clew", help="🧶 Clew - Context Engine for AI Agents", add_completion=False)
console = Console()

# Configuration
# Default to Local Engine (No Auth)
CLEW_API_URL = os.getenv("CLEW_API_URL", "http://localhost:8000/v1")
CLEW_API_KEY = os.getenv("CLEW_API_KEY")


def get_git_diff(staged: bool = False, pr: bool = False) -> str:
    """Get the diff of the current repo.

    Args:
        staged: Compare staged changes against HEAD
        pr: Compare current branch against origin/main (for PR analysis)
    """
    if pr:
        # For PRs, compare against main branch
        cmd = ["git", "diff", "origin/main...HEAD"]
    elif staged:
        cmd = ["git", "diff", "--staged"]
    else:
        cmd = ["git", "diff"]
    result = subprocess.run(cmd, capture_output=True, text=True)
    return result.stdout


def get_changed_files(diff: str) -> list[str]:
    """Parse diff to find changed files."""
    files = []
    # Simple parser: look for "diff --git a/..."
    for line in diff.splitlines():
        if line.startswith("diff --git"):
            # diff --git a/src/foo.py b/src/foo.py
            parts = line.split()
            if len(parts) >= 3:
                # b/src/foo.py is usually the last one?
                # Actually standard format is a/path b/path
                # Let's take the b/ path (target)
                path = parts[-1]
                if path.startswith("b/"):
                    files.append(path[2:])
                elif path.startswith("a/"):  # If deleted?
                    files.append(path[2:])
                else:
                    files.append(path)
    return list(set(files))


async def _search_for_node(client: httpx.AsyncClient, path: str) -> str | None:
    """Find the best matching node ID for a given path."""
    # 1. Search for Node - use path filter for exact matching
    search_resp = await client.post(
        "/search",
        json={
            "query": path,
            "limit": 5,
            "filters": {"path_contains": path},  # Uses Qdrant MatchText
        },
    )

    results = []
    if search_resp.status_code == 200:
        search_data = search_resp.json()
        results = search_data if isinstance(search_data, list) else search_data.get("results", [])

    # Fallback to semantic search if path filter returns nothing
    if not results:
        search_resp = await client.post("/search", json={"query": path, "limit": 10})
        if search_resp.status_code != 200:
            console.print(f"[red]Error searching {path}: {search_resp.status_code}[/red]")
            return None
        search_data = search_resp.json()
        results = search_data if isinstance(search_data, list) else search_data.get("results", [])

    if not results:
        console.print(f"[yellow]Could not find module {path}[/yellow]")
        return None

    # Find best path match from results
    start_id = None
    for node in results:
        node_metadata = node.get("metadata", {})
        node_path = node_metadata.get("path", "")
        # Check for exact match or suffix match (handles relative vs absolute paths)
        if node_path == path or node_path.endswith(path) or path.endswith(node_path):
            start_id = node_path
            break

    # Fallback to first result if no exact match
    if not start_id:
        node = results[0]
        node_metadata = node.get("metadata", {})
        start_id = node_metadata.get("path") or node.get("id")
        console.print(f"[yellow]No exact match for {path}, using similar: {start_id}[/yellow]")

    return start_id


async def _traverse_graph(client: httpx.AsyncClient, start_id: str) -> dict | None:
    """Traverse graph from start node."""
    trav_resp = await client.post(
        "/graph/traverse",
        json={
            "start_node_id": start_id,
            "relationship_types": ["IMPORTS", "DEFINES", "CALLS", "CONTAINS"],
        },
    )
    if trav_resp.status_code != 200:
        return None
    return trav_resp.json()


def _synthesize_output(path: str, start_id: str, graph: dict) -> dict:
    """Generate Mermaid diagram and analysis from graph data."""
    mermaid = ["```mermaid", "graph TD"]

    def clean_id(s: str) -> str:
        """Create valid Mermaid node ID from path."""
        return "".join(c for c in str(s) if c.isalnum())[:40]  # Limit length

    def get_label(n: dict) -> str:
        """BUG FIX #2: Get human-readable label, preferring path over type."""
        # Priority: id (usually path) > name > label (usually type like 'Module')
        raw = n.get("id") or n.get("name") or n.get("label") or "unknown"
        # Shorten long paths for readability
        if len(raw) > 40:
            raw = "..." + raw[-37:]
        return raw.replace('"', "'")

    # BUG FIX #3: Build node_map with readable names (use id/path, not label/type)
    node_map = {}
    for n in graph.get("nodes", []):
        # Use id (path) as the human-readable name, fallback to name, then label
        readable = n.get("id") or n.get("name") or n.get("label") or "unknown"
        node_map[n["id"]] = readable

    for n in graph.get("nodes", []):
        nid = clean_id(n["id"])
        label = get_label(n)
        if n["id"] == start_id:
            mermaid.append(f'    {nid}["{label}"]:::current')
        else:
            mermaid.append(f'    {nid}["{label}"]')

    mermaid.append("    classDef current fill:#f9f,stroke:#333,stroke-width:2px;")

    usage, dependencies = _generate_mermaid_edges(graph, mermaid, start_id, node_map, clean_id)
    mermaid.append("```")

    return {
        "path": path,
        "mermaid": "\n".join(mermaid),
        "usage": usage,
        "dependencies": dependencies,
    }


def _generate_mermaid_edges(
    graph: dict,
    mermaid: list[str],
    start_id: str,
    node_map: dict,
    clean_id: callable,
) -> tuple[list[str], list[str]]:
    """Generate Mermaid edges and extract usage/dependencies."""
    usage = []
    dependencies = []

    for edge in graph.get("edges", []):
        s = edge["source"]
        t = edge["target"]
        rel = edge["type"]

        s_name = node_map.get(s, s)
        t_name = node_map.get(t, t)

        s_id = clean_id(s)
        t_id = clean_id(t)
        mermaid.append(f"    {s_id} -->|{rel}| {t_id}")

        if s == start_id:
            if rel == "IMPORTS":
                dependencies.append(t_name)
        elif t == start_id:
            if rel == "CALLS" or rel == "IMPORTS":
                usage.append(f"{s_name} ({rel})")

    return usage, dependencies


async def analyze_file(client: httpx.AsyncClient, path: str) -> dict | None:
    """
    Analyze a specific file using Clew Engine API.
    Replicates the logic of 'explore_module' tool.
    """
    try:
        start_id = await _search_for_node(client, path)
        if not start_id:
            return None

        graph = await _traverse_graph(client, start_id)
        if not graph:
            console.print(f"[red]Error traversing {path}[/red]")
            return None

        return _synthesize_output(path, start_id, graph)

    except Exception as e:
        console.print(f"[red]Exception analyzing {path}: {e}[/red]")
        return None


async def fetch_context(files: list[str], output_format: str = "rich"):
    """Fetch and print context for files.

    Args:
        files: List of file paths to analyze
        output_format: Output format - 'rich' (terminal), 'markdown' (for GitHub), 'json'
    """
    headers = {}
    if CLEW_API_KEY:
        headers["Authorization"] = f"Bearer {CLEW_API_KEY}"

    async with httpx.AsyncClient(headers=headers, base_url=CLEW_API_URL, timeout=30.0, follow_redirects=True) as client:
        tasks = [analyze_file(client, f) for f in files]
        results = await asyncio.gather(*tasks)

        valid_results = [r for r in results if r]

        if not valid_results:
            if output_format == "markdown":
                print("No context found for changes.")
            else:
                console.print("[yellow]No context found for changes.[/yellow]")
            return


def _print_markdown_context(results: list[dict]):
    """Print results in Markdown format."""
    # Clean markdown output for GitHub PR comments
    print("*Source: Clew Engine (tree-sitter AST)*\n")

    for res in results:
        print(f"### `{res['path']}`\n")
        print(res["mermaid"])
        print("")

        if res["usage"]:
            print("**⚠️ IMPACT (Incoming Calls):**")
            for u in res["usage"]:
                print(f"- `{u}`")
            print("")
        else:
            print("✅ No incoming calls found (Low Risk).\n")

        if res["dependencies"]:
            print("<details><summary>Dependencies</summary>\n")
            for d in res["dependencies"]:
                print(f"- `{d}`")
            print("\n</details>\n")


def _get_risk_level(impact_count: int) -> str:
    """Determine risk level based on impact count."""
    return "🔴 High" if impact_count > 3 else "🟡 Medium" if impact_count > 0 else "🟢 Low"


def _print_rich_panel(res: dict, impact_count: int, dep_count: int):
    """Print the rich panel for a single file."""
    md = Markdown(res["mermaid"])
    console.print(
        Panel(
            md,
            title=f"[bold]{res['path']}[/bold]",
            subtitle=f"[dim]{impact_count} callers, {dep_count} deps[/dim]",
            border_style=("cyan" if impact_count == 0 else "yellow" if impact_count <= 3 else "red"),
        )
    )

    if res["usage"]:
        console.print("[bold red]  ⚠️  IMPACT (Incoming Calls):[/bold red]")
        for u in res["usage"]:
            console.print(f"      • {u}")
    else:
        console.print("[green]  ✅ No incoming calls found (Low Risk)[/green]")

    if res["dependencies"]:
        console.print("[blue]  📦 Dependencies:[/blue]")
        for d in res["dependencies"][:5]:  # Limit to 5
            console.print(f"      • {d}")
        if len(res["dependencies"]) > 5:
            console.print(f"      [dim]... and {len(res['dependencies']) - 5} more[/dim]")


def _print_rich_summary_table(summary_data: list[dict]):
    """Print the summary table."""
    console.print()
    table = Table(title="📊 Summary", box=None)
    table.add_column("File", style="cyan")
    table.add_column("Callers", justify="right")
    table.add_column("Deps", justify="right")
    table.add_column("Risk", justify="center")

    for item in summary_data:
        table.add_row(
            item["path"][:40] + "..." if len(item["path"]) > 40 else item["path"],
            str(item["impact"]),
            str(item["deps"]),
            item["risk"],
        )

    console.print(table)


def _print_rich_context(results: list[dict]):
    """Print results in Rich format."""
    # Rich terminal output with improved UX
    console.print()
    console.print(
        Panel.fit(
            "[bold blue]🧶 Clew Context Analysis[/bold blue]\n[dim]Source: tree-sitter AST[/dim]",
            border_style="blue",
        )
    )

    # Build summary data
    summary_data = []

    for res in results:
        impact_count = len(res["usage"])
        dep_count = len(res["dependencies"])
        risk = _get_risk_level(impact_count)

        summary_data.append({"path": res["path"], "impact": impact_count, "deps": dep_count, "risk": risk})

        # Output panel for each file
        console.print()
        _print_rich_panel(res, impact_count, dep_count)

    # Summary table
    _print_rich_summary_table(summary_data)


@app.command()
def review(
    staged: bool = typer.Option(False, "--staged", help="Analyze staged changes"),
    pr: bool = typer.Option(False, "--pr", help="Analyze PR (diff against origin/main)"),
    output: str = typer.Option("rich", "--output", help="Output format: rich, markdown"),
):
    """
    Review current changes using Clew Context.

    Examples:
        python clew_bot.py --staged           # Analyze staged changes
        python clew_bot.py --pr --output markdown  # For GitHub Actions
    """
    diff = get_git_diff(staged=staged, pr=pr)
    if not diff:
        if output == "markdown":
            print("No changes found.")
        else:
            console.print("[green]No changes found.[/green]")
        return

    files = get_changed_files(diff)
    if output == "markdown":
        print(f"Analyzing {len(files)} changed files...\n")
    else:
        console.print(f"Found {len(files)} changed files: {files}")

    # Run async loop
    asyncio.run(fetch_context(files, output_format=output))


if __name__ == "__main__":
    app()
