"""Contract enforcement middleware for ALL API endpoints.

Maps every write endpoint (POST/PUT/PATCH/DELETE) to a contract action.
Checks the contract before the request handler runs. Returns 403 if blocked,
428 if confirmation required.

GET requests are never blocked (read access is controlled at the tool level).
User-initiated actions (chat, onboarding) are exempt — the contract governs
the *agent*, not the user.
"""

from __future__ import annotations

import logging

from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware

log = logging.getLogger(__name__)

# Map: (method, path_prefix) → contract_action
# More specific paths are checked first (longest prefix match)
ROUTE_CONTRACT_MAP: list[tuple[str, str, str]] = [
    # === CRITICAL — always enforced ===
    ("PUT", "/api/contract", "modify_contract"),
    ("PUT", "/api/vault/file/system/identity/SOUL", "modify_soul"),
    ("PUT", "/api/vault/file/system/identity/CONTRACT", "modify_contract"),
    ("POST", "/api/emergency", "override_security"),

    # === Credentials & integrations ===
    ("POST", "/api/settings/credentials", "write_vault"),
    ("DELETE", "/api/settings/credentials", "write_vault"),
    ("PUT", "/api/settings", "write_vault"),
    ("POST", "/api/settings/ollama/pull", "write_vault"),
    ("DELETE", "/api/settings/ollama", "write_vault"),

    # === Email ===
    ("POST", "/api/email/connect", "read_inbox"),
    ("POST", "/api/email/disconnect", "read_inbox"),
    ("POST", "/api/email/send", "send_email"),
    ("POST", "/api/email/draft", "draft_content"),
    ("POST", "/api/email/message", "read_inbox"),
    ("POST", "/api/email/sync", "read_inbox"),

    # === Channels ===
    ("POST", "/api/channels/broadcast", "send_notification"),
    ("POST", "/api/channels/whatsapp/connect", "send_message"),
    ("POST", "/api/channels/whatsapp/disconnect", "send_message"),

    # === Knowledge base ===
    # NOTE: POST /api/kb (create KB) is exempt — user-initiated structural action
    # NOTE: POST /api/kb/{slug}/scrape is user-initiated — exempt like create
    # NOTE: DELETE /api/kb, /api/wiki/article, /api/wiki/stale are user-initiated UI actions — exempt

    # === Wiki ===
    ("DELETE", "/api/wiki/reset", "reset_wiki"),

    # === Projects ===
    ("POST", "/api/projects", "modify_project"),
    ("PUT", "/api/projects", "modify_project"),
    ("PATCH", "/api/projects", "modify_project"),
    # NOTE: DELETE /api/projects is user-initiated UI action — exempt

    # === Tasks ===
    ("POST", "/api/tasks", "create_task"),
    ("PATCH", "/api/tasks", "create_task"),
    ("PUT", "/api/tasks", "create_task"),  # task reassignment
    ("DELETE", "/api/tasks", "create_task"),

    # === Goals ===
    ("POST", "/api/goals", "create_task"),
    ("PATCH", "/api/goals", "create_task"),
    ("PUT", "/api/goals", "create_task"),  # edit goal description/target_date
    ("DELETE", "/api/goals", "create_task"),  # detach goal from project

    # === Journal ===
    ("POST", "/api/journal", "journal_write"),

    # === Memory ===
    ("POST", "/api/memory/generate", "update_memory"),
    ("POST", "/api/memory/compact", "update_memory"),
    ("POST", "/api/memory/pending", "update_memory"),

    # === Reflection ===
    ("POST", "/api/reflections", "daily_briefing"),

    # === Orchestrator ===
    ("POST", "/api/orchestrator/agents", "delegate_task"),
    ("PUT", "/api/orchestrator/agents", "delegate_task"),
    ("DELETE", "/api/orchestrator/agents", "delegate_task"),
    ("POST", "/api/orchestrator/dispatch", "delegate_task"),
    ("POST", "/api/orchestrator/workflow", "delegate_task"),

    # === Skills ===
    ("POST", "/api/skills/create", "write_vault"),
    ("PUT", "/api/skills", "write_vault"),

    # === Graph ===
    ("POST", "/api/graph/build", "write_vault"),
    ("POST", "/api/graph/refresh", "write_vault"),
    ("POST", "/api/graph/triples", "write_vault"),
    ("POST", "/api/graph/decay", "write_vault"),

    # === Guardian ===
    ("POST", "/api/guardian", "override_security"),
    ("DELETE", "/api/guardian", "override_security"),

    # === Sync ===
    ("POST", "/api/sync", "write_vault"),

    # === Daily cycle ===
    ("POST", "/api/daily", "daily_briefing"),

    # === Cron ===
    ("POST", "/api/cron", "delegate_task"),
    ("DELETE", "/api/cron", "delegate_task"),

    # === Domains ===
    ("PUT", "/api/domains", "write_vault"),

    # === Vault file writes (catch-all for vault) ===
    ("PUT", "/api/vault/file", "modify_vault_file"),

    # === Costs budget ===
    ("PUT", "/api/costs/budget", "write_vault"),

    # === Search reindex ===
    ("POST", "/api/search/reindex", "write_vault"),

    # === MCP ===
    ("POST", "/api/mcp/servers/add", "write_vault"),
    ("POST", "/api/mcp/servers", "write_vault"),  # connect/disconnect
    ("DELETE", "/api/mcp/servers", "write_vault"),

    # === Integrations ===
    ("POST", "/api/integrations", "write_vault"),

    # === Autopilot ===
    ("POST", "/api/autopilot", "daily_briefing"),
]

# Exempt paths — never contract-checked (user-initiated, not agent actions)
EXEMPT_PATHS = frozenset({
    "/api/chat",             # User-initiated chat
    "/api/transcribe",       # Speech-to-text for chat input
    "/api/onboarding/chat",  # Onboarding conversation
    "/api/health",           # Health check
    "/api/orchestrator/reload",  # Admin reload
    "/api/emergency-stop",   # Safety mechanism — never gated
    "/api/resume",           # Resume after emergency stop
    "/api/launch",           # Service lifecycle — user-initiated
})


def _find_action(method: str, path: str) -> str | None:
    """Find the contract action for a route. Longest prefix match wins."""
    for route_method, route_prefix, action in ROUTE_CONTRACT_MAP:
        if method == route_method and path.startswith(route_prefix):
            return action
    return None


class ContractMiddleware(BaseHTTPMiddleware):
    """FastAPI middleware that enforces the contract on all write endpoints."""

    async def dispatch(self, request: Request, call_next) -> Response:  # noqa: ANN001
        method = request.method
        path = request.url.path

        # Only check write methods
        if method not in ("POST", "PUT", "PATCH", "DELETE"):
            return await call_next(request)

        # Exempt paths
        for exempt in EXEMPT_PATHS:
            if path.startswith(exempt):
                return await call_next(request)

        # Channel webhooks are incoming — don't block
        if "/webhook" in path or "/incoming" in path:
            return await call_next(request)

        # Find the contract action
        action = _find_action(method, path)
        if not action:
            # No mapping = no enforcement (unmapped routes pass through)
            return await call_next(request)

        # Get the contract enforcer
        contract = getattr(request.app.state, "contract", None)
        if not contract:
            return await call_next(request)

        # Check frozen state
        frozen = getattr(request.app.state, "frozen", False)
        if frozen and action != "override_security":
            return Response(
                content="Agent is frozen — emergency stop active",
                status_code=503,
                media_type="text/plain",
            )

        # Enforce
        result = contract.check(action)
        if not result.allowed:
            log.warning("Contract middleware BLOCKED: %s %s → %s: %s", method, path, action, result.reason)
            return Response(
                content=f"Contract blocked: {result.reason}",
                status_code=403,
                media_type="text/plain",
            )

        if result.requires_confirmation:
            log.info("Contract middleware CONFIRM: %s %s → %s", method, path, action)
            # For API calls, confirmation-required means proceed with a warning header
            # (the UI can show this to the user)
            response = await call_next(request)
            response.headers["X-Contract-Confirmation"] = f"Action '{action}' requires confirmation"
            return response

        return await call_next(request)
