"""
Mizan core registry — function and composition registration with an
extension hook for backend-specific registries (channels, forms, etc.)
to plug into.

This is the framework-agnostic registry. Backends own their own
type-specific registries (channels in Django Channels, forms in Django
Forms, websockets in FastAPI, etc.) and register them as extensions
here so the unified schema export can include them.
"""

from __future__ import annotations

from typing import Any, Callable, Protocol


# ─── Core registries ────────────────────────────────────────────────────────

_functions: dict[str, Any] = {}
_compositions: dict[str, Any] = {}


# ─── Extension hook ─────────────────────────────────────────────────────────

class RegistryExtension(Protocol):
    """
    Backend-specific registries plug into core via this Protocol.

    Each extension owns its own registry of backend-shaped registrations
    (channels, forms, websocket consumers, etc.) and contributes a schema
    subdict to the unified schema export.
    """

    def schema(self) -> dict[str, Any]: ...
    def clear(self) -> None: ...


_extensions: dict[str, RegistryExtension] = {}


def register_extension(name: str, extension: RegistryExtension) -> None:
    """
    Register a backend extension. The extension contributes to schema
    output under its name (e.g. 'channels', 'forms').
    """
    _extensions[name] = extension


# ─── Function registration ──────────────────────────────────────────────────

def register(view_class: Any, name: str) -> Any:
    """
    Register a server function class. Used by the @client decorator.

    Idempotent for the same class (supports module reloads).
    """
    view_class.name = name

    if name in _functions:
        existing = _functions[name]
        if existing.__name__ == view_class.__name__:
            _functions[name] = view_class
            return view_class
        raise ValueError(
            f"Function '{name}' already registered by {existing.__name__}"
        )
    _functions[name] = view_class
    return view_class


def register_as(name: str) -> Callable[[Any], Any]:
    """Decorator form of register()."""
    def decorator(view_class: Any) -> Any:
        return register(view_class, name)
    return decorator


def register_compose(composed: Any, name: str) -> Any:
    """Register a composed context."""
    if name in _compositions:
        existing = _compositions[name]
        if existing.name == composed.name:
            _compositions[name] = composed
            return composed
        raise ValueError(
            f"Composition '{name}' already registered by {existing.name}"
        )
    _compositions[name] = composed
    return composed


# ─── Lookups ────────────────────────────────────────────────────────────────

def get_function(name: str) -> Any | None:
    """Get a registered server function by name."""
    return _functions.get(name)


def get_compose(name: str) -> Any | None:
    """Get a registered composition by name."""
    return _compositions.get(name)


def get_all_functions() -> dict[str, Any]:
    """Get all registered functions."""
    return _functions.copy()


def get_all_compositions() -> dict[str, Any]:
    """Get all registered compositions."""
    return _compositions.copy()


def get_contexts() -> dict[str, Any]:
    """Get all server functions marked as contexts (meta.context truthy)."""
    return {
        name: cls
        for name, cls in _functions.items()
        if getattr(cls, "_meta", {}).get("context")
    }


def get_context_groups() -> dict[str, list[str]]:
    """Group function names by their context string."""
    groups: dict[str, list[str]] = {}
    for name, cls in _functions.items():
        ctx = getattr(cls, "_meta", {}).get("context")
        if ctx:
            groups.setdefault(ctx, []).append(name)
    return groups


def get_registry() -> dict[str, Any]:
    """
    Full registry organized by type, including extension contributions.

    Returns:
        {
            "functions": {...},
            "compositions": {...},
            "<extension>": {...},  # one per registered extension
        }
    """
    out: dict[str, Any] = {
        "functions": _functions.copy(),
        "compositions": _compositions.copy(),
    }
    for name, ext in _extensions.items():
        # Extensions optionally expose their backing dict via .all()
        # (Protocol doesn't require it; only schema() and clear() are mandatory)
        if hasattr(ext, "all"):
            out[name] = ext.all()
    return out


def get_schema() -> dict[str, Any]:
    """
    Export the unified schema for codegen consumption.

    Aggregates function and composition schemas plus contributions from
    each registered backend extension under its name.
    """
    schema: dict[str, Any] = {
        "functions": {
            name: cls.get_schema_export() for name, cls in _functions.items()
        },
        "compositions": {
            name: {
                "name": composed.name,
                "type": "compose",
                "meta": composed._meta,
                "children": composed._meta.get("children", []),
                "leaves": composed._meta.get("leaves", []),
            }
            for name, composed in _compositions.items()
        },
    }
    for name, ext in _extensions.items():
        schema[name] = ext.schema()
    return schema


# ─── Validation ─────────────────────────────────────────────────────────────

def validate_registry() -> list[str]:
    """
    Check that all `affects` targets resolve to known contexts or functions.

    Emits warnings for unresolved targets (e.g. typos in string-based affects).
    Returns the list of warning messages (empty if all resolve).
    """
    import warnings

    issues: list[str] = []
    groups = get_context_groups()
    all_fn_names = set(_functions.keys())

    for fn_name, fn_cls in _functions.items():
        meta = getattr(fn_cls, "_meta", {})
        affects = meta.get("affects")
        if not affects:
            continue
        for target in affects:
            target_name = target.get("name", "")
            target_type = target.get("type", "")
            if target_type == "context" and target_name not in groups:
                issues.append(
                    f"@client function '{fn_name}' declares affects='{target_name}', "
                    f"but no context named '{target_name}' is registered."
                )
            elif target_type == "function" and target_name not in all_fn_names:
                issues.append(
                    f"@client function '{fn_name}' targets function '{target_name}', "
                    f"but no function named '{target_name}' is registered."
                )

    for msg in issues:
        warnings.warn(msg, stacklevel=2)

    return issues


# ─── Clear (testing) ────────────────────────────────────────────────────────

def clear_registry() -> None:
    """Clear all registrations, including extension state. For testing."""
    _functions.clear()
    _compositions.clear()
    for ext in _extensions.values():
        ext.clear()
