from __future__ import annotations

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

from mcp.server.fastmcp import FastMCP

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.

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.

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: str, content: str, summary: str, reason: str = "") -> dict[str, Any]:
    """Create a pending Markdown document change when durable non-secret knowledge should be added or corrected."""
    request = ProposedChangeRequest(
        kind="doc",
        target=path,
        summary=summary,
        reason=reason,
        payload={"content": content},
    )
    return _pending().create(request)


def propose_inventory_item_change(
    inventory_type: str,
    item_id: str,
    patch: dict[str, Any],
    summary: str,
    reason: str = "",
    mode: Literal["update", "create"] = "update",
) -> dict[str, Any]:
    """Create a pending structured inventory patch or new item for durable facts about infrastructure."""
    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()
