from __future__ import annotations

import os
import sys
from typing import Annotated

import httpx
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
from mcp.types import TextContent, ToolAnnotations
from pydantic import Field

from obris_mcp import routes
from obris_mcp.decorators import APIError, handle_api_errors

load_dotenv()

mcp = FastMCP("obris")
API_BASE = os.environ.get("OBRIS_API_URL", "https://api.obris.ai")
API_KEY = os.environ.get("OBRIS_API_KEY", "")

SOURCE_PARAM = "source"
SOURCE_MCP = "mcp"
API_TIMEOUT = 30.0
SOURCE_TYPE_LOCAL_MCP = "local-mcp"


def _api_headers() -> dict:
    return {"X-API-Key": API_KEY}


async def api_get(path: str, params: dict = None) -> dict:
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"{API_BASE}{path}",
            params=params,
            headers=_api_headers(),
            timeout=API_TIMEOUT,
        )
        _check_response(resp)
        return resp.json()


async def api_post(path: str, json: dict) -> dict:
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            f"{API_BASE}{path}",
            json=json,
            headers=_api_headers(),
            timeout=API_TIMEOUT,
        )
        _check_response(resp)
        return resp.json()


def _check_response(resp: httpx.Response) -> None:
    if resp.status_code == 401:
        raise APIError(
            "Invalid or missing API key. "
            "Ensure OBRIS_API_KEY is set in your environment."
        )
    if resp.status_code == 403:
        raise APIError(
            "Access denied. "
            "Your API key doesn't have permission for this resource."
        )
    resp.raise_for_status()


DEFAULT_PAGE_SIZE = 50


@mcp.tool(
    annotations=ToolAnnotations(title="List Topics", readOnlyHint=True, destructiveHint=False, openWorldHint=True))
@handle_api_errors
async def list_topics(
        cursor: Annotated[str, Field(description="Pagination cursor from a previous call (omit for first page).",
                                     default=None)] = None,
        limit: Annotated[int, Field(description="Max topics to return per page.", ge=1, le=100)] = DEFAULT_PAGE_SIZE,
) -> list[TextContent]:
    """List all topics available to the current user.
    Call this to help the user identify which topic they want to work within.

    If has_more is true, call again with the returned cursor to get the next page.
    """
    params: dict = {SOURCE_PARAM: SOURCE_MCP, "limit": limit}
    if cursor:
        params["cursor"] = cursor
    data = await api_get(routes.topics(), params=params)
    topics = data.get("results", [])
    if not topics:
        return [TextContent(type="text", text="No topics found.")]
    results = []
    for t in topics:
        item_count = t.get("item_count", 0)
        desc = f"\n  Description: {t['description']}" if t.get("description") else ""
        results.append(
            f"**{t['name']}**{desc}\n"
            f"- ID: {t['id']}\n"
            f"- Knowledge Count: {item_count}"
        )

    blocks = [TextContent(type="text", text="\n\n".join(results))]

    if data.get("has_more"):
        blocks.append(TextContent(
            type="text",
            text=f'Call again with cursor="{data["next_cursor"]}" to get the next page.',
        ))

    return blocks


@mcp.tool(annotations=ToolAnnotations(title="Get Topic Knowledge", readOnlyHint=True, destructiveHint=False,
                                      openWorldHint=True))
@handle_api_errors
async def get_topic_knowledge(
        topic_id: Annotated[str, Field(description="The topic ID (from list_topics).")],
        cursor: Annotated[str, Field(description="Pagination cursor from a previous call (omit for first page).",
                                     default=None)] = None,
        client_hint: Annotated[str, Field(
            description="Client type hint to optimize response size. Values: claude-code, claude, chatgpt, gemini.",
            default=None)] = None,
) -> list[TextContent]:
    """Get saved knowledge for a specific topic.
    Call list_topics first to get the topic ID.

    Small pieces of knowledge include full content inline. Larger pieces of knowledge
    and file uploads include a summary and a file URL — download the file
    to access the full content.

    If has_more is true, call again with the returned cursor to get the next page.

    Args:
        topic_id: The topic ID (from list_topics).
        cursor: Pagination cursor from a previous call (omit for first page).
        client_hint: Optional. If you know your client, pass it to optimize
            response size. Known values: "claude-code", "claude",
            "chatgpt", "gemini". Omit if unsure.
    """
    params = {SOURCE_PARAM: SOURCE_MCP, "status": "ready"}
    if client_hint:
        params["client_hint"] = client_hint
    if cursor:
        params["cursor"] = cursor
    data = await api_get(routes.topic_knowledge(topic_id), params=params)

    items = data.get("items", [])
    if not items:
        return [TextContent(type="text", text="No knowledge saved for this topic.")]

    blocks = []
    file_items = [item for item in items if item.get("file_url")]
    has_pdfs = any(item.get("mime_type") == "application/pdf" for item in file_items)

    # Emit download instructions at the top of the first page only.
    if file_items and not cursor:
        instructions = (
            "INSTRUCTIONS: You MUST download file URLs below to reference their content in the conversation. "
            "Use a tool like curl to download files, as web fetch tools may not accept MCP-provided URLs. "
            "If a download fails or isn't appearing in the UI, the user may need to allow the file hosting domain."
        )
        if has_pdfs:
            instructions += (
                " For PDFs, render pages as images and parse visually to get the most context"
                " — text extraction alone may miss important details."
            )
        blocks.append(TextContent(type="text", text=instructions))

    for item in items:
        parts = [f"### {item['title']} (id: {item['id']})"]
        if item.get("file_url"):
            if item.get("summary"):
                parts.append(item["summary"])
            parts.append(f"File: {item['file_url']}")
        elif item.get("content"):
            parts.append(item["content"])
        if item.get("url"):
            parts.append(f"(source: {item['url']})")
        blocks.append(TextContent(type="text", text="\n".join(parts)))

    if data.get("has_more"):
        shown = data.get("shown", len(items))
        total = data.get("total")
        progress = f"Shown {shown} of {total}. " if total else ""
        blocks.append(TextContent(
            type="text",
            text=f'{progress}Call again with cursor="{data["next_cursor"]}" to get the next page.',
        ))

    return blocks


@mcp.tool(
    annotations=ToolAnnotations(title="Create Topic", readOnlyHint=False, destructiveHint=False, openWorldHint=True))
@handle_api_errors
async def create_topic(
        name: Annotated[str, Field(description="Name of the topic.")],
        description: Annotated[str, Field(description="Optional description of the topic.", default="")] = "",
) -> list[TextContent]:
    """Create a new topic to organize knowledge.

    Only call this when the user explicitly asks to create a topic.
    Do not create topics automatically or preemptively.
    """
    data = await api_post(routes.topics(), json={"name": name, "description": description})

    desc = data.get("description") or "(none)"
    return [TextContent(
        type="text",
        text=f"Topic created:\n- **{data['name']}**\n- ID: {data['id']}\n- Description: {desc}",
    )]


@mcp.tool(
    annotations=ToolAnnotations(title="Add Knowledge", readOnlyHint=False, destructiveHint=False, openWorldHint=True))
@handle_api_errors
async def add_knowledge(
        topic_id: Annotated[str, Field(description="The topic ID to add knowledge to (from list_topics or create_topic).")],
        title: Annotated[str, Field(description="A one-liner describing this piece of knowledge.")],
        content: Annotated[str, Field(description="The knowledge content to save.")],
) -> list[TextContent]:
    """Save text knowledge to a topic. This is for text content only, not file uploads.

    Only call this when the user explicitly asks to save knowledge.
    Always confirm the topic (name and ID), title, and content with the user before calling this tool.
    Call list_topics or create_topic first to get the topic ID.
    """
    data = await api_post(
        routes.topic_knowledge(topic_id),
        json={"title": title, "content": content, "source_type": SOURCE_TYPE_LOCAL_MCP},
    )

    return [TextContent(
        type="text",
        text=(
            f"Knowledge saved:\n- **{data['title']}**\n"
            f"- ID: {data['id']}\n"
            f"- Topic ID: {data['topic_id']}\n"
            f"- Status: {data['status']}"
        ),
    )]


@mcp.prompt(
    name="topic_knowledge",
    title="Get Topic Knowledge",
    description="Pull knowledge from an Obris topic into the conversation",
)
async def topic_knowledge_prompt(
        topic_name: Annotated[str, Field(description="Name of the topic to pull knowledge from")],
) -> str:
    return f"Using my Obris knowledge, pull in my {topic_name} topic and use it as context for this conversation."


def main():
    print("Obris MCP server running on stdio", file=sys.stderr)
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
