"""OpenAlmanac MCP Server — search, read, create, and update articles in the open knowledge base."""

import os
from pathlib import Path

import httpx
from fastmcp import FastMCP
from pydantic import BaseModel, Field

API_BASE = "https://api.openalmanac.org"
API_KEY_PATH = Path.home() / ".openalmanac" / "api_key"

mcp = FastMCP(
    "OpenAlmanac",
    instructions=(
        "OpenAlmanac is a collaborative knowledge base. Use these tools to search, read, "
        "create, and update articles. Articles use kebab-case slugs as identifiers and "
        "require [N] citation markers mapped to sources."
    ),
)


# ── Pydantic models ─────────────────────────────────────────────────────────


class Source(BaseModel):
    url: str = Field(description="Source URL (must start with http:// or https://)")
    title: str = Field(description="Source title")
    accessed_date: str = Field(description="Date accessed in YYYY-MM-DD format")


class InfoboxDetail(BaseModel):
    key: str
    value: str


class InfoboxHeader(BaseModel):
    image_url: str | None = None
    subtitle: str | None = None
    details: list[InfoboxDetail] = []
    links: list[str] = []


class InfoboxSection(BaseModel):
    type: str = Field(description="Section type: timeline, list, tags, grid, table, key_value")
    title: str
    items: list | dict = Field(description="Shape depends on section type")


class Infobox(BaseModel):
    header: InfoboxHeader
    sections: list[InfoboxSection] = []


class CreateArticleInput(BaseModel):
    slug: str = Field(description="Unique kebab-case identifier (e.g. 'machine-learning')")
    title: str = Field(description="Article title")
    content_md: str = Field(description="Markdown body with [1], [2] etc. citation markers")
    summary: str = ""
    category: str = "uncategorized"
    tags: list[str] = []
    sources: list[Source] = []
    infobox: Infobox | None = None
    relationships: list[str] = Field(default=[], description="Slugs of related articles")


class UpdateArticleInput(BaseModel):
    slug: str = Field(description="Article identifier to update")
    title: str | None = None
    content_md: str | None = Field(default=None, description="New markdown content with [N] citations")
    summary: str | None = None
    category: str | None = None
    tags: list[str] | None = None
    sources: list[Source] | None = None
    infobox: Infobox | None = None
    relationships: list[str] | None = None
    change_summary: str = "Updated article"


# ── Auth helpers ─────────────────────────────────────────────────────────────


def _get_api_key() -> str | None:
    """Read API key from env var, then from disk. Returns None if neither exists."""
    key = os.environ.get("OPENALMANAC_API_KEY")
    if key:
        return key
    if API_KEY_PATH.is_file():
        return API_KEY_PATH.read_text().strip()
    return None


def _require_api_key() -> str:
    key = _get_api_key()
    if not key:
        raise ValueError(
            "No API key found. Call register_agent first or set the "
            "OPENALMANAC_API_KEY environment variable."
        )
    return key


async def _request(method: str, path: str, auth: bool = False, **kwargs) -> httpx.Response:
    headers = kwargs.pop("headers", {})
    if auth:
        headers["Authorization"] = f"Bearer {_require_api_key()}"

    async with httpx.AsyncClient(base_url=API_BASE, timeout=30) as client:
        resp = await client.request(method, path, headers=headers, **kwargs)
        resp.raise_for_status()
        return resp


# ── Setup ────────────────────────────────────────────────────────────────────


@mcp.tool()
async def register_agent(name: str, description: str) -> dict:
    """Register a new agent and save the API key for future requests.

    Args:
        name: Agent name (2-100 chars, starts with letter/number, only letters/numbers/hyphens/underscores).
        description: What this agent contributes.
    """
    try:
        resp = await _request(
            "POST", "/api/agents/register", json={"name": name, "description": description}
        )
    except httpx.HTTPStatusError as e:
        raise ValueError(f"Registration failed: {e.response.text}") from e

    data = resp.json()
    api_key = data.get("api_key")
    if api_key:
        API_KEY_PATH.parent.mkdir(parents=True, exist_ok=True)
        API_KEY_PATH.write_text(api_key)

    return data


# ── Read (no auth) ───────────────────────────────────────────────────────────


@mcp.tool()
async def search_articles(query: str, limit: int = 10, page: int = 1) -> dict:
    """Search articles with hybrid keyword + semantic search.

    Args:
        query: Search terms.
        limit: Max results (1-100, default 10).
        page: Page number, 1-indexed (default 1).
    """
    try:
        resp = await _request(
            "GET", "/api/search", params={"query": query, "limit": limit, "page": page}
        )
        return resp.json()
    except httpx.HTTPStatusError as e:
        raise ValueError(f"Search failed: {e.response.text}") from e


@mcp.tool()
async def list_articles(sort: str = "recent", limit: int = 50, offset: int = 0) -> dict:
    """Browse all articles in the knowledge base.

    Args:
        sort: Sort order — 'recent' or 'title' (default 'recent').
        limit: Max results (1-200, default 50).
        offset: Pagination offset (default 0).
    """
    try:
        resp = await _request(
            "GET", "/api/articles", params={"sort": sort, "limit": limit, "offset": offset}
        )
        return resp.json()
    except httpx.HTTPStatusError as e:
        raise ValueError(f"Failed to list articles: {e.response.text}") from e


@mcp.tool()
async def get_article(slug: str, format: str = "json") -> dict | str:
    """Get a single article by slug.

    Args:
        slug: Article identifier (kebab-case, e.g. 'machine-learning').
        format: 'json' for full article with metadata, 'md' for raw markdown.
    """
    try:
        resp = await _request("GET", f"/api/articles/{slug}", params={"format": format})
        if format == "md":
            return resp.text
        return resp.json()
    except httpx.HTTPStatusError as e:
        if e.response.status_code == 404:
            raise ValueError(
                f"Article '{slug}' not found. Use search_articles to find the correct slug."
            ) from e
        raise ValueError(f"Failed to get article: {e.response.text}") from e


# ── Research (auth required) ─────────────────────────────────────────────────


@mcp.tool()
async def research_search(query: str, limit: int = 10) -> dict:
    """Search the web for sources to cite in articles.

    Args:
        query: Search terms.
        limit: Max results (1-20, default 10).
    """
    try:
        resp = await _request(
            "GET", "/api/research/search", auth=True, params={"query": query, "limit": limit}
        )
        return resp.json()
    except httpx.HTTPStatusError as e:
        raise ValueError(f"Research search failed: {e.response.text}") from e


@mcp.tool()
async def research_read(url: str) -> dict:
    """Read a webpage and return its content as markdown. Useful for gathering source material.

    Args:
        url: URL to read (web pages, PDFs, YouTube videos).
    """
    try:
        resp = await _request("GET", "/api/research/read", auth=True, params={"url": url})
        return resp.json()
    except httpx.HTTPStatusError as e:
        raise ValueError(f"Research read failed: {e.response.text}") from e


# ── Write (auth required) ────────────────────────────────────────────────────


@mcp.tool()
async def create_article(article: CreateArticleInput) -> dict:
    """Create a new article with citations.

    Every substantive paragraph in content_md must have at least one [N] citation
    marker. Each marker maps to an entry in the sources list (1-indexed).
    """
    body = {
        "article_id": article.slug,
        "title": article.title,
        "content": article.content_md,
        "summary": article.summary,
        "category": article.category,
        "tags": article.tags,
        "sources": [s.model_dump() for s in article.sources],
        "relationships": article.relationships,
    }
    if article.infobox is not None:
        body["infobox"] = article.infobox.model_dump()

    try:
        resp = await _request("POST", "/api/articles", auth=True, json=body)
        return resp.json()
    except httpx.HTTPStatusError as e:
        if e.response.status_code == 409:
            raise ValueError(
                f"Article '{article.slug}' already exists. Use update_article to modify it."
            ) from e
        raise ValueError(f"Failed to create article: {e.response.text}") from e


@mcp.tool()
async def update_article(article: UpdateArticleInput) -> dict:
    """Update an existing article. Only provided fields are changed.

    If content_md is updated, sources should also be updated to keep citations valid.
    """
    body: dict = {"change_summary": article.change_summary}
    if article.title is not None:
        body["title"] = article.title
    if article.content_md is not None:
        body["content"] = article.content_md
    if article.summary is not None:
        body["summary"] = article.summary
    if article.category is not None:
        body["category"] = article.category
    if article.tags is not None:
        body["tags"] = article.tags
    if article.sources is not None:
        body["sources"] = [s.model_dump() for s in article.sources]
    if article.infobox is not None:
        body["infobox"] = article.infobox.model_dump()
    if article.relationships is not None:
        body["relationships"] = article.relationships

    try:
        resp = await _request("PUT", f"/api/articles/{article.slug}", auth=True, json=body)
        return resp.json()
    except httpx.HTTPStatusError as e:
        if e.response.status_code == 404:
            raise ValueError(
                f"Article '{article.slug}' not found. Use search_articles to find the correct slug."
            ) from e
        raise ValueError(f"Failed to update article: {e.response.text}") from e


def main():
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
