from __future__ import annotations

import hashlib
import json
import logging
import re
from pathlib import Path
from typing import Any

from fastmcp import FastMCP

from .config import configure_logging, load_settings
from .openapi_tools import register_openapi_tools
from .spec_loader import SpecLoadError, load_spec_with_cache
from .visibility_policy import VisibilityPolicy, load_visibility_policy

logger = logging.getLogger("brp_mcp")

_DEFAULT_ENRICHMENTS_PATH = ".config/n8n_description_enrichments.json"


def _load_enrichments(enrichments_path: Path) -> dict[str, Any]:
    if not enrichments_path.exists():
        return {}
    data = json.loads(enrichments_path.read_text(encoding="utf-8"))
    return data if isinstance(data, dict) else {}


def _apply_description_enrichments(
    spec: dict[str, Any], enrichments: dict[str, Any],
) -> int:
    if not enrichments:
        return 0

    enriched = 0
    for api_path, path_item in spec.get("paths", {}).items():
        norm_api_path = re.sub(r"\{[^}]+\}", "{*}", api_path)
        for method in ("get", "post", "put", "patch", "delete"):
            op = path_item.get(method)
            if not op:
                continue
            key = f"{method.upper()} {norm_api_path}"
            entry = enrichments.get(key)
            if not entry or not entry.get("description"):
                continue
            n8n_desc = entry["description"]
            n8n_name = entry.get("n8n_node", "")
            existing = op.get("description", "") or ""
            if n8n_desc in existing:
                continue
            prefix = f"[N8N: {n8n_name}] {n8n_desc}" if n8n_name else n8n_desc
            op["description"] = f"{prefix}\n\n---\n\n{existing}" if existing else prefix
            enriched += 1
    return enriched


def _build_visibility_from_enrichments(
    spec: dict[str, Any], enrichments: dict[str, Any],
) -> VisibilityPolicy:
    """Derive visibility policy from N8N enrichments.

    Only OpenAPI operations whose normalised ``METHOD /path`` appears as a key
    in the enrichments dict are marked visible.  Everything else is hidden.
    This guarantees all environments (Dev / Staging / Prod) expose the exact
    same set of tools — the ones N8N actually uses.
    """
    if not enrichments:
        return VisibilityPolicy(default_visible=None, services={}, operations={})

    enrichment_keys = {k for k, v in enrichments.items() if isinstance(v, dict)}

    operations: dict[str, bool] = {}
    for api_path, path_item in spec.get("paths", {}).items():
        if not isinstance(path_item, dict):
            continue
        norm_api_path = re.sub(r"\{[^}]+\}", "{*}", api_path)
        for method in ("get", "post", "put", "patch", "delete"):
            op = path_item.get(method)
            if not op:
                continue
            if f"{method.upper()} {norm_api_path}" in enrichment_keys:
                operations[f"{method.upper()} {api_path}"] = True

    return VisibilityPolicy(
        default_visible=False,
        services={},
        operations=operations,
    )


def build_server() -> FastMCP:
    settings = load_settings()
    spec, source = load_spec_with_cache(
        openapi_url=settings.openapi_url,
        cache_path=settings.openapi_cache_path,
        timeout_seconds=settings.timeout_seconds,
    )

    enrichments_path = Path(_DEFAULT_ENRICHMENTS_PATH)
    enrichments = _load_enrichments(enrichments_path)
    
    from .config import ENV_PROFILES
    valid_prod_stage_keys = set()
    for env_name in ("prod", "stage"):
        try:
            s_spec, _ = load_spec_with_cache(
                openapi_url=ENV_PROFILES[env_name]["openapi_url"],
                cache_path=Path(f".cache/{env_name}-openapi.json"),
                timeout_seconds=settings.timeout_seconds,
            )
            for api_path, path_item in s_spec.get("paths", {}).items():
                if isinstance(path_item, dict):
                    norm_api_path = re.sub(r"\{[^}]+\}", "{*}", api_path)
                    for method in ("get", "post", "put", "patch", "delete"):
                        if method in path_item:
                            valid_prod_stage_keys.add(f"{method.upper()} {norm_api_path}")
        except Exception as e:
            logger.warning("Failed to load %s spec for filtering: %s", env_name, e)

    if valid_prod_stage_keys:
        filtered_enrichments = {}
        for k, v in enrichments.items():
            if k in valid_prod_stage_keys:
                filtered_enrichments[k] = v
            else:
                logger.info("Skipping API enrichment '%s' (not in Prod/Staging swagger)", k)
        enrichments = filtered_enrichments

    enrichment_count = _apply_description_enrichments(spec, enrichments)

    visibility_policy = _build_visibility_from_enrichments(spec, enrichments)
    if visibility_policy.is_empty:
        visibility_policy = load_visibility_policy(settings.visibility_policy_path)

    if valid_prod_stage_keys:
        filtered_operations = {}
        for op_key, visible in visibility_policy.operations.items():
            try:
                method, path = op_key.split(" ", 1)
                path_pattern = re.sub(r"\{[^}]+\}", "{*}", path)
                norm_op_key = f"{method.upper()} {path_pattern}"
                if norm_op_key in valid_prod_stage_keys:
                    filtered_operations[op_key] = visible
                elif visible:
                    logger.info("Hiding API '%s' (not in Prod/Staging swagger)", op_key)
            except ValueError:
                filtered_operations[op_key] = visible
        visibility_policy = VisibilityPolicy(
            default_visible=visibility_policy.default_visible,
            services=visibility_policy.services,
            operations=filtered_operations,
        )
    mcp = FastMCP(settings.mcp_name)
    registration_stats = register_openapi_tools(
        mcp=mcp,
        spec=spec,
        gateway_base_url=settings.gateway_base_url,
        timeout_seconds=settings.timeout_seconds,
        visibility_policy=visibility_policy,
        default_visibility=settings.default_visibility,
        default_auth0_audience=settings.auth0_audience,
    )

    used_enrichments_policy = visibility_policy.default_visible is not None and bool(
        visibility_policy.operations
    )
    spec_hash = hashlib.sha256(json.dumps(spec, sort_keys=True).encode("utf-8")).hexdigest()[:12]
    summary = {
        "event": "startup_summary",
        "env": settings.brp_env,
        "envInferred": settings.inferred_env,
        "urlOverride": settings.used_url_override,
        "specSource": source,
        "specHash": spec_hash,
        "defaultVisibility": settings.default_visibility,
        "visibilitySource": "n8n_enrichments" if used_enrichments_policy else "policy_file",
        "policyPath": str(settings.visibility_policy_path),
        "policyLoaded": not visibility_policy.is_empty,
        "totalOperations": registration_stats.total_operations,
        "filteredByServiceVisibility": registration_stats.filtered_by_service_visibility,
        "filteredByOperationVisibility": registration_stats.filtered_by_operation_visibility,
        "toolCount": registration_stats.registered_tools,
        "descriptionEnrichments": enrichment_count,
        "transport": settings.transport,
    }
    logger.info("startup_summary %s", json.dumps(summary, separators=(",", ":")))
    return mcp


def run() -> None:
    configure_logging()
    settings = load_settings()
    logger.info("Starting BRP MCP (env=%s, transport=%s)", settings.brp_env, settings.transport)
    try:
        server = build_server()
    except SpecLoadError as exc:
        logger.error("BRP MCP startup failed: %s", exc)
        raise SystemExit(f"BRP MCP startup failed: {exc}") from exc

    if settings.transport == "sse":
        server.run(transport="sse", host=settings.host, port=settings.port, path="/brp-mcp-server/sse")
        return

    server.run(transport="stdio")


if __name__ == "__main__":
    run()
