"""
Lexical JSON → HTML renderer.

Produces HTML matching Ghost's ``kg-*`` class conventions so the existing
``cards.css`` stylesheet works unchanged.

Public API
----------
    render_lexical(doc)   – Lexical JSON (dict or string) → HTML string
"""
from __future__ import annotations

import html
import json
from typing import Callable

import mistune

# ---------------------------------------------------------------------------
# Registry
# ---------------------------------------------------------------------------

_RENDERERS: dict[str, Callable[[dict], str]] = {}


def _renderer(node_type: str):
    """Decorator — register a function as the renderer for *node_type*."""
    def decorator(fn: Callable[[dict], str]) -> Callable[[dict], str]:
        _RENDERERS[node_type] = fn
        return fn
    return decorator


# ---------------------------------------------------------------------------
# Public entry point
# ---------------------------------------------------------------------------

def render_lexical(doc: dict | str) -> str:
    """Render a Lexical JSON document to an HTML string."""
    if isinstance(doc, str):
        doc = json.loads(doc)
    root = doc.get("root", doc)
    return _render_children(root.get("children", []))


# ---------------------------------------------------------------------------
# Core dispatch
# ---------------------------------------------------------------------------

def _render_node(node: dict) -> str:
    node_type = node.get("type", "")
    renderer = _RENDERERS.get(node_type)
    if renderer:
        return renderer(node)
    return ""


def _render_children(children: list[dict]) -> str:
    return "".join(_render_node(c) for c in children)


# ---------------------------------------------------------------------------
# Text formatting
# ---------------------------------------------------------------------------

# Lexical format bitmask
_FORMAT_BOLD = 1
_FORMAT_ITALIC = 2
_FORMAT_STRIKETHROUGH = 4
_FORMAT_UNDERLINE = 8
_FORMAT_CODE = 16
_FORMAT_SUBSCRIPT = 32
_FORMAT_SUPERSCRIPT = 64
_FORMAT_HIGHLIGHT = 128

_FORMAT_TAGS: list[tuple[int, str, str]] = [
    (_FORMAT_BOLD,          "<strong>", "</strong>"),
    (_FORMAT_ITALIC,        "<em>",     "</em>"),
    (_FORMAT_STRIKETHROUGH, "<s>",      "</s>"),
    (_FORMAT_UNDERLINE,     "<u>",      "</u>"),
    (_FORMAT_CODE,          "<code>",   "</code>"),
    (_FORMAT_SUBSCRIPT,     "<sub>",    "</sub>"),
    (_FORMAT_SUPERSCRIPT,   "<sup>",    "</sup>"),
    (_FORMAT_HIGHLIGHT,     "<mark>",   "</mark>"),
]

# Element-level alignment from ``format`` field
_ALIGN_MAP = {
    1: "text-align: left",
    2: "text-align: center",
    3: "text-align: right",
    4: "text-align: justify",
}


def _align_style(node: dict) -> str:
    fmt = node.get("format")
    if isinstance(fmt, int) and fmt in _ALIGN_MAP:
        return f' style="{_ALIGN_MAP[fmt]}"'
    if isinstance(fmt, str) and fmt:
        return f' style="text-align: {fmt}"'
    return ""


def _wrap_format(text: str, fmt: int) -> str:
    for mask, open_tag, close_tag in _FORMAT_TAGS:
        if fmt & mask:
            text = f"{open_tag}{text}{close_tag}"
    return text


# ---------------------------------------------------------------------------
# Tier 1 — text nodes
# ---------------------------------------------------------------------------

@_renderer("text")
def _text(node: dict) -> str:
    text = html.escape(node.get("text", ""))
    fmt = node.get("format", 0)
    if isinstance(fmt, int) and fmt:
        text = _wrap_format(text, fmt)
    return text


@_renderer("linebreak")
def _linebreak(_node: dict) -> str:
    return "<br>"


@_renderer("tab")
def _tab(_node: dict) -> str:
    return "\t"


@_renderer("paragraph")
def _paragraph(node: dict) -> str:
    inner = _render_children(node.get("children", []))
    if not inner:
        inner = "<br>"
    style = _align_style(node)
    return f"<p{style}>{inner}</p>"


@_renderer("extended-text")
def _extended_text(node: dict) -> str:
    return _paragraph(node)


@_renderer("heading")
def _heading(node: dict) -> str:
    tag = node.get("tag", "h2")
    inner = _render_children(node.get("children", []))
    style = _align_style(node)
    return f"<{tag}{style}>{inner}</{tag}>"


@_renderer("extended-heading")
def _extended_heading(node: dict) -> str:
    return _heading(node)


@_renderer("quote")
def _quote(node: dict) -> str:
    inner = _render_children(node.get("children", []))
    return f"<blockquote>{inner}</blockquote>"


@_renderer("extended-quote")
def _extended_quote(node: dict) -> str:
    return _quote(node)


@_renderer("aside")
def _aside(node: dict) -> str:
    inner = _render_children(node.get("children", []))
    return f"<aside>{inner}</aside>"


@_renderer("link")
def _link(node: dict) -> str:
    href = html.escape(node.get("url", ""), quote=True)
    target = node.get("target", "")
    rel = node.get("rel", "")
    inner = _render_children(node.get("children", []))
    attrs = f' href="{href}"'
    if target:
        attrs += f' target="{html.escape(target, quote=True)}"'
    if rel:
        attrs += f' rel="{html.escape(rel, quote=True)}"'
    return f"<a{attrs}>{inner}</a>"


@_renderer("autolink")
def _autolink(node: dict) -> str:
    return _link(node)


@_renderer("at-link")
def _at_link(node: dict) -> str:
    return _link(node)


@_renderer("list")
def _list(node: dict) -> str:
    tag = "ol" if node.get("listType") == "number" else "ul"
    start = node.get("start")
    inner = _render_children(node.get("children", []))
    attrs = ""
    if tag == "ol" and start and start != 1:
        attrs = f' start="{start}"'
    return f"<{tag}{attrs}>{inner}</{tag}>"


@_renderer("listitem")
def _listitem(node: dict) -> str:
    inner = _render_children(node.get("children", []))
    return f"<li>{inner}</li>"


@_renderer("horizontalrule")
def _horizontalrule(_node: dict) -> str:
    return "<hr>"


@_renderer("code")
def _code(node: dict) -> str:
    # Inline code nodes from Lexical — just render inner text
    inner = _render_children(node.get("children", []))
    return f"<code>{inner}</code>"


@_renderer("codeblock")
def _codeblock(node: dict) -> str:
    lang = node.get("language", "")
    code = html.escape(node.get("code", ""))
    cls = f' class="language-{html.escape(lang)}"' if lang else ""
    return f'<pre><code{cls}>{code}</code></pre>'


@_renderer("code-highlight")
def _code_highlight(node: dict) -> str:
    text = html.escape(node.get("text", ""))
    highlight_type = node.get("highlightType", "")
    if highlight_type:
        return f'<span class="token {html.escape(highlight_type)}">{text}</span>'
    return text


# ---------------------------------------------------------------------------
# Tier 2 — common cards
# ---------------------------------------------------------------------------

@_renderer("image")
def _image(node: dict) -> str:
    src = node.get("src", "")
    alt = node.get("alt", "")
    caption = node.get("caption", "")
    width = node.get("cardWidth", "") or node.get("width", "")
    href = node.get("href", "")

    width_class = ""
    if width == "wide":
        width_class = " kg-width-wide"
    elif width == "full":
        width_class = " kg-width-full"

    img_tag = f'<img src="{html.escape(src, quote=True)}" alt="{html.escape(alt, quote=True)}" loading="lazy">'
    if href:
        img_tag = f'<a href="{html.escape(href, quote=True)}">{img_tag}</a>'

    parts = [f'<figure class="kg-card kg-image-card{width_class}">']
    parts.append(img_tag)
    if caption:
        parts.append(f"<figcaption>{caption}</figcaption>")
    parts.append("</figure>")
    return "".join(parts)


@_renderer("gallery")
def _gallery(node: dict) -> str:
    images = node.get("images", [])
    if not images:
        return ""

    rows = []
    for i in range(0, len(images), 3):
        row_imgs = images[i:i + 3]
        row_cls = f"kg-gallery-row" if len(row_imgs) <= 3 else "kg-gallery-row"
        imgs_html = []
        for img in row_imgs:
            src = img.get("src", "")
            alt = img.get("alt", "")
            caption = img.get("caption", "")
            img_tag = f'<img src="{html.escape(src, quote=True)}" alt="{html.escape(alt, quote=True)}" loading="lazy">'
            fig = f'<figure class="kg-gallery-image">{img_tag}'
            if caption:
                fig += f"<figcaption>{caption}</figcaption>"
            fig += "</figure>"
            imgs_html.append(fig)
        rows.append(f'<div class="{row_cls}">{"".join(imgs_html)}</div>')

    caption = node.get("caption", "")
    caption_html = f"<figcaption>{caption}</figcaption>" if caption else ""
    return (
        f'<figure class="kg-card kg-gallery-card kg-width-wide">'
        f'<div class="kg-gallery-container">{"".join(rows)}</div>'
        f"{caption_html}</figure>"
    )


@_renderer("html")
def _html_card(node: dict) -> str:
    raw = node.get("html", "")
    return f"<!--kg-card-begin: html-->{raw}<!--kg-card-end: html-->"


@_renderer("markdown")
def _markdown(node: dict) -> str:
    md_text = node.get("markdown", "")
    rendered = mistune.html(md_text)
    return f"<!--kg-card-begin: markdown-->{rendered}<!--kg-card-end: markdown-->"


@_renderer("embed")
def _embed(node: dict) -> str:
    embed_html = node.get("html", "")
    caption = node.get("caption", "")
    url = node.get("url", "")
    caption_html = f"<figcaption>{caption}</figcaption>" if caption else ""
    return (
        f'<figure class="kg-card kg-embed-card">'
        f"{embed_html}{caption_html}</figure>"
    )


@_renderer("bookmark")
def _bookmark(node: dict) -> str:
    url = node.get("url", "")
    title = html.escape(node.get("metadata", {}).get("title", "") or node.get("title", ""))
    description = html.escape(node.get("metadata", {}).get("description", "") or node.get("description", ""))
    icon = node.get("metadata", {}).get("icon", "") or node.get("icon", "")
    author = html.escape(node.get("metadata", {}).get("author", "") or node.get("author", ""))
    publisher = html.escape(node.get("metadata", {}).get("publisher", "") or node.get("publisher", ""))
    thumbnail = node.get("metadata", {}).get("thumbnail", "") or node.get("thumbnail", "")
    caption = node.get("caption", "")

    icon_html = f'<img class="kg-bookmark-icon" src="{html.escape(icon, quote=True)}" alt="">' if icon else ""
    thumbnail_html = (
        f'<div class="kg-bookmark-thumbnail">'
        f'<img src="{html.escape(thumbnail, quote=True)}" alt=""></div>'
    ) if thumbnail else ""

    meta_parts = []
    if icon_html:
        meta_parts.append(icon_html)
    if author:
        meta_parts.append(f'<span class="kg-bookmark-author">{author}</span>')
    if publisher:
        meta_parts.append(f'<span class="kg-bookmark-publisher">{publisher}</span>')
    metadata_html = f'<span class="kg-bookmark-metadata">{"".join(meta_parts)}</span>' if meta_parts else ""

    caption_html = f"<figcaption>{caption}</figcaption>" if caption else ""

    return (
        f'<figure class="kg-card kg-bookmark-card">'
        f'<a class="kg-bookmark-container" href="{html.escape(url, quote=True)}">'
        f'<div class="kg-bookmark-content">'
        f'<div class="kg-bookmark-title">{title}</div>'
        f'<div class="kg-bookmark-description">{description}</div>'
        f'{metadata_html}'
        f'</div>'
        f'{thumbnail_html}'
        f'</a>'
        f'{caption_html}'
        f'</figure>'
    )


@_renderer("callout")
def _callout(node: dict) -> str:
    color = node.get("backgroundColor", "grey")
    emoji = node.get("calloutEmoji", "")
    inner = _render_children(node.get("children", []))

    emoji_html = f'<div class="kg-callout-emoji">{emoji}</div>' if emoji else ""
    return (
        f'<div class="kg-card kg-callout-card kg-callout-card-{html.escape(color)}">'
        f'{emoji_html}'
        f'<div class="kg-callout-text">{inner}</div>'
        f'</div>'
    )


@_renderer("button")
def _button(node: dict) -> str:
    text = html.escape(node.get("buttonText", ""))
    url = html.escape(node.get("buttonUrl", ""), quote=True)
    alignment = node.get("alignment", "center")
    return (
        f'<div class="kg-card kg-button-card kg-align-{alignment}">'
        f'<a href="{url}" class="kg-btn kg-btn-accent">{text}</a>'
        f'</div>'
    )


@_renderer("toggle")
def _toggle(node: dict) -> str:
    heading = node.get("heading", "")
    # Toggle content is in children
    inner = _render_children(node.get("children", []))
    return (
        f'<div class="kg-card kg-toggle-card" data-kg-toggle-state="close">'
        f'<div class="kg-toggle-heading">'
        f'<h4 class="kg-toggle-heading-text">{heading}</h4>'
        f'<button class="kg-toggle-card-icon">'
        f'<svg viewBox="0 0 14 14"><path d="M7 0a.5.5 0 0 1 .5.5v6h6a.5.5 0 1 1 0 1h-6v6a.5.5 0 1 1-1 0v-6h-6a.5.5 0 0 1 0-1h6v-6A.5.5 0 0 1 7 0Z" fill="currentColor"/></svg>'
        f'</button>'
        f'</div>'
        f'<div class="kg-toggle-content">{inner}</div>'
        f'</div>'
    )


# ---------------------------------------------------------------------------
# Tier 3 — media & remaining cards
# ---------------------------------------------------------------------------

@_renderer("audio")
def _audio(node: dict) -> str:
    src = node.get("src", "")
    title = html.escape(node.get("title", ""))
    duration = node.get("duration", 0)
    thumbnail = node.get("thumbnailSrc", "")

    duration_min = int(duration) // 60
    duration_sec = int(duration) % 60
    duration_str = f"{duration_min}:{duration_sec:02d}"

    if thumbnail:
        thumb_html = (
            f'<img src="{html.escape(thumbnail, quote=True)}" alt="audio-thumbnail" '
            f'class="kg-audio-thumbnail">'
        )
    else:
        thumb_html = (
            '<div class="kg-audio-thumbnail placeholder">'
            '<svg viewBox="0 0 24 24"><path d="M2 12C2 6.48 6.48 2 12 2s10 4.48 10 10-4.48 10-10 10S2 17.52 2 12zm7.5 5.25L16 12 9.5 6.75v10.5z" fill="currentColor"/></svg>'
            '</div>'
        )

    return (
        f'<div class="kg-card kg-audio-card">'
        f'{thumb_html}'
        f'<div class="kg-audio-player-container">'
        f'<div class="kg-audio-title">{title}</div>'
        f'<div class="kg-audio-player">'
        f'<button class="kg-audio-play-icon"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z" fill="currentColor"/></svg></button>'
        f'<div class="kg-audio-current-time">0:00</div>'
        f'<div class="kg-audio-time">/ {duration_str}</div>'
        f'<input type="range" class="kg-audio-seek-slider" max="100" value="0">'
        f'<button class="kg-audio-playback-rate">1&#215;</button>'
        f'<button class="kg-audio-unmute-icon"><svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" fill="currentColor"/></svg></button>'
        f'<input type="range" class="kg-audio-volume-slider" max="100" value="100">'
        f'</div>'
        f'</div>'
        f'<audio src="{html.escape(src, quote=True)}" preload="metadata"></audio>'
        f'</div>'
    )


@_renderer("video")
def _video(node: dict) -> str:
    src = node.get("src", "")
    caption = node.get("caption", "")
    width = node.get("cardWidth", "")
    thumbnail = node.get("thumbnailSrc", "") or node.get("customThumbnailSrc", "")
    loop = node.get("loop", False)

    width_class = ""
    if width == "wide":
        width_class = " kg-width-wide"
    elif width == "full":
        width_class = " kg-width-full"

    loop_attr = " loop" if loop else ""
    poster_attr = f' poster="{html.escape(thumbnail, quote=True)}"' if thumbnail else ""
    caption_html = f"<figcaption>{caption}</figcaption>" if caption else ""

    return (
        f'<figure class="kg-card kg-video-card{width_class}">'
        f'<div class="kg-video-container">'
        f'<video src="{html.escape(src, quote=True)}" controls preload="metadata"{poster_attr}{loop_attr}></video>'
        f'</div>'
        f'{caption_html}'
        f'</figure>'
    )


@_renderer("file")
def _file(node: dict) -> str:
    src = node.get("src", "")
    title = html.escape(node.get("fileName", "") or node.get("title", ""))
    caption = node.get("caption", "")
    file_size = node.get("fileSize", 0)
    file_name = html.escape(node.get("fileName", ""))

    # Format size
    if file_size:
        kb = file_size / 1024
        if kb < 1024:
            size_str = f"{kb:.0f} KB"
        else:
            size_str = f"{kb / 1024:.1f} MB"
    else:
        size_str = ""

    caption_html = f'<div class="kg-file-card-caption">{caption}</div>' if caption else ""
    size_html = f'<div class="kg-file-card-filesize">{size_str}</div>' if size_str else ""

    return (
        f'<div class="kg-card kg-file-card">'
        f'<a class="kg-file-card-container" href="{html.escape(src, quote=True)}" download="{file_name}">'
        f'<div class="kg-file-card-contents">'
        f'<div class="kg-file-card-title">{title}</div>'
        f'{size_html}'
        f'</div>'
        f'<div class="kg-file-card-icon">'
        f'<svg viewBox="0 0 24 24"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" fill="currentColor"/></svg>'
        f'</div>'
        f'</a>'
        f'{caption_html}'
        f'</div>'
    )


@_renderer("paywall")
def _paywall(_node: dict) -> str:
    return "<!--members-only-->"


@_renderer("header")
def _header(node: dict) -> str:
    heading = node.get("heading", "")
    subheading = node.get("subheading", "")
    size = node.get("size", "small")
    style = node.get("style", "dark")
    bg_image = node.get("backgroundImageSrc", "")
    button_text = node.get("buttonText", "")
    button_url = node.get("buttonUrl", "")

    bg_style = f' style="background-image: url({html.escape(bg_image, quote=True)})"' if bg_image else ""
    heading_html = f"<h2>{heading}</h2>" if heading else ""
    subheading_html = f"<p>{subheading}</p>" if subheading else ""
    button_html = (
        f'<a class="kg-header-card-button" href="{html.escape(button_url, quote=True)}">{html.escape(button_text)}</a>'
        if button_text and button_url else ""
    )

    return (
        f'<div class="kg-card kg-header-card kg-style-{html.escape(style)} kg-size-{html.escape(size)}"{bg_style}>'
        f'{heading_html}{subheading_html}{button_html}'
        f'</div>'
    )


@_renderer("signup")
def _signup(node: dict) -> str:
    heading = node.get("heading", "")
    subheading = node.get("subheading", "")
    disclaimer = node.get("disclaimer", "")
    button_text = html.escape(node.get("buttonText", "Subscribe"))
    button_color = node.get("buttonColor", "")
    bg_color = node.get("backgroundColor", "")
    bg_image = node.get("backgroundImageSrc", "")
    style = node.get("style", "dark")

    bg_style_parts = []
    if bg_color:
        bg_style_parts.append(f"background-color: {bg_color}")
    if bg_image:
        bg_style_parts.append(f"background-image: url({html.escape(bg_image, quote=True)})")
    style_attr = f' style="{"; ".join(bg_style_parts)}"' if bg_style_parts else ""

    heading_html = f"<h2>{heading}</h2>" if heading else ""
    subheading_html = f"<p>{subheading}</p>" if subheading else ""
    disclaimer_html = f'<p class="kg-signup-card-disclaimer">{disclaimer}</p>' if disclaimer else ""
    btn_style = f' style="background-color: {button_color}"' if button_color else ""

    return (
        f'<div class="kg-card kg-signup-card kg-style-{html.escape(style)}"{style_attr}>'
        f'{heading_html}{subheading_html}'
        f'<form class="kg-signup-card-form" data-members-form>'
        f'<input type="email" placeholder="Your email" required>'
        f'<button type="submit" class="kg-signup-card-button"{btn_style}>{button_text}</button>'
        f'</form>'
        f'{disclaimer_html}'
        f'</div>'
    )


@_renderer("product")
def _product(node: dict) -> str:
    title = html.escape(node.get("productTitle", "") or node.get("title", ""))
    description = node.get("productDescription", "") or node.get("description", "")
    img_src = node.get("productImageSrc", "")
    button_text = html.escape(node.get("buttonText", ""))
    button_url = node.get("buttonUrl", "")
    rating = node.get("rating", 0)

    img_html = (
        f'<img class="kg-product-card-image" src="{html.escape(img_src, quote=True)}" alt="">'
        if img_src else ""
    )
    button_html = (
        f'<a class="kg-product-card-button kg-btn kg-btn-accent" href="{html.escape(button_url, quote=True)}">{button_text}</a>'
        if button_text and button_url else ""
    )
    stars = ""
    if rating:
        active = int(rating)
        stars_html = []
        for i in range(5):
            cls = "kg-product-card-rating-active" if i < active else ""
            stars_html.append(
                f'<svg class="kg-product-card-rating-star {cls}" viewBox="0 0 24 24">'
                f'<path d="M12 .587l3.668 7.568 8.332 1.151-6.064 5.828 1.48 8.279L12 19.771l-7.416 3.642 1.48-8.279L0 9.306l8.332-1.151z" fill="currentColor"/>'
                f'</svg>'
            )
            stars = f'<div class="kg-product-card-rating">{"".join(stars_html)}</div>'

    return (
        f'<div class="kg-card kg-product-card">'
        f'{img_html}'
        f'<div class="kg-product-card-container">'
        f'<h4 class="kg-product-card-title">{title}</h4>'
        f'{stars}'
        f'<div class="kg-product-card-description">{description}</div>'
        f'{button_html}'
        f'</div>'
        f'</div>'
    )


@_renderer("email")
def _email(node: dict) -> str:
    raw_html = node.get("html", "")
    return f"<!--kg-card-begin: email-->{raw_html}<!--kg-card-end: email-->"


@_renderer("email-cta")
def _email_cta(node: dict) -> str:
    raw_html = node.get("html", "")
    return f"<!--kg-card-begin: email-cta-->{raw_html}<!--kg-card-end: email-cta-->"


@_renderer("call-to-action")
def _call_to_action(node: dict) -> str:
    raw_html = node.get("html", "")
    sponsor_label = node.get("sponsorLabel", "")
    label_html = (
        f'<span class="kg-cta-sponsor-label">{html.escape(sponsor_label)}</span>'
        if sponsor_label else ""
    )
    return (
        f'<div class="kg-card kg-cta-card">'
        f'{label_html}{raw_html}'
        f'</div>'
    )
