"""
Laterless MCP Server

Provides 6 MCP tools for AI assistants to access a user's bookmarks knowledge base.

Usage:
  LATERLESS_API_KEY=ltls_xxx mcp-server-laterless
  or
  uvx mcp-server-laterless
"""

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Annotated

import httpx
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.fastmcp.exceptions import ToolError
from mcp.types import ToolAnnotations
from pydantic import Field

from .client import LaterlessClient
from .config import get_api_key, get_api_url


# =============================================================================
# Lifespan: manage httpx client lifecycle
# =============================================================================


@asynccontextmanager
async def app_lifespan(server: FastMCP) -> AsyncIterator[dict]:
    """Create and manage the httpx client; close connection on server shutdown."""
    api_url = get_api_url()
    api_key = get_api_key()
    http_client = httpx.AsyncClient(
        base_url=api_url,
        headers={"Authorization": f"Bearer {api_key}"},
        timeout=30.0,
    )
    try:
        yield {"client": LaterlessClient(http_client=http_client)}
    finally:
        await http_client.aclose()


# Create MCP Server
mcp = FastMCP(
    "laterless",
    # Description for MCP clients to understand what this server provides
    instructions=(
        "Laterless is a personal knowledge base built from bookmarks. "
        "Use these tools to search, browse, and manage the user's bookmarks."
    ),
    lifespan=app_lifespan,
)


def _get_client(ctx: Context) -> LaterlessClient:
    """Retrieve the lifespan-managed API client from Context."""
    return ctx.request_context.lifespan_context["client"]


def _format_bookmark_list(bookmarks: list[dict]) -> str:
    """Format a list of bookmarks into human-readable text."""
    if not bookmarks:
        return "No bookmarks found."

    lines = []
    for b in bookmarks:
        title = b.get("title") or "(untitled)"
        url = b.get("url") or ""
        source = b.get("source", "")
        score = b.get("score")

        line = f"- **{title}**"
        if score is not None:
            line += f" (relevance: {score:.0%})"
        if url:
            line += f"\n  URL: {url}"
        if source:
            line += f" | Source: {source}"

        summary = b.get("summary")
        if summary:
            line += f"\n  {summary[:200]}"

        line += f"\n  ID: {b.get('id', '')}"
        lines.append(line)

    return "\n\n".join(lines)


# =============================================================================
# MCP Tools
# =============================================================================


@mcp.tool(
    name="laterless_search_bookmarks",
    annotations=ToolAnnotations(
        title="Search Bookmarks",
        readOnlyHint=True,
        destructiveHint=False,
        idempotentHint=True,
        openWorldHint=True,
    ),
)
async def search_bookmarks(
    query: str,
    limit: Annotated[int, Field(ge=1, le=50, description="Maximum results (1-50)")] = 10,
    ctx: Context = None,
) -> str:
    """
    Search bookmarks by meaning (semantic search).

    Use this when the user wants to find bookmarks related to a topic,
    concept, or question. Returns the most relevant bookmarks ranked
    by similarity.
    """
    try:
        client = _get_client(ctx)
        result = await client.search_bookmarks(query, limit=limit)
        bookmarks = result.get("bookmarks", [])

        if not bookmarks:
            return f"No bookmarks found matching '{query}'."

        header = f"Found {len(bookmarks)} bookmark(s) for '{query}':\n\n"
        return header + _format_bookmark_list(bookmarks)
    except ToolError:
        raise
    except Exception as e:
        raise ToolError(f"Unexpected error: {e}") from e


@mcp.tool(
    name="laterless_list_bookmarks",
    annotations=ToolAnnotations(
        title="List Bookmarks",
        readOnlyHint=True,
        destructiveHint=False,
        idempotentHint=True,
        openWorldHint=False,
    ),
)
async def list_bookmarks(
    limit: Annotated[int, Field(ge=1, le=100, description="Number of bookmarks (1-100)")] = 20,
    offset: Annotated[int, Field(ge=0, description="Skip this many bookmarks")] = 0,
    ctx: Context = None,
) -> str:
    """
    List recent bookmarks.

    Use this when the user wants to browse their bookmarks without
    a specific search query. Returns bookmarks sorted by most recent first.
    """
    try:
        client = _get_client(ctx)
        result = await client.list_bookmarks(limit=limit, offset=offset)
        bookmarks = result.get("bookmarks", [])
        total = result.get("total", 0)

        if not bookmarks:
            return "No bookmarks yet."

        header = f"Showing {len(bookmarks)} of {total} bookmarks"
        if offset > 0:
            header += f" (offset: {offset})"
        header += ":\n\n"

        body = _format_bookmark_list(bookmarks)

        # Pagination hint: let the agent know there are more results
        next_offset = offset + limit
        if next_offset < total:
            body += f"\n\n---\n_Has more results. Use offset={next_offset} to see next page._"

        return header + body
    except ToolError:
        raise
    except Exception as e:
        raise ToolError(f"Unexpected error: {e}") from e


@mcp.tool(
    name="laterless_get_bookmark",
    annotations=ToolAnnotations(
        title="Get Bookmark Details",
        readOnlyHint=True,
        destructiveHint=False,
        idempotentHint=True,
        openWorldHint=False,
    ),
)
async def get_bookmark(
    bookmark_id: str,
    ctx: Context = None,
) -> str:
    """
    Get full details of a specific bookmark.

    Use this when the user wants to read the complete content,
    key concepts, or metadata of a bookmark. Requires a bookmark ID
    (usually obtained from search_bookmarks or list_bookmarks).

    Args:
        bookmark_id: The bookmark's UUID
    """
    try:
        client = _get_client(ctx)
        b = await client.get_bookmark(bookmark_id)

        lines = [
            f"# {b.get('title') or '(untitled)'}",
            "",
        ]

        if b.get("url"):
            lines.append(f"**URL:** {b['url']}")
        if b.get("source"):
            lines.append(f"**Source:** {b['source']}")
        if b.get("content_author"):
            lines.append(f"**Author:** {b['content_author']}")
        if b.get("core_topic"):
            topic = b["core_topic"]
            if b.get("sub_topic"):
                topic += f" > {b['sub_topic']}"
            lines.append(f"**Topic:** {topic}")
        if b.get("key_concepts"):
            lines.append(f"**Key Concepts:** {', '.join(b['key_concepts'])}")

        lines.append("")

        if b.get("summary"):
            lines.append(f"## Summary\n{b['summary']}")
            lines.append("")

        if b.get("content"):
            lines.append(f"## Content\n{b['content']}")

        return "\n".join(lines)
    except ToolError:
        raise
    except Exception as e:
        raise ToolError(f"Unexpected error: {e}") from e


@mcp.tool(
    name="laterless_add_bookmark",
    annotations=ToolAnnotations(
        title="Add Bookmark",
        readOnlyHint=False,
        destructiveHint=False,
        idempotentHint=False,
        openWorldHint=True,
    ),
)
async def add_bookmark(
    url: Annotated[str, Field(min_length=1, description="The URL to bookmark")],
    title: str | None = None,
    ctx: Context = None,
) -> str:
    """
    Save a URL as a new bookmark.

    The bookmark will be processed by AI in the background
    (content extraction, summarization, embedding generation).
    """
    try:
        client = _get_client(ctx)
        result = await client.add_bookmark(url, title=title)

        return (
            f"Bookmark saved!\n"
            f"- ID: {result.get('id')}\n"
            f"- URL: {result.get('url')}\n"
            f"- Title: {result.get('title') or '(will be extracted)'}\n"
            f"AI processing will complete in the background."
        )
    except ToolError:
        raise
    except Exception as e:
        raise ToolError(f"Unexpected error: {e}") from e


@mcp.tool(
    name="laterless_add_brain_dump",
    annotations=ToolAnnotations(
        title="Add Brain Dump",
        readOnlyHint=False,
        destructiveHint=False,
        idempotentHint=False,
        openWorldHint=False,
    ),
)
async def add_brain_dump(
    content: Annotated[str, Field(min_length=1, description="The thought or idea to save")],
    ctx: Context = None,
) -> str:
    """
    Record a thought or idea as a brain dump.

    Use this when the user wants to save a thought, note, or idea
    that isn't a URL. The content will be analyzed by AI.
    """
    try:
        client = _get_client(ctx)
        result = await client.add_brain_dump(content)

        return (
            f"Brain dump saved!\n"
            f"- ID: {result.get('id')}\n"
            f"AI analysis will complete in the background."
        )
    except ToolError:
        raise
    except Exception as e:
        raise ToolError(f"Unexpected error: {e}") from e


@mcp.tool(
    name="laterless_get_profile",
    annotations=ToolAnnotations(
        title="Get Profile",
        readOnlyHint=True,
        destructiveHint=False,
        idempotentHint=True,
        openWorldHint=False,
    ),
)
async def get_profile(ctx: Context = None) -> str:
    """
    Get user profile and statistics.

    Shows bookmark count, chat count, connected services,
    and top interests. Useful for understanding the user's
    knowledge base at a glance.
    """
    try:
        client = _get_client(ctx)
        profile = await client.get_profile()

        lines = [f"# {profile.get('name') or 'Laterless User'}"]

        # Stats
        stats = profile.get("stats", {})
        lines.append(f"\n**Bookmarks:** {stats.get('bookmarks_count', 0)}")
        lines.append(f"**Chats:** {stats.get('chats_count', 0)}")
        lines.append(f"**Insights:** {stats.get('insights_count', 0)}")

        # Connection status
        connections = profile.get("connections", {})
        connected = [k for k, v in connections.items() if v]
        if connected:
            lines.append(f"\n**Connected:** {', '.join(connected)}")

        # Top concepts
        top_concepts = profile.get("key_concepts", [])
        if top_concepts:
            lines.append(f"\n**Top Interests:** {', '.join(top_concepts)}")

        return "\n".join(lines)
    except ToolError:
        raise
    except Exception as e:
        raise ToolError(f"Unexpected error: {e}") from e


# =============================================================================
# Entry point
# =============================================================================


def main():
    """MCP Server entry point (stdio transport)."""
    mcp.run()


if __name__ == "__main__":
    main()
