"""
Mizan Edge Manifest Generator.

Generates the Edge manifest — a static JSON mapping contexts to URL
patterns and params, consumed by Mizan Edge at deploy time for CDN
cache invalidation. Independent from the Mizan IR; the IR drives
codegen, the manifest drives CDN purging.

Usage:
    from mizan.export import generate_edge_manifest, generate_edge_manifest_json
"""

from __future__ import annotations

import json
import re
from typing import Any

from mizan_core.registry import get_context_groups, get_registry


__all__ = [
    "generate_edge_manifest",
    "generate_edge_manifest_json",
]


def generate_edge_manifest(
    base_url: str = "/api/mizan",
    view_urls: dict[str, list[str]] | None = None,
) -> dict[str, Any]:
    """
    Generate the Edge manifest — a static JSON mapping contexts to URL
    patterns and params for CDN cache purging.

    The manifest is consumed by Mizan Edge at deploy time. When Edge
    receives X-Mizan-Invalidate: user;user_id=5, it:
    1. Looks up 'user' in the manifest
    2. Resolves URL patterns with params: /profile/:user_id/ → /profile/5/
    3. Purges the resolved URLs + the context API endpoint

    Args:
        base_url: The Mizan API mount point (default: /api/mizan)
        view_urls: Optional mapping of context names to URL patterns for
                   view-path functions. These are URLs that Edge should
                   also purge when a context is invalidated.

    Returns:
        Manifest dict suitable for JSON serialization.
    """
    _USER_SCOPED_PARAMS = {"user_id", "user", "owner_id", "account_id"}

    groups = get_context_groups()
    registry = get_registry()
    all_functions = registry.get("functions", {})

    manifest: dict[str, Any] = {"version": 1, "contexts": {}, "mutations": {}}

    for ctx_name, fn_names in sorted(groups.items()):
        param_names: set[str] = set()
        functions_meta: list[dict[str, Any]] = []
        page_routes: list[str] = []

        for fn_name in fn_names:
            fn_cls = all_functions.get(fn_name)
            if fn_cls is None:
                continue

            input_cls = getattr(fn_cls, "Input", None)
            if input_cls is not None and hasattr(input_cls, "model_fields"):
                for param_name in input_cls.model_fields:
                    param_names.add(param_name)

            meta = getattr(fn_cls, "_meta", {})
            route = meta.get("route")
            view_path = meta.get("view_path")

            fn_entry: dict[str, Any] = {
                "name": fn_name,
                "path": "view" if view_path else "rpc",
            }
            if route:
                fn_entry["route"] = route
                fn_entry["methods"] = meta.get("methods", ["GET"])
                page_routes.append(route)
            if meta.get("rev"):
                fn_entry["rev"] = meta["rev"]
            if meta.get("cache") is not None and meta.get("cache") is not True:
                fn_entry["cache"] = meta["cache"]
            functions_meta.append(fn_entry)

        sorted_params = sorted(param_names)
        user_scoped = any(p in _USER_SCOPED_PARAMS for p in param_names)

        ctx_entry: dict[str, Any] = {
            "functions": functions_meta,
            "endpoints": [f"{base_url}/ctx/{ctx_name}/"],
            "params": sorted_params,
            "user_scoped": user_scoped,
            "render_strategy": "dynamic_cached" if user_scoped else "psr",
        }

        if page_routes:
            ctx_entry["page_routes"] = page_routes
        if view_urls and ctx_name in view_urls:
            ctx_entry.setdefault("page_routes", []).extend(view_urls[ctx_name])

        manifest["contexts"][ctx_name] = ctx_entry

    for fn_name, fn_cls in sorted(all_functions.items()):
        meta = getattr(fn_cls, "_meta", {})
        if not meta.get("affects"):
            continue

        affected_contexts = list({a["name"] for a in meta["affects"]})
        mutation: dict[str, Any] = {"affects": affected_contexts}

        # Auto-scoped params — function params that match context params
        input_cls = getattr(fn_cls, "Input", None)
        if input_cls is not None and hasattr(input_cls, "model_fields"):
            fn_params = set(input_cls.model_fields.keys())
            auto_scoped: list[str] = []
            for ctx_name in affected_contexts:
                ctx_param_names: set[str] = set()
                ctx_fns = groups.get(ctx_name, [])
                for ctx_fn_name in ctx_fns:
                    ctx_fn_cls = all_functions.get(ctx_fn_name)
                    if ctx_fn_cls is None:
                        continue
                    ctx_input = getattr(ctx_fn_cls, "Input", None)
                    if ctx_input is not None and hasattr(ctx_input, "model_fields"):
                        ctx_param_names.update(ctx_input.model_fields.keys())
                for p in fn_params:
                    if p in ctx_param_names and p not in auto_scoped:
                        auto_scoped.append(p)
            if auto_scoped:
                mutation["auto_scoped_params"] = sorted(auto_scoped)

        if meta.get("private"):
            mutation["private"] = True
        if meta.get("route"):
            mutation["route"] = meta["route"]
            mutation["methods"] = meta.get("methods", ["POST"])

        manifest["mutations"][fn_name] = mutation

    return manifest


def generate_edge_manifest_json(
    base_url: str = "/api/mizan",
    view_urls: dict[str, list[str]] | None = None,
    indent: int = 2,
) -> str:
    """JSON-serialize the Edge manifest."""
    return json.dumps(generate_edge_manifest(base_url, view_urls), indent=indent)
