"""
Server-side validation for Lexical editor JSON.

Walk the document tree and reject any node whose ``type`` is not in
ALLOWED_NODE_TYPES.  This is a belt-and-braces check: the Lexical
client already restricts which nodes can be created, but we validate
server-side too.
"""
from __future__ import annotations

ALLOWED_NODE_TYPES: frozenset[str] = frozenset(
    {
        # Standard Lexical nodes
        "root",
        "paragraph",
        "heading",
        "quote",
        "list",
        "listitem",
        "link",
        "autolink",
        "code",
        "code-highlight",
        "linebreak",
        "text",
        "horizontalrule",
        "image",
        "tab",
        # Ghost "extended-*" variants
        "extended-text",
        "extended-heading",
        "extended-quote",
        # Ghost card types
        "html",
        "gallery",
        "embed",
        "bookmark",
        "markdown",
        "email",
        "email-cta",
        "button",
        "callout",
        "toggle",
        "video",
        "audio",
        "file",
        "product",
        "header",
        "signup",
        "aside",
        "codeblock",
        "call-to-action",
        "at-link",
        "paywall",
    }
)


def validate_lexical(doc: dict) -> tuple[bool, str | None]:
    """Recursively validate a Lexical JSON document.

    Returns ``(True, None)`` when the document is valid, or
    ``(False, reason)`` when an unknown node type is found.
    """
    if not isinstance(doc, dict):
        return False, "Document must be a JSON object"

    root = doc.get("root")
    if not isinstance(root, dict):
        return False, "Document must contain a 'root' object"

    return _walk(root)


def _walk(node: dict) -> tuple[bool, str | None]:
    node_type = node.get("type")
    if node_type is not None and node_type not in ALLOWED_NODE_TYPES:
        return False, f"Disallowed node type: {node_type}"

    for child in node.get("children", []):
        if isinstance(child, dict):
            ok, reason = _walk(child)
            if not ok:
                return False, reason

    return True, None
