"""Chlo MCP server — tools and resources for AI agent integration."""

import json
import os
from typing import Optional

from mcp.server.fastmcp import FastMCP

from .api_client import ChloApiClient, ChloApiError

DEFAULT_API_URL = "https://orchestrate.patadev.lv"

mcp = FastMCP(
    "Chlo",
    instructions="Access your indexed codebases and context buckets from Chlo",
)


def _get_client() -> ChloApiClient:
    api_key = os.environ.get("CHLO_API_KEY", "")
    if not api_key:
        raise ValueError(
            "CHLO_API_KEY environment variable is required. "
            "Generate one at https://chlo.dev"
        )
    api_url = os.environ.get("CHLO_API_URL", DEFAULT_API_URL)
    return ChloApiClient(api_url=api_url, api_key=api_key)


def _fmt_error(e: ChloApiError) -> str:
    return f"**Error ({e.status_code}):** {e.detail}"


async def _report(
    client: ChloApiClient,
    project_id: str,
    event_type: str,
    file_path: str = "",
    summary: str = "",
    chunk_ids: list[str] | None = None,
    subgraph_title: str | None = None,
    bucket_name: str | None = None,
    plan_title: str | None = None,
    plan_content: str | None = None,
    notes: str | None = None,
) -> None:
    """Fire-and-forget activity report — failures are silently ignored."""
    try:
        await client.post_activity(
            project_id, event_type, file_path, summary,
            chunk_ids=chunk_ids,
            subgraph_title=subgraph_title,
            bucket_name=bucket_name,
            plan_title=plan_title,
            plan_content=plan_content,
            notes=notes,
        )
    except Exception:
        pass


async def _resolve_project_id(client: ChloApiClient, project_id: str) -> str:
    """Resolve a project name or ID to a UUID project_id."""
    # Already a UUID
    if len(project_id) == 36 and "-" in project_id:
        return project_id
    # Look up by name
    projects = await client.list_projects()
    for p in projects:
        if p.get("name", "") == project_id or p.get("project_id", "") == project_id:
            return p["project_id"]
    raise ChloApiError(404, f"Project '{project_id}' not found")


# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------


@mcp.tool()
async def list_projects() -> str:
    """List all your indexed projects with their IDs and names."""
    client = _get_client()
    try:
        projects = await client.list_projects()
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

    if not projects:
        return "No projects found. Index a repository at https://chlo.dev first."

    lines = [f"{len(projects)} projects:\n"]
    for p in projects:
        name = p.get("name", "Untitled")
        pid = p.get("project_id", "")
        lines.append(f"  {name}  (ID: {pid})")
    return "\n".join(lines)


@mcp.tool()
async def list_context_buckets() -> str:
    """List all context buckets with item counts and code previews."""
    client = _get_client()
    try:
        data = await client.get_buckets()
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

    buckets = data.get("buckets", [])
    if not buckets:
        return "No context buckets found. Create one at https://chlo.dev"

    active_id = data.get("activeBucketId")
    parts: list[str] = []
    for b in buckets:
        items = b.get("items", [])
        active = " (active)" if b.get("id") == active_id else ""
        parts.append(f"Bucket: {b.get('name', 'Untitled')}{active}")
        parts.append(f"  ID: {b.get('id')}  |  Items: {len(items)}")
        if items:
            for item in items[:5]:
                name = item.get("name", "")
                fpath = item.get("filePath", "")
                is_uploaded = item.get("isUploaded", False)
                item_type = item.get("type", "")
                if is_uploaded:
                    label = "image" if item_type == "uploaded-image" else "file"
                    parts.append(f"  - {name} (uploaded {label})")
                else:
                    short_path = fpath.split("/", 2)[-1] if fpath.count("/") >= 2 else fpath
                    parts.append(f"  - {name} ({short_path})")
            if len(items) > 5:
                parts.append(f"  ...and {len(items) - 5} more items")
        parts.append("")
    return "\n".join(parts)


@mcp.tool()
async def get_bucket_contents(bucket_id: str, project_id: str) -> str:
    """Get the full source code for every item in a context bucket.

    Args:
        bucket_id: The bucket ID (from list_context_buckets)
        project_id: The project ID or name (e.g. "owner/repo") that the bucket items belong to
    """
    client = _get_client()
    try:
        resolved_id = await _resolve_project_id(client, project_id)
        data = await client.get_buckets()
    except ChloApiError as e:
        await client.close()
        return _fmt_error(e)

    bucket = None
    for b in data.get("buckets", []):
        if b.get("id") == bucket_id:
            bucket = b
            break

    if not bucket:
        await client.close()
        return f"Bucket `{bucket_id}` not found."

    items = bucket.get("items", [])
    if not items:
        await client.close()
        return "Bucket is empty."

    await _report(client, resolved_id, "file_read", summary=f"Reading bucket: {bucket.get('name', 'Bucket')}")

    parts: list[str] = [f"## {bucket.get('name', 'Bucket')} — {len(items)} items\n"]
    for item in items:
        chunk_id = item.get("id", "")
        name = item.get("name", "")
        fpath = item.get("filePath", "")
        is_uploaded = item.get("isUploaded", False) or chunk_id.startswith("upload_")
        is_file = item.get("isFile", False) or chunk_id.startswith("file_")
        item_type = item.get("type", "")
        lang = fpath.rsplit(".", 1)[-1] if "." in fpath else ""

        # Uploaded files — images use server URL, text uses preview or fetched content
        if is_uploaded:
            uploaded = item.get("uploadedContent", "")
            if item_type == "uploaded-image":
                parts.append(f"### {name} (uploaded image)")
                if uploaded:
                    parts.append(f"![{name}]({uploaded})\n")
                else:
                    parts.append("(image data not available)\n")
            else:
                parts.append(f"### {name} (uploaded file)")
                ext = name.rsplit(".", 1)[-1] if "." in name else ""
                # Use codePreview for text content; uploadedContent is now a URL
                content = item.get("codePreview", "") or "(no content)"
                parts.append(f"```{ext}\n{content}\n```\n")

        # File-level snipes — use preview
        elif is_file:
            parts.append(f"### {name} — `{fpath}`")
            preview = item.get("codePreview", "")
            content = preview or "(file-level snipe — no inline content)"
            parts.append(f"```{lang}\n{content}\n```\n")

        # Code chunks — fetch full content from API
        else:
            parts.append(f"### {name} — `{fpath}`")
            try:
                chunk = await client.get_chunk(resolved_id, chunk_id)
                content = chunk.get("content", "")
                parts.append(f"```{lang}\n{content}\n```\n")
            except ChloApiError:
                preview = item.get("codePreview", "(no content)")
                parts.append(f"```\n{preview}\n```\n")

    await client.close()
    return "\n".join(parts)


@mcp.tool()
async def search_codebase(
    project_id: str,
    query: str,
    search_type: Optional[str] = None,
    limit: Optional[int] = None,
) -> str:
    """Search an indexed codebase using semantic or keyword search.

    Args:
        project_id: The project ID or name (e.g. "owner/repo") to search in
        query: Natural language or keyword query
        search_type: "semantic" (default) or "keyword"
        limit: Max results to return (default 10)
    """
    client = _get_client()
    try:
        resolved_id = await _resolve_project_id(client, project_id)
        results = await client.search(
            resolved_id, query, search_type=search_type, limit=limit
        )
        found_chunk_ids = [
            c.get("chunk", c).get("id", "")
            for c in results
            if c.get("chunk", c).get("id")
        ]
        await _report(client, resolved_id, "search", summary=f"Searching: {query}", chunk_ids=found_chunk_ids or None)
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

    if not results:
        return "No results found."

    parts: list[str] = [f"Found {len(results)} results for: {query}\n"]
    for r in results:
        # Results may be nested under "chunk" key or flat
        chunk = r.get("chunk", r)
        name = chunk.get("name", "")
        fpath = chunk.get("file", chunk.get("file_path", ""))
        score = r.get("score", r.get("similarity", 0))
        chunk_type = chunk.get("type", "")
        chunk_id = chunk.get("id", "")
        content = chunk.get("content", "")

        # Strip repo prefix from file path for readability
        short_path = fpath.split("/", 2)[-1] if fpath.count("/") >= 2 else fpath

        parts.append(f"--- {name} ({chunk_type}) ---")
        parts.append(f"File: {short_path}")
        parts.append(f"Score: {score:.1%} | ID: {chunk_id}")
        if content:
            lang = fpath.rsplit(".", 1)[-1] if "." in fpath else ""
            if len(content) > 2000:
                content = content[:2000] + "\n... (truncated)"
            parts.append(f"```{lang}\n{content}\n```")
        parts.append("")
    return "\n".join(parts)


@mcp.tool()
async def get_chunk_content(project_id: str, chunk_id: str) -> str:
    """Get the full source code of a specific chunk.

    Args:
        project_id: The project ID or name (e.g. "owner/repo")
        chunk_id: The chunk ID (from search results or similar chunks)
    """
    client = _get_client()
    try:
        resolved_id = await _resolve_project_id(client, project_id)
        chunk = await client.get_chunk(resolved_id, chunk_id)
        file_path = chunk.get("file_path", "")
        await _report(client, resolved_id, "file_read", file_path=file_path, summary=f"Reading chunk: {chunk_id}")
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

    name = chunk.get("name", "")
    fpath = chunk.get("file_path", "")
    short_path = fpath.split("/", 2)[-1] if fpath.count("/") >= 2 else fpath
    ctype = chunk.get("type", "")
    content = chunk.get("content", "")
    start = chunk.get("start_line", "?")
    end = chunk.get("end_line", "?")
    lang = fpath.rsplit(".", 1)[-1] if "." in fpath else ""

    return (
        f"{name} ({ctype})\n"
        f"File: {short_path} (lines {start}-{end})\n\n"
        f"```{lang}\n{content}\n```"
    )


@mcp.tool()
async def get_similar_chunks(
    project_id: str, chunk_id: str, limit: Optional[int] = None
) -> str:
    """Find chunks semantically similar to a given chunk.

    Args:
        project_id: The project ID or name (e.g. "owner/repo")
        chunk_id: The source chunk ID
        limit: Max results (default 5)
    """
    client = _get_client()
    try:
        resolved_id = await _resolve_project_id(client, project_id)
        results = await client.get_similar_chunks(resolved_id, chunk_id, limit=limit)
        await _report(client, resolved_id, "search", summary=f"Finding similar to {chunk_id}")
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

    if not results:
        return "No similar chunks found."

    parts: list[str] = [f"{len(results)} similar chunks:\n"]
    for r in results:
        c = r.get("chunk", r)
        sim = r.get("similarity", 0)
        name = c.get("name", "")
        fpath = c.get("file", c.get("file_path", ""))
        short_path = fpath.split("/", 2)[-1] if fpath.count("/") >= 2 else fpath
        ctype = c.get("type", "")
        cid = c.get("id", "")
        parts.append(f"  {name} ({ctype}) — {short_path}")
        parts.append(f"    Similarity: {sim:.1%} | ID: {cid}")
    return "\n".join(parts)


@mcp.tool()
async def get_project_metrics(project_id: str) -> str:
    """Get code quality metrics summary for a project.

    Args:
        project_id: The project ID or name (e.g. "owner/repo")
    """
    client = _get_client()
    try:
        resolved_id = await _resolve_project_id(client, project_id)
        m = await client.get_metrics(resolved_id)
        await _report(client, resolved_id, "other", summary="Loading project metrics")
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

    parts: list[str] = [
        "Project Metrics\n",
        f"  Files: {m.get('files_count', 0)}",
        f"  Chunks: {m.get('chunks_count', 0)}",
        f"  Edges: {m.get('edges_count', 0)}",
        "",
    ]

    # Summaries
    for metric in ("complexity", "coupling", "modularity", "dead_code", "cohesion", "instability"):
        summary = m.get(f"{metric}_summary")
        if summary:
            parts.append(
                f"  {metric.replace('_', ' ').title()}: "
                f"mean={summary.get('mean', 0):.2f}, "
                f"median={summary.get('median', 0):.2f}, "
                f"min={summary.get('min', 0):.2f}, "
                f"max={summary.get('max', 0):.2f}"
            )

    # Type distribution
    type_dist = m.get("type_distribution")
    if type_dist:
        parts.append("\nChunk types:")
        for t, count in sorted(type_dist.items(), key=lambda x: -x[1]):
            parts.append(f"  {t}: {count}")

    # Language distribution
    lang_dist = m.get("language_distribution")
    if lang_dist:
        parts.append("\nLanguages:")
        for lang, count in sorted(lang_dist.items(), key=lambda x: -x[1]):
            parts.append(f"  {lang}: {count}")

    return "\n".join(parts)


@mcp.tool()
async def find_duplicates(
    project_id: str,
    threshold: Optional[float] = None,
    limit: Optional[int] = None,
) -> str:
    """Find semantically duplicate code in a project.

    Args:
        project_id: The project ID or name (e.g. "owner/repo")
        threshold: Minimum similarity to flag as duplicate (0-1, default 0.85)
        limit: Max duplicate pairs to return (default 50)
    """
    client = _get_client()
    try:
        resolved_id = await _resolve_project_id(client, project_id)
        data = await client.get_duplicates(
            resolved_id, threshold=threshold, limit=limit
        )
        await _report(client, resolved_id, "search", summary="Finding duplicate code")
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

    dupes = data.get("duplicates", [])
    if not dupes:
        return "No duplicates found at the given threshold."

    parts: list[str] = [f"{data.get('count', len(dupes))} duplicate pairs:\n"]
    for d in dupes:
        c1 = d.get("chunk1", {})
        c2 = d.get("chunk2", {})
        sim = d.get("similarity", 0)
        f1 = c1.get("file", "")
        f2 = c2.get("file", "")
        sf1 = f1.split("/", 2)[-1] if f1.count("/") >= 2 else f1
        sf2 = f2.split("/", 2)[-1] if f2.count("/") >= 2 else f2
        parts.append(
            f"  {c1.get('name')} ({sf1})\n"
            f"    <-> {c2.get('name')} ({sf2})\n"
            f"    {sim:.1%} similar"
        )
    return "\n".join(parts)


@mcp.tool()
async def create_graph(
    project_id: str,
    title: str,
    chunk_ids: list[str],
    description: str = "",
) -> str:
    """Create a visual subgraph on Chlo from a set of code chunks.

    Use this after researching a codebase to present architectural flows,
    feature traces, or dependency maps as interactive graphs the user can
    explore.  The graph opens as a new tab in the Chlo UI.

    The description is rendered as markdown in the activity feed.
    Use **bold** for key names, bullet points for lists, and `backticks`
    for code references. Keep it concise — a short paragraph or a few
    bullet points summarizing what the graph shows and why.

    Args:
        project_id: The project ID or name (e.g. "owner/repo")
        title: Short title for the graph tab (e.g. "Auth Flow")
        chunk_ids: List of chunk IDs to include (from search results)
        description: Markdown-formatted summary shown in the activity feed
    """
    if not chunk_ids:
        return "No chunk IDs provided — search the codebase first to find relevant chunks."

    client = _get_client()
    try:
        resolved_id = await _resolve_project_id(client, project_id)
        await _report(
            client, resolved_id, "create_subgraph",
            summary=description or f"Created graph: {title}",
            chunk_ids=chunk_ids,
            subgraph_title=title,
        )
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

    return f"Graph '{title}' created with {len(chunk_ids)} chunks"


@mcp.tool()
async def add_to_context(
    project_id: str,
    chunk_ids: list[str],
    bucket_name: str = "Agent Picks",
    summary: str = "",
    notes: str = "",
) -> str:
    """Add code chunks to a context bucket on Chlo.

    Use this to save important chunks you've found during research so the
    user can review and reuse them.  Chunks appear in the Context panel.

    Args:
        project_id: The project ID or name (e.g. "owner/repo")
        chunk_ids: List of chunk IDs to add (from search results)
        bucket_name: Name of the target bucket (created if it doesn't exist)
        summary: Human-readable description shown in the activity feed
        notes: Annotation explaining why this context matters
    """
    if not chunk_ids:
        return "No chunk IDs provided."

    client = _get_client()
    try:
        resolved_id = await _resolve_project_id(client, project_id)
        await _report(
            client, resolved_id, "add_to_bucket",
            summary=summary or f"Added {len(chunk_ids)} chunks to {bucket_name}",
            chunk_ids=chunk_ids,
            bucket_name=bucket_name,
            notes=notes or None,
        )
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

    return f"Added {len(chunk_ids)} chunks to '{bucket_name}'"


@mcp.tool()
async def create_plan(
    project_id: str,
    title: str,
    content: str,
    bucket_name: str = "Plans",
) -> str:
    """Create a structured plan that persists in a context bucket on Chlo.

    Use this to create implementation plans, task breakdowns, or design
    documents that the user can review and edit in the Context panel.

    Args:
        project_id: The project ID or name (e.g. "owner/repo")
        title: Short title for the plan (e.g. "Auth Refactor Plan")
        content: Markdown-formatted plan content
        bucket_name: Target bucket name (default "Plans")
    """
    if not title or not content:
        return "Both title and content are required."

    client = _get_client()
    try:
        resolved_id = await _resolve_project_id(client, project_id)
        await _report(
            client, resolved_id, "create_plan",
            summary=f"Created plan: {title}",
            plan_title=title,
            plan_content=content,
            bucket_name=bucket_name,
        )
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

    return f"Plan '{title}' created in '{bucket_name}'"


@mcp.tool()
async def report_activity(
    project_id: str,
    event_type: str,
    file_path: str = "",
    summary: str = "",
) -> str:
    """Report agent activity for real-time visualization on Chlo's graph.

    Call this to make file nodes glow and appear in the activity feed when
    you read, edit, or reason about code.

    Args:
        project_id: The project ID or name (e.g. "owner/repo")
        event_type: "file_read", "file_edit", "file_create", "planning", "search", or "other"
        file_path: Path of the affected file (optional)
        summary: Human-readable description of what you're doing (optional)
    """
    client = _get_client()
    try:
        resolved_id = await _resolve_project_id(client, project_id)
        await client.post_activity(
            resolved_id,
            event_type=event_type,
            file_path=file_path,
            summary=summary,
        )
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()
    return "Activity reported"


# ---------------------------------------------------------------------------
# Resources
# ---------------------------------------------------------------------------


@mcp.resource("chlo://projects")
async def projects_resource() -> str:
    """All projects as JSON."""
    client = _get_client()
    try:
        projects = await client.list_projects()
    except ChloApiError as e:
        return json.dumps({"error": e.detail})
    finally:
        await client.close()
    return json.dumps(projects, indent=2)


@mcp.resource("chlo://buckets")
async def buckets_resource() -> str:
    """All context buckets as JSON."""
    client = _get_client()
    try:
        data = await client.get_buckets()
    except ChloApiError as e:
        return json.dumps({"error": e.detail})
    finally:
        await client.close()
    return json.dumps(data, indent=2)


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------


def main() -> None:
    import sys

    if len(sys.argv) > 1 and sys.argv[1] == "init":
        from .init import run_init

        # Optional --project-id flag
        project_id = None
        for i, arg in enumerate(sys.argv[2:], 2):
            if arg == "--project-id" and i + 1 < len(sys.argv):
                project_id = sys.argv[i + 1]
                break
            if arg.startswith("--project-id="):
                project_id = arg.split("=", 1)[1]
                break

        run_init(project_id=project_id)
        return

    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
