"""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 _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 = ["| Project | ID |", "|---|---|"]
    for p in projects:
        name = p.get("name", "Untitled")
        pid = p.get("project_id", "")
        lines.append(f"| {name} | `{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"### {b.get('name', 'Untitled')}{active}")
        parts.append(f"**Bucket ID:** `{b.get('id')}`  |  **Items:** {len(items)}")
        if items:
            for item in items[:5]:
                name = item.get("name", "")
                fpath = item.get("filePath", "")
                preview = (item.get("codePreview", "") or "")[:120]
                parts.append(f"- **{name}** — `{fpath}`")
                if preview:
                    parts.append(f"  ```\n  {preview}\n  ```")
            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."

    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_file = item.get("isFile", False) or chunk_id.startswith("file_")
        lang = fpath.rsplit(".", 1)[-1] if "." in fpath else ""
        parts.append(f"### {name} — `{fpath}`")

        # File-level items don't have chunk content — use uploaded content or preview
        if is_file:
            uploaded = item.get("uploadedContent", "")
            preview = item.get("codePreview", "")
            content = uploaded or preview or "(file-level snipe — no inline content)"
            parts.append(f"```{lang}\n{content}\n```\n")
        else:
            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
        )
    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:
        name = r.get("name", "")
        fpath = r.get("file_path", r.get("file", ""))
        score = r.get("score", r.get("similarity", 0))
        chunk_type = r.get("type", "")
        chunk_id = r.get("id", r.get("chunk_id", ""))
        content = r.get("content", "")

        parts.append(f"#### {name} ({chunk_type}) — `{fpath}`")
        parts.append(f"Score: {score:.3f} | ID: `{chunk_id}`")
        if content:
            lang = fpath.rsplit(".", 1)[-1] if "." in fpath else ""
            # Truncate very large chunks
            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)
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

    name = chunk.get("name", "")
    fpath = chunk.get("file_path", "")
    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:** `{fpath}` (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)
    except ChloApiError as e:
        return _fmt_error(e)
    finally:
        await client.close()

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

    parts: list[str] = [f"Found **{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", ""))
        ctype = c.get("type", "")
        cid = c.get("id", "")
        parts.append(
            f"- **{name}** ({ctype}) — `{fpath}` — similarity: {sim:.3f} — 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)
    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("\n**Chunk 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("\n**Languages:**")
        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
        )
    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"Found **{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)
        parts.append(
            f"- **{c1.get('name')}** (`{c1.get('file')}`) "
            f"↔ **{c2.get('name')}** (`{c2.get('file')}`) "
            f"— {sim:.1%} similar"
        )
    return "\n".join(parts)


# ---------------------------------------------------------------------------
# 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:
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
