from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import FastMCP
from pydantic import Field

from .config import Settings
from .docs_repository import DocsRepository
from .freshness import build_freshness_report
from .git_adapter import CommitRequest, GitAdapter
from .inventory import InventoryRepository
from .pending_changes import PendingChangeRepository, ProposedChangeRequest
from .relationships import build_relationships
from .validation import validate_repository


MCP_INSTRUCTIONS = """
Gnexus Book is the canonical knowledge base for the owner's digital, server,
network, project, smart-home, and local infrastructure.

This MCP server is not a generic file browser. It is the preferred operational
interface for AI agents that need infrastructure context or need to maintain the
knowledge base. Use it proactively when a user request mentions, depends on, or
may be affected by documented infrastructure.

Use Gnexus Book before answering or acting when the task involves:
- hosts, VPS, VMs, servers, routers, networks, domains, ports, traffic routes, or VPNs;
- services such as GitBucket, nginx proxies, smart-home, auth, storage, media, or project deployments;
- repository/project ownership, activity, deployment mapping, or source-of-truth questions;
- credentials/access methods, but never request or store raw secrets in documentation;
- checking whether information is stale, incomplete, already documented, or safe to change;
- updating documentation after discovering new facts.

Typical read workflow:
1. search_docs(query) for natural-language lookup.
2. read_doc(path) for canonical narrative context.
3. list_inventory(inventory_type) or get_inventory_item(inventory_type, item_id) for structured facts.
4. get_relationships() when dependencies, upstreams, domains, hosts, or unresolved references matter.
5. check_freshness() and validate_repository() when making maintenance decisions.

Typical write workflow:
1. If you learn a durable infrastructure fact that is missing, stale, or wrong, update Gnexus Book in the same task whenever practical.
2. Prefer propose_doc_change or propose_inventory_item_change for non-trivial changes.
3. Use apply_pending_change to validate and apply.
4. Use commit_changes with a short factual summary.
5. For simple full-document replacements, update_doc may create, apply, validate, and commit in one call.

Inventory creation workflow (critical):
- Some inventory schemas (e.g., hosts) require a `docs` field linking to a Markdown file.
- Before creating an inventory item with a `docs` reference, use propose_doc_change to create the target Markdown document first, then apply_pending_change to apply it.
- Only after the document exists should you propose the inventory item with the `docs` path.
- Never use local filesystem tools to edit gnexus-book repository files directly — always use the MCP tools.

For propose_inventory_item_change, pass concrete top-level arguments, not a wrapper:
inventory_type="hosts", item_id="bserv", mode="create", patch={"ip_address": "192.168.1.130", "user": "bserv"}, summary="Document bserv host".
The patch object is the inventory item fields to create or update.

Knowledge maintenance triggers:
- new host, VM, domain, endpoint, route, service, repository, device, credential reference, or dependency;
- changed IP address, port, upstream, status, operating system, version, owner, deployment path, or last-reviewed state;
- live observation contradicts documentation;
- user provides infrastructure facts in chat;
- agent discovers facts while connected to hosts, routers, APIs, GitBucket, or local services.

If a discovered fact is useful beyond the current chat and is not secret, it belongs in Gnexus Book. If you decide not to update it, say why.

Rules:
- Do not store raw passwords, tokens, private keys, recovery codes, session cookies, or secret config values.
- IP addresses, hostnames, usernames, domains, routes, ports, and credential references may be documented when useful.
- Treat Git history as the rollback mechanism; still keep changes focused and commit summaries clear.
- If documentation conflicts with live observations, say so and update the knowledge base when appropriate.
- If the user asks about infrastructure and you have this MCP server available, do not wait for the user to say "look in the knowledge base"; consult it yourself.
""".strip()


AGENT_USAGE_GUIDE = """
Gnexus Book MCP quick guide for agents:

Mission:
- Treat this MCP server as the owner's infrastructure memory.
- Use it before answering infrastructure-specific questions.
- Keep it current when durable facts are discovered.

Use it proactively for:
- hosts, VMs, VPS, routers, LANs, VPNs, domains, ports, routes, proxies;
- deployed services, smart-home, GitBucket repositories, project activity, source/deployment mapping;
- freshness, missing documentation, stale records, conflicting observations.

Read path:
1. search_docs(query)
2. read_doc(path)
3. list_inventory(inventory_type) or get_inventory_item(inventory_type, item_id)
4. get_relationships() for dependencies and routes
5. check_freshness() if age matters

Write path:
1. If a fact is durable, useful later, and non-secret, update docs/inventory.
2. Use propose_doc_change or propose_inventory_item_change for reviewable changes.
3. Use apply_pending_change, then validate_repository.
4. Use commit_changes with a concise factual summary.
5. Use update_doc only for simple full-document updates.

Inventory creation rule:
- When an inventory item requires a `docs` field, create the Markdown document first via propose_doc_change + apply_pending_change, then propose the inventory item.
- Never edit gnexus-book files directly through filesystem or terminal — always go through MCP tools.

Inventory write call shape:
- propose_inventory_item_change(inventory_type, item_id, patch, summary, reason?, mode?)
- Do not wrap arguments in params.
- For create mode, patch is the new inventory item fields.
- For update mode, patch is the fields to merge into the existing item.

Do document:
- hostnames, usernames, IPs, domains, ports, routes, access models, credential references, versions, repository activity.

Do not document:
- raw passwords, tokens, private keys, recovery codes, session cookies, secret config values.

End-of-task maintenance check:
- Did I learn a new durable infrastructure fact?
- Did I find stale or conflicting documentation?
- Did I update Gnexus Book or explicitly explain why not?
""".strip()


def _settings() -> Settings:
    repo_root = os.environ.get("GNEXUS_BOOK_REPO_ROOT")
    return Settings() if not repo_root else Settings(repo_root=Path(repo_root))


def _docs() -> DocsRepository:
    return DocsRepository(_settings())


def _inventory() -> InventoryRepository:
    return InventoryRepository(_settings())


def _pending() -> PendingChangeRepository:
    return PendingChangeRepository(_settings())


def _git() -> GitAdapter:
    return GitAdapter(_settings())


def _json(data: Any) -> str:
    return json.dumps(data, ensure_ascii=False, indent=2)


def get_agent_usage_guide() -> dict[str, str]:
    """Read the mandatory quick guide that tells agents when and how to use Gnexus Book MCP."""
    return {"guide": AGENT_USAGE_GUIDE}


def search_docs(query: str) -> list[dict[str, object]]:
    """Search canonical Markdown docs. Use this first for broad infrastructure lookup."""
    return _docs().search(query)


def read_doc(path: str) -> dict[str, object]:
    """Read one canonical Markdown document by repository-relative path after search/list discovery."""
    return _docs().read_doc(path)


def list_docs() -> list[dict[str, object]]:
    """List all canonical Markdown docs with titles and frontmatter."""
    return _docs().list_docs()


def list_inventory(inventory_type: str | None = None) -> object:
    """List inventory types or read parsed structured inventory such as hosts, services, domains, endpoints, projects, networks, hardware, or virtual-machines."""
    repo = _inventory()
    if inventory_type:
        return repo.read_parsed(inventory_type)
    return repo.list_types()


def get_inventory_item(inventory_type: str, item_id: str) -> dict[str, Any]:
    """Read one structured inventory record by type and id when you know the canonical id."""
    return _inventory().read_item(inventory_type, item_id)


def get_relationships() -> dict[str, object]:
    """Build the inventory relationship graph for dependencies, domains, upstreams, hosts, routes, and unresolved references."""
    return build_relationships(_inventory())


def validate_repository_tool() -> dict[str, object]:
    """Validate schemas, docs, inventory links, duplicate ids, and raw secret patterns before trusting or committing changes."""
    return validate_repository(_settings())


def check_freshness() -> dict[str, object]:
    """Return freshness/staleness report for deciding what documentation needs review."""
    return build_freshness_report(_settings())


def git_status() -> dict[str, Any]:
    """Return local Git status before editing or committing documentation changes."""
    return _git().status()


def git_diff(files: list[str] | None = None) -> dict[str, str]:
    """Return local Git diff, optionally limited to repository-relative files, for reviewing documentation changes."""
    return _git().diff(files)


def list_pending_changes() -> list[dict[str, Any]]:
    """List pending/applied/rejected knowledge-change records created by agents."""
    return _pending().list()


def propose_doc_change(
    path: Annotated[str, Field(description="Repository-relative Markdown path to create or replace, for example '10-systems/applications/gitbucket.md'.")],
    content: Annotated[str, Field(description="Full Markdown document content, including frontmatter when the target document uses it.")],
    summary: Annotated[str, Field(description="Short factual change summary used for the pending change and later commit message.")],
    reason: Annotated[str, Field(description="Why this change is needed; mention the source of the fact when useful.")] = "",
) -> dict[str, Any]:
    """Create a pending Markdown document change. Pass path, content, summary, and optional reason as top-level arguments."""
    request = ProposedChangeRequest(
        kind="doc",
        target=path,
        summary=summary,
        reason=reason,
        payload={"content": content},
    )
    return _pending().create(request)


def propose_inventory_item_change(
    inventory_type: Annotated[str, Field(description="Inventory collection name, for example 'hosts', 'services', 'domains', 'networks', 'hardware', or 'virtual-machines'.")],
    item_id: Annotated[str, Field(description="Canonical inventory item id. For a host, use the stable host id such as 'bserv', not the IP address.")],
    patch: Annotated[dict[str, Any], Field(description="Inventory fields to create or update. For mode='create', this object is the new item body. Do not wrap it in 'inventory_item' or 'params'.")],
    summary: Annotated[str, Field(description="Short factual summary of the inventory change.")],
    reason: Annotated[str, Field(description="Why this inventory fact should be persisted; mention the source when useful.")] = "",
    mode: Annotated[Literal["update", "create"], Field(description="'create' for a new inventory item; 'update' to patch an existing item.")] = "update",
) -> dict[str, Any]:
    """Create a pending inventory change. Use top-level arguments: inventory_type, item_id, patch, summary, reason, mode."""
    request = ProposedChangeRequest(
        kind="inventory-item",
        target=f"{inventory_type}/{item_id}",
        summary=summary,
        reason=reason,
        payload={"mode": mode, "patch": patch},
    )
    return _pending().create(request)


def apply_pending_change(change_id: str) -> dict[str, Any]:
    """Apply one pending knowledge change and roll it back automatically if validation fails."""
    return _pending().apply(change_id)


def commit_changes(summary: str, files: list[str], details: str = "") -> dict[str, Any]:
    """Validate the repository and create a local Git commit for selected documentation files."""
    return _git().commit(CommitRequest(summary=summary, details=details, files=files))


def update_doc(path: str, content: str, summary: str, reason: str = "") -> dict[str, Any]:
    """For simple full-document updates: create pending change, apply, validate, and commit in one call."""
    change = propose_doc_change(path=path, content=content, summary=summary, reason=reason)
    applied = apply_pending_change(str(change["id"]))
    pending_path = f"90-maintenance/pending-changes/{change['id']}.json"
    commit = commit_changes(summary=summary, files=[path, pending_path], details=reason)
    return {"change": applied, "commit": commit}


mcp = FastMCP(
    "gnexus-book",
    instructions=MCP_INSTRUCTIONS,
)


mcp.tool(name="search_docs")(search_docs)
mcp.tool(name="get_agent_usage_guide")(get_agent_usage_guide)
mcp.tool(name="read_doc")(read_doc)
mcp.tool(name="list_docs")(list_docs)
mcp.tool(name="list_inventory")(list_inventory)
mcp.tool(name="get_inventory_item")(get_inventory_item)
mcp.tool(name="get_relationships")(get_relationships)
mcp.tool(name="validate_repository")(validate_repository_tool)
mcp.tool(name="check_freshness")(check_freshness)
mcp.tool(name="git_status")(git_status)
mcp.tool(name="git_diff")(git_diff)
mcp.tool(name="list_pending_changes")(list_pending_changes)
mcp.tool(name="propose_doc_change")(propose_doc_change)
mcp.tool(name="propose_inventory_item_change")(propose_inventory_item_change)
mcp.tool(name="apply_pending_change")(apply_pending_change)
mcp.tool(name="commit_changes")(commit_changes)
mcp.tool(name="update_doc")(update_doc)


@mcp.resource("gnexus-book://docs", mime_type="application/json")
def docs_resource() -> str:
    return _json(list_docs())


@mcp.resource("gnexus-book://agent-guide", mime_type="text/markdown")
def agent_guide_resource() -> str:
    return AGENT_USAGE_GUIDE


@mcp.resource("gnexus-book://docs/{path}", mime_type="application/json")
def doc_resource(path: str) -> str:
    return _json(read_doc(path))


@mcp.resource("gnexus-book://inventory", mime_type="application/json")
def inventory_resource() -> str:
    return _json(list_inventory())


@mcp.resource("gnexus-book://inventory/{inventory_type}", mime_type="application/json")
def inventory_type_resource(inventory_type: str) -> str:
    return _json(list_inventory(inventory_type))


@mcp.resource("gnexus-book://relationships", mime_type="application/json")
def relationships_resource() -> str:
    return _json(get_relationships())


@mcp.resource("gnexus-book://freshness", mime_type="application/json")
def freshness_resource() -> str:
    return _json(check_freshness())


@mcp.resource("gnexus-book://validation", mime_type="application/json")
def validation_resource() -> str:
    return _json(validate_repository_tool())


@mcp.resource("gnexus-book://git/status", mime_type="application/json")
def git_status_resource() -> str:
    return _json(git_status())


def main() -> None:
    transport = os.environ.get("GNEXUS_BOOK_MCP_TRANSPORT", "stdio")
    if transport not in {"stdio", "sse", "streamable-http"}:
        raise SystemExit("GNEXUS_BOOK_MCP_TRANSPORT must be stdio, sse, or streamable-http")
    mcp.run(transport=transport)  # type: ignore[arg-type]


if __name__ == "__main__":
    main()
