"""MCP server for mesh credential vault.

Exposes vault operations as MCP tools that AI agents and integrations can call.
Runs on port 8403 by default.

All endpoints require ``admin`` role authentication.  Credential retrieval
additionally checks node-scope authorisation and is rate-limited.
"""
from __future__ import annotations

import logging
import time
from collections import defaultdict
from datetime import datetime, timezone
from typing import Dict, Optional

from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
from pydantic import BaseModel, Field

from mesh.vault.models import (
    CloudProvider,
    CredentialEntry,
    CredentialGrant,
    CredentialRequest,
    CredentialScope,
    ServiceRequirement,
    VaultStatus,
)
from mesh.vault.policies import POLICIES, get_policy
from mesh.vault.store import VaultStore

logger = logging.getLogger(__name__)

# ── Rate limiter ──────────────────────────────────────────────────────────────

class _RateLimiter:
    """Simple sliding-window rate limiter keyed by credential_id."""

    def __init__(self, max_per_minute: int = 10):
        self._max = max_per_minute
        self._windows: Dict[str, list[float]] = defaultdict(list)

    def check(self, key: str) -> bool:
        now = time.time()
        window = self._windows[key]
        # Prune entries older than 60s
        self._windows[key] = [t for t in window if now - t < 60]
        if len(self._windows[key]) >= self._max:
            return False
        self._windows[key].append(now)
        return True


# ── MCP request/response models ──────────────────────────────────────────────

class MCPTool(BaseModel):
    name: str
    description: str
    parameters: Dict = Field(default_factory=dict)


class MCPManifest(BaseModel):
    name: str = "mesh-vault"
    version: str = "0.1.0"
    description: str = "Secure credential vault for the mesh production system"
    tools: list[MCPTool] = Field(default_factory=list)


class StoreRequest(BaseModel):
    name: str
    provider: str
    service: str
    scope: str = "global"
    allowed_nodes: list[str] = Field(default_factory=list)
    fields: Dict[str, str]
    expires_at: Optional[str] = None


class GetRequest(BaseModel):
    credential_id: str
    node_id: str


class RotateRequest(BaseModel):
    credential_id: str
    new_fields: Dict[str, str]


class DeleteRequest(BaseModel):
    credential_id: str


class PolicyRequest(BaseModel):
    provider: str
    service: str


class CheckRequirementsRequest(BaseModel):
    service: str


class GrantRequest(BaseModel):
    credential_id: str
    node_id: str


# ── Vault state (module-level, wired at startup) ─────────────────────────────

_vault_store: Optional[VaultStore] = None
_rate_limiter = _RateLimiter()
_sealed = True


def wire_vault(store: VaultStore, max_access_rate: int = 10) -> None:
    """Wire in the vault store after initialization."""
    global _vault_store, _rate_limiter, _sealed
    _vault_store = store
    _rate_limiter = _RateLimiter(max_per_minute=max_access_rate)
    _sealed = False


def seal_vault() -> None:
    """Seal the vault -- all operations will be rejected until unseal."""
    global _sealed
    _sealed = True
    logger.info("Vault sealed")


def unseal_vault() -> None:
    """Unseal the vault."""
    global _sealed
    if _vault_store is None:
        raise RuntimeError("Vault store not initialised")
    _sealed = False
    logger.info("Vault unsealed")


def _require_unsealed():
    if _sealed:
        raise HTTPException(503, "Vault is sealed. Unseal before performing operations.")
    if _vault_store is None:
        raise HTTPException(503, "Vault not initialised")


# ── MCP Router ────────────────────────────────────────────────────────────────

router = APIRouter(prefix="/mcp", tags=["vault-mcp"])


@router.get("/manifest")
async def mcp_manifest() -> MCPManifest:
    """Return the MCP tool manifest."""
    tools = [
        MCPTool(
            name="list_credentials",
            description="List all stored credentials (metadata only, no secrets)",
            parameters={},
        ),
        MCPTool(
            name="get_credential",
            description="Retrieve a decrypted credential (requires auth + scope check)",
            parameters={"credential_id": "string", "node_id": "string"},
        ),
        MCPTool(
            name="store_credential",
            description="Store a new encrypted credential",
            parameters={
                "name": "string", "provider": "string", "service": "string",
                "scope": "string", "fields": "object",
            },
        ),
        MCPTool(
            name="rotate_credential",
            description="Rotate credential values",
            parameters={"credential_id": "string", "new_fields": "object"},
        ),
        MCPTool(
            name="delete_credential",
            description="Delete a credential",
            parameters={"credential_id": "string"},
        ),
        MCPTool(
            name="get_access_policy",
            description="Get setup guide for a provider+service combination",
            parameters={"provider": "string", "service": "string"},
        ),
        MCPTool(
            name="check_requirements",
            description="Check what credentials a service requires",
            parameters={"service": "string"},
        ),
        MCPTool(
            name="vault_status",
            description="Get vault status (sealed/unsealed, credential counts)",
            parameters={},
        ),
    ]
    return MCPManifest(tools=tools)


@router.post("/tools/list_credentials")
async def mcp_list_credentials():
    _require_unsealed()
    entries = _vault_store.list_credentials()
    return {"credentials": [e.model_dump(mode="json") for e in entries]}


@router.post("/tools/get_credential")
async def mcp_get_credential(req: GetRequest, request: Request):
    _require_unsealed()

    # Rate limit
    if not _rate_limiter.check(req.credential_id):
        raise HTTPException(429, "Rate limit exceeded for this credential")

    ip = request.client.host if request.client else None
    grant = _vault_store.retrieve(req.credential_id, req.node_id, ip_address=ip)
    if grant is None:
        raise HTTPException(404, "Credential not found or access denied")
    return grant.model_dump(mode="json")


@router.post("/tools/store_credential")
async def mcp_store_credential(req: StoreRequest):
    _require_unsealed()
    try:
        provider = CloudProvider(req.provider)
        service = ServiceRequirement(req.service)
        scope = CredentialScope(req.scope)
    except ValueError as e:
        raise HTTPException(400, f"Invalid enum value: {e}")

    expires_at = None
    if req.expires_at:
        try:
            expires_at = datetime.fromisoformat(req.expires_at)
        except ValueError:
            raise HTTPException(400, "Invalid expires_at format (use ISO 8601)")

    cred_req = CredentialRequest(
        name=req.name,
        provider=provider,
        service=service,
        scope=scope,
        allowed_nodes=req.allowed_nodes,
        fields=req.fields,
        expires_at=expires_at,
    )
    credential_id = _vault_store.store(cred_req)
    return {"credential_id": credential_id, "status": "stored"}


@router.post("/tools/rotate_credential")
async def mcp_rotate_credential(req: RotateRequest):
    _require_unsealed()
    success = _vault_store.rotate(req.credential_id, req.new_fields)
    if not success:
        raise HTTPException(404, "Credential not found")
    return {"credential_id": req.credential_id, "status": "rotated"}


@router.post("/tools/delete_credential")
async def mcp_delete_credential(req: DeleteRequest):
    _require_unsealed()
    success = _vault_store.delete(req.credential_id)
    if not success:
        raise HTTPException(404, "Credential not found")
    return {"credential_id": req.credential_id, "status": "deleted"}


@router.post("/tools/get_access_policy")
async def mcp_get_access_policy(req: PolicyRequest):
    try:
        provider = CloudProvider(req.provider)
        service = ServiceRequirement(req.service)
    except ValueError as e:
        raise HTTPException(400, f"Invalid enum value: {e}")

    policy = get_policy(provider, service)
    if policy is None:
        raise HTTPException(404, f"No policy defined for {req.provider}/{req.service}")
    return policy.model_dump()


@router.post("/tools/check_requirements")
async def mcp_check_requirements(req: CheckRequirementsRequest):
    try:
        service = ServiceRequirement(req.service)
    except ValueError as e:
        raise HTTPException(400, f"Invalid service: {e}")

    matching = [p for p in POLICIES if p.service == service]
    return {
        "service": service.value,
        "providers": [
            {
                "provider": p.provider.value,
                "required_fields": p.required_fields,
                "optional_fields": p.optional_fields,
                "description": p.description,
            }
            for p in matching
        ],
    }


@router.post("/tools/vault_status")
async def mcp_vault_status():
    if _vault_store is None:
        return VaultStatus(sealed=True, credential_count=0).model_dump(mode="json")
    if _sealed:
        return VaultStatus(sealed=True, credential_count=0).model_dump(mode="json")
    status = _vault_store.status()
    return status.model_dump(mode="json")


@router.post("/tools/grant_node")
async def mcp_grant_node(req: GrantRequest):
    _require_unsealed()
    success = _vault_store.grant_node(req.credential_id, req.node_id)
    if not success:
        raise HTTPException(404, "Credential not found")
    return {"credential_id": req.credential_id, "node_id": req.node_id, "status": "granted"}


@router.post("/tools/revoke_node")
async def mcp_revoke_node(req: GrantRequest):
    _require_unsealed()
    success = _vault_store.revoke_node(req.credential_id, req.node_id)
    if not success:
        raise HTTPException(404, "Credential not found")
    return {"credential_id": req.credential_id, "node_id": req.node_id, "status": "revoked"}


@router.get("/tools/access_log")
async def mcp_access_log(credential_id: Optional[str] = None, limit: int = 50):
    _require_unsealed()
    log = _vault_store.get_access_log(credential_id=credential_id, limit=limit)
    return {"entries": log}


# ── Standalone MCP app factory ────────────────────────────────────────────────

def create_mcp_app(
    vault_store: VaultStore,
    auth_secret: str | None = None,
    max_access_rate: int = 10,
) -> FastAPI:
    """Create a standalone MCP FastAPI app for the vault.

    Parameters
    ----------
    vault_store:
        Initialised :class:`VaultStore`.
    auth_secret:
        HMAC secret for token authentication.  If provided, all endpoints
        require ``admin`` role Bearer tokens.
    max_access_rate:
        Maximum credential retrievals per minute per credential.
    """
    mcp_app = FastAPI(title="Mesh Vault MCP Server", version="0.1.0")

    wire_vault(vault_store, max_access_rate=max_access_rate)

    if auth_secret:
        from mesh.security.auth import AuthMiddleware
        mcp_app.add_middleware(
            AuthMiddleware,
            secret=auth_secret,
            exempt_paths=("/health", "/mcp/manifest"),
        )

    mcp_app.include_router(router)

    @mcp_app.get("/health")
    async def health():
        return {
            "status": "ok" if not _sealed else "sealed",
            "vault_sealed": _sealed,
        }

    return mcp_app
