"""Blog page helpers — async functions available in .sx defpage expressions.

All helpers return data values (dicts, lists) — no sx_call().
Markup composition lives entirely in .sx defpage and .sx defcomp files.
"""
from __future__ import annotations

from typing import Any


# ---------------------------------------------------------------------------
# Shared hydration helpers (kept for auth/g._defpage_ctx side effects)
# ---------------------------------------------------------------------------

def _add_to_defpage_ctx(**kwargs: Any) -> None:
    from quart import g
    if not hasattr(g, '_defpage_ctx'):
        g._defpage_ctx = {}
    g._defpage_ctx.update(kwargs)


async def _ensure_post_data(slug: str | None) -> None:
    """Load post data and set g.post_data + defpage context.

    Replicates post bp's hydrate_post_data + context_processor.
    """
    from quart import g, abort

    if hasattr(g, 'post_data') and g.post_data:
        await _inject_post_context(g.post_data)
        return

    if not slug:
        abort(404)

    from bp.post.services.post_data import post_data

    is_admin = bool((g.get("rights") or {}).get("admin"))
    p_data = await post_data(slug, g.s, include_drafts=True)
    if not p_data:
        abort(404)

    # Draft access control
    if p_data["post"].get("status") != "published":
        if is_admin:
            pass
        elif g.user and p_data["post"].get("user_id") == g.user.id:
            pass
        else:
            abort(404)

    g.post_data = p_data
    g.post_slug = slug
    await _inject_post_context(p_data)


async def _inject_post_context(p_data: dict) -> None:
    """Add post context_processor data to defpage context."""
    from shared.config import config
    from shared.infrastructure.fragments import fetch_fragment
    from shared.infrastructure.data_client import fetch_data
    from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
    from shared.infrastructure.cart_identity import current_cart_identity

    db_post_id = p_data["post"]["id"]
    post_slug = p_data["post"]["slug"]

    container_nav = await fetch_fragment("relations", "container-nav", params={
        "container_type": "page",
        "container_id": str(db_post_id),
        "post_slug": post_slug,
    })

    ctx: dict = {
        **p_data,
        "base_title": config()["title"],
        "container_nav": container_nav,
    }

    if p_data["post"].get("is_page"):
        ident = current_cart_identity()
        summary_params: dict = {"page_slug": post_slug}
        if ident["user_id"] is not None:
            summary_params["user_id"] = ident["user_id"]
        if ident["session_id"] is not None:
            summary_params["session_id"] = ident["session_id"]
        raw_summary = await fetch_data(
            "cart", "cart-summary", params=summary_params, required=False,
        )
        page_summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
        ctx["page_cart_count"] = (
            page_summary.count + page_summary.calendar_count + page_summary.ticket_count
        )
        ctx["page_cart_total"] = float(
            page_summary.total + page_summary.calendar_total + page_summary.ticket_total
        )

    _add_to_defpage_ctx(**ctx)


# ---------------------------------------------------------------------------
# Registration
# ---------------------------------------------------------------------------

def _register_blog_helpers() -> None:
    from shared.sx.pages import register_page_helpers
    register_page_helpers("blog", {
        "editor-data": _h_editor_data,
        "editor-page-data": _h_editor_page_data,
        "post-admin-data": _h_post_admin_data,
        "post-data-data": _h_post_data_data,
        "post-preview-data": _h_post_preview_data,
        "post-entries-data": _h_post_entries_data,
        "post-settings-data": _h_post_settings_data,
        "post-edit-data": _h_post_edit_data,
    })


# ---------------------------------------------------------------------------
# Editor helpers
# ---------------------------------------------------------------------------

def _editor_init_js(urls: dict, *, form_id: str = "post-edit-form",
                    has_initial_json: bool = True) -> str:
    """Build the editor initialization JavaScript string.

    URLs dict must contain: upload_image, upload_media, upload_file, oembed,
    snippets, unsplash_key.
    """
    font_size_preamble = (
        "(function() {"
        "  function applyEditorFontSize() {"
        "    document.documentElement.style.fontSize = '62.5%';"
        "    document.body.style.fontSize = '1.6rem';"
        "  }"
        "  function restoreDefaultFontSize() {"
        "    document.documentElement.style.fontSize = '';"
        "    document.body.style.fontSize = '';"
        "  }"
        "  applyEditorFontSize();"
        "  document.body.addEventListener('htmx:beforeSwap', function cleanup(e) {"
        "    if (e.detail.target && e.detail.target.id === 'main-panel') {"
        "      restoreDefaultFontSize();"
        "      document.body.removeEventListener('htmx:beforeSwap', cleanup);"
        "    }"
        "  });"
    )

    upload_image = urls["upload_image"]
    upload_media = urls["upload_media"]
    upload_file = urls["upload_file"]
    oembed = urls["oembed"]
    unsplash_key = urls["unsplash_key"]
    snippets = urls["snippets"]

    init_body = (
        "  function init() {"
        "    var csrfToken = document.querySelector('input[name=\"csrf_token\"]').value;"
        f"    var uploadUrl = '{upload_image}';"
        "    var uploadUrls = {"
        "      image: uploadUrl,"
        f"      media: '{upload_media}',"
        f"      file: '{upload_file}',"
        "    };"
        "    var fileInput = document.getElementById('feature-image-file');"
        "    var addBtn = document.getElementById('feature-image-add-btn');"
        "    var deleteBtn = document.getElementById('feature-image-delete-btn');"
        "    var preview = document.getElementById('feature-image-preview');"
        "    var emptyState = document.getElementById('feature-image-empty');"
        "    var filledState = document.getElementById('feature-image-filled');"
        "    var hiddenUrl = document.getElementById('feature-image-input');"
        "    var hiddenCaption = document.getElementById('feature-image-caption-input');"
        "    var captionInput = document.getElementById('feature-image-caption');"
        "    var uploading = document.getElementById('feature-image-uploading');"
        "    function showFilled(url) {"
        "      preview.src = url; hiddenUrl.value = url;"
        "      emptyState.classList.add('hidden'); filledState.classList.remove('hidden'); uploading.classList.add('hidden');"
        "    }"
        "    function showEmpty() {"
        "      preview.src = ''; hiddenUrl.value = ''; hiddenCaption.value = ''; captionInput.value = '';"
        "      emptyState.classList.remove('hidden'); filledState.classList.add('hidden'); uploading.classList.add('hidden');"
        "    }"
        "    function uploadFile(file) {"
        "      emptyState.classList.add('hidden'); uploading.classList.remove('hidden');"
        "      var fd = new FormData(); fd.append('file', file);"
        "      fetch(uploadUrl, { method: 'POST', body: fd, headers: { 'X-CSRFToken': csrfToken } })"
        "      .then(function(r) { if (!r.ok) throw new Error('Upload failed (' + r.status + ')'); return r.json(); })"
        "      .then(function(data) {"
        "        var url = data.images && data.images[0] && data.images[0].url;"
        "        if (url) showFilled(url); else { showEmpty(); alert('Upload succeeded but no image URL returned.'); }"
        "      })"
        "      .catch(function(e) { showEmpty(); alert(e.message); });"
        "    }"
        "    addBtn.addEventListener('click', function() { fileInput.click(); });"
        "    preview.addEventListener('click', function() { fileInput.click(); });"
        "    deleteBtn.addEventListener('click', function(e) { e.stopPropagation(); showEmpty(); });"
        "    fileInput.addEventListener('change', function() {"
        "      if (fileInput.files && fileInput.files[0]) { uploadFile(fileInput.files[0]); fileInput.value = ''; }"
        "    });"
        "    captionInput.addEventListener('input', function() { hiddenCaption.value = captionInput.value; });"
        "    var excerpt = document.querySelector('textarea[name=\"custom_excerpt\"]');"
        "    function autoResize() { excerpt.style.height = 'auto'; excerpt.style.height = excerpt.scrollHeight + 'px'; }"
        "    excerpt.addEventListener('input', autoResize); autoResize();"
    )

    if has_initial_json:
        init_body += (
            "    var dataEl = document.getElementById('lexical-initial-data');"
            "    var initialJson = dataEl ? dataEl.textContent.trim() : null;"
            "    if (initialJson) { var hidden = document.getElementById('lexical-json-input'); if (hidden) hidden.value = initialJson; }"
        )
        initial_json_arg = "initialJson: initialJson,"
    else:
        initial_json_arg = "initialJson: null,"

    init_body += (
        "    window.mountEditor('lexical-editor', {"
        f"      {initial_json_arg}"
        "      csrfToken: csrfToken,"
        "      uploadUrls: uploadUrls,"
        f"      oembedUrl: '{oembed}',"
        f"      unsplashApiKey: '{unsplash_key}',"
        f"      snippetsUrl: '{snippets}',"
        "    });"
        "    if (typeof SxEditor !== 'undefined') {"
        "      SxEditor.mount('sx-editor', {"
        "        initialSx: (document.getElementById('sx-content-input') || {}).value || null,"
        "        csrfToken: csrfToken,"
        "        uploadUrls: uploadUrls,"
        f"        oembedUrl: '{oembed}',"
        "        onChange: function(sx) {"
        "          document.getElementById('sx-content-input').value = sx;"
        "        }"
        "      });"
        "    }"
        "    document.addEventListener('keydown', function(e) {"
        "      if ((e.ctrlKey || e.metaKey) && e.key === 's') {"
        f"        e.preventDefault(); document.getElementById('{form_id}').requestSubmit();"
        "      }"
        "    });"
        "  }"
        "  if (typeof window.mountEditor === 'function') { init(); }"
        "  else { var _t = setInterval(function() {"
        "    if (typeof window.mountEditor === 'function') { clearInterval(_t); init(); }"
        "  }, 50); }"
        "})();"
    )

    return font_size_preamble + init_body


def _editor_urls() -> dict:
    """Extract editor API URLs and asset paths."""
    import os
    from quart import url_for as qurl, current_app

    asset_url_fn = current_app.jinja_env.globals.get("asset_url", lambda p: "")
    return {
        "upload_image": qurl("blog.editor_api.upload_image"),
        "upload_media": qurl("blog.editor_api.upload_media"),
        "upload_file": qurl("blog.editor_api.upload_file"),
        "oembed": qurl("blog.editor_api.oembed_proxy"),
        "snippets": qurl("blog.editor_api.list_snippets"),
        "unsplash_key": os.environ.get("UNSPLASH_ACCESS_KEY", ""),
        "css_href": asset_url_fn("scripts/editor.css"),
        "js_src": asset_url_fn("scripts/editor.js"),
        "sx_editor_js_src": asset_url_fn("scripts/sx-editor.js"),
    }


def _h_editor_data(**kw) -> dict:
    """New post editor — return data for ~blog-editor-content."""
    from shared.browser.app.csrf import generate_csrf_token

    urls = _editor_urls()
    csrf = generate_csrf_token()
    init_js = _editor_init_js(urls, form_id="post-new-form", has_initial_json=False)

    return {
        "csrf": csrf,
        "title-placeholder": "Post title...",
        "create-label": "Create Post",
        "css-href": urls["css_href"],
        "js-src": urls["js_src"],
        "sx-editor-js-src": urls["sx_editor_js_src"],
        "init-js": init_js,
    }


def _h_editor_page_data(**kw) -> dict:
    """New page editor — return data for ~blog-editor-content."""
    from shared.browser.app.csrf import generate_csrf_token

    urls = _editor_urls()
    csrf = generate_csrf_token()
    init_js = _editor_init_js(urls, form_id="post-new-form", has_initial_json=False)

    return {
        "csrf": csrf,
        "title-placeholder": "Page title...",
        "create-label": "Create Page",
        "css-href": urls["css_href"],
        "js-src": urls["js_src"],
        "sx-editor-js-src": urls["sx_editor_js_src"],
        "init-js": init_js,
    }


# ---------------------------------------------------------------------------
# Post admin helpers
# ---------------------------------------------------------------------------

async def _h_post_admin_data(slug=None, **kw) -> dict:
    await _ensure_post_data(slug)
    return {}


# ---------------------------------------------------------------------------
# Data introspection
# ---------------------------------------------------------------------------

def _extract_model_data(obj, depth=0, max_depth=2) -> dict:
    """Recursively extract ORM model data into a nested dict for .sx rendering."""
    from markupsafe import escape as esc

    # Scalar columns
    columns = []
    for col in obj.__mapper__.columns:
        key = col.key
        if key == "_sa_instance_state":
            continue
        val = getattr(obj, key, None)
        if val is None:
            columns.append({"key": str(key), "value": "", "type": "nil"})
        elif hasattr(val, "isoformat"):
            columns.append({"key": str(key), "value": str(esc(val.isoformat())), "type": "date"})
        elif isinstance(val, str):
            columns.append({"key": str(key), "value": str(esc(val)), "type": "str"})
        else:
            columns.append({"key": str(key), "value": str(esc(str(val))), "type": "other"})

    # Relationships
    relationships = []
    for rel in obj.__mapper__.relationships:
        rel_name = rel.key
        loaded = rel_name in obj.__dict__
        value = getattr(obj, rel_name, None) if loaded else None
        cardinality = "many" if rel.uselist else "one"
        cls_name = rel.mapper.class_.__name__

        rel_data: dict[str, Any] = {
            "name": rel_name,
            "cardinality": cardinality,
            "class_name": cls_name,
            "loaded": loaded,
            "value": None,
        }

        if value is None:
            pass  # value stays None
        elif rel.uselist:
            items_list = list(value) if value else []
            val_data: dict[str, Any] = {"is_list": True, "count": len(items_list)}
            if items_list and depth < max_depth:
                items = []
                for i, it in enumerate(items_list, 1):
                    summary = _obj_summary(it)
                    children = _extract_model_data(it, depth + 1, max_depth) if depth < max_depth else None
                    items.append({"index": i, "summary": summary, "children": children})
                val_data["items"] = items
            rel_data["value"] = val_data
        else:
            child = value
            summary = _obj_summary(child)
            children = _extract_model_data(child, depth + 1, max_depth) if depth < max_depth else None
            rel_data["value"] = {"is_list": False, "summary": summary, "children": children}

        relationships.append(rel_data)

    return {"columns": columns, "relationships": relationships}


def _obj_summary(obj) -> str:
    """Build a summary string for an ORM object."""
    from markupsafe import escape as esc
    ident_parts = []
    for k in ("id", "ghost_id", "uuid", "slug", "name", "title"):
        if k in obj.__mapper__.c:
            v = getattr(obj, k, "")
            ident_parts.append(f"{k}={v}")
    return str(esc(" \u2022 ".join(ident_parts) if ident_parts else str(obj)))


async def _h_post_data_data(slug=None, **kw) -> dict:
    await _ensure_post_data(slug)
    from quart import g

    original_post = getattr(g, "post_data", {}).get("original_post")
    if original_post is None:
        return {"tablename": None, "model-data": None}

    tablename = getattr(original_post, "__tablename__", "?")
    model_data = _extract_model_data(original_post, 0, 2)

    return {"tablename": tablename, "model-data": model_data}


# ---------------------------------------------------------------------------
# Preview content
# ---------------------------------------------------------------------------

async def _h_post_preview_data(slug=None, **kw) -> dict:
    await _ensure_post_data(slug)
    from quart import g
    from shared.services.registry import services
    from shared.sx.helpers import SxExpr

    preview = await services.blog_page.preview_data(g.s)

    return {
        "sx-pretty": SxExpr(preview["sx_pretty"]) if preview.get("sx_pretty") else None,
        "json-pretty": SxExpr(preview["json_pretty"]) if preview.get("json_pretty") else None,
        "sx-rendered": preview.get("sx_rendered") or None,
        "lex-rendered": preview.get("lex_rendered") or None,
    }


# ---------------------------------------------------------------------------
# Entries browser
# ---------------------------------------------------------------------------

def _extract_associated_entries_data(all_calendars, associated_entry_ids, post_slug: str) -> list:
    """Extract associated entry data for .sx rendering."""
    from quart import url_for as qurl
    from shared.utils import host_url

    entries = []
    for calendar in all_calendars:
        cal_entries = getattr(calendar, "entries", []) or []
        cal_name = getattr(calendar, "name", "")
        cal_post = getattr(calendar, "post", None)
        cal_fi = getattr(cal_post, "feature_image", None) if cal_post else None
        cal_title = getattr(cal_post, "title", "") if cal_post else ""

        for entry in cal_entries:
            e_id = getattr(entry, "id", None)
            if e_id not in associated_entry_ids:
                continue
            if getattr(entry, "deleted_at", None) is not None:
                continue

            e_name = getattr(entry, "name", "")
            e_start = getattr(entry, "start_at", None)
            e_end = getattr(entry, "end_at", None)

            toggle_url = host_url(qurl("blog.post.admin.toggle_entry",
                                       slug=post_slug, entry_id=e_id))

            date_str = e_start.strftime("%A, %B %d, %Y at %H:%M") if e_start else ""
            if e_end:
                date_str += f" \u2013 {e_end.strftime('%H:%M')}"

            entries.append({
                "name": e_name,
                "confirm_text": f"This will remove {e_name} from this post",
                "toggle_url": toggle_url,
                "cal_image": cal_fi or "",
                "cal_title": cal_title,
                "date_str": f"{cal_name} \u2022 {date_str}",
            })

    return entries


def _extract_calendar_browser_data(all_calendars, post_slug: str) -> list:
    """Extract calendar browser data for .sx rendering."""
    from quart import url_for as qurl
    from shared.utils import host_url

    calendars = []
    for cal in all_calendars:
        cal_post = getattr(cal, "post", None)
        cal_fi = getattr(cal_post, "feature_image", None) if cal_post else None
        cal_title = getattr(cal_post, "title", "") if cal_post else ""
        cal_name = getattr(cal, "name", "")
        view_url = host_url(qurl("blog.post.admin.calendar_view",
                                  slug=post_slug, calendar_id=cal.id))
        calendars.append({
            "name": cal_name,
            "title": cal_title,
            "image": cal_fi or "",
            "view_url": view_url,
        })
    return calendars


async def _h_post_entries_data(slug=None, **kw) -> dict:
    await _ensure_post_data(slug)
    from quart import g
    from sqlalchemy import select
    from shared.models.calendars import Calendar
    from shared.browser.app.csrf import generate_csrf_token
    from bp.post.services.entry_associations import get_post_entry_ids

    post_id = g.post_data["post"]["id"]
    post_slug = g.post_data["post"]["slug"]
    associated_entry_ids = await get_post_entry_ids(post_id)
    result = await g.s.execute(
        select(Calendar)
        .where(Calendar.deleted_at.is_(None))
        .order_by(Calendar.name.asc())
    )
    all_calendars = result.scalars().all()
    for calendar in all_calendars:
        await g.s.refresh(calendar, ["entries", "post"])

    csrf = generate_csrf_token()
    entries = _extract_associated_entries_data(
        all_calendars, associated_entry_ids, post_slug)
    calendars = _extract_calendar_browser_data(all_calendars, post_slug)

    return {"entries": entries, "calendars": calendars, "csrf": csrf}


# ---------------------------------------------------------------------------
# Settings form
# ---------------------------------------------------------------------------

async def _h_post_settings_data(slug=None, **kw) -> dict:
    await _ensure_post_data(slug)
    from quart import g, request
    from models.ghost_content import Post
    from sqlalchemy import select as sa_select
    from sqlalchemy.orm import selectinload
    from shared.browser.app.csrf import generate_csrf_token
    from bp.post.admin.routes import _post_to_edit_dict

    post_id = g.post_data["post"]["id"]
    post = (await g.s.execute(
        sa_select(Post)
        .where(Post.id == post_id)
        .options(selectinload(Post.tags))
    )).scalar_one_or_none()
    ghost_post = _post_to_edit_dict(post) if post else {}
    save_success = request.args.get("saved") == "1"
    csrf = generate_csrf_token()

    p = g.post_data.get("post", {}) if hasattr(g, "post_data") else {}
    is_page = p.get("is_page", False)
    gp = ghost_post

    # Extract tag names
    tags = gp.get("tags") or []
    if tags:
        tag_names = ", ".join(
            getattr(t, "name", t.get("name", "") if isinstance(t, dict) else str(t))
            for t in tags
        )
    else:
        tag_names = ""

    # Published at — trim to datetime-local format
    pub_at = gp.get("published_at") or ""
    pub_at_val = pub_at[:16] if pub_at else ""

    return {
        "csrf": csrf,
        "updated-at": gp.get("updated_at") or "",
        "is-page": is_page,
        "save-success": save_success,
        "settings-slug": gp.get("slug") or "",
        "published-at": pub_at_val,
        "featured": bool(gp.get("featured")),
        "visibility": gp.get("visibility") or "public",
        "email-only": bool(gp.get("email_only")),
        "tags": tag_names,
        "feature-image-alt": gp.get("feature_image_alt") or "",
        "meta-title": gp.get("meta_title") or "",
        "meta-description": gp.get("meta_description") or "",
        "canonical-url": gp.get("canonical_url") or "",
        "og-title": gp.get("og_title") or "",
        "og-description": gp.get("og_description") or "",
        "og-image": gp.get("og_image") or "",
        "twitter-title": gp.get("twitter_title") or "",
        "twitter-description": gp.get("twitter_description") or "",
        "twitter-image": gp.get("twitter_image") or "",
        "custom-template": gp.get("custom_template") or "",
    }


# ---------------------------------------------------------------------------
# Post edit content
# ---------------------------------------------------------------------------

def _extract_newsletter_options(newsletters) -> list:
    """Extract newsletter data for .sx rendering."""
    return [{"slug": getattr(nl, "slug", ""),
             "name": getattr(nl, "name", "")} for nl in newsletters]


def _extract_footer_badges(ghost_post: dict, post: dict, save_success: bool,
                           publish_requested: bool, already_emailed: bool) -> list:
    """Extract footer badge data for .sx rendering."""
    badges = []
    if save_success:
        badges.append({"cls": "text-[14px] text-green-600", "text": "Saved."})
    if publish_requested:
        badges.append({"cls": "text-[14px] text-blue-600",
                       "text": "Publish requested \u2014 an admin will review."})
    if post.get("publish_requested"):
        badges.append({"cls": "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800",
                       "text": "Publish requested"})
    if already_emailed:
        nl_name = ""
        newsletter = ghost_post.get("newsletter")
        if newsletter:
            nl_name = (getattr(newsletter, "name", "")
                       if not isinstance(newsletter, dict)
                       else newsletter.get("name", ""))
        suffix = f" to {nl_name}" if nl_name else ""
        badges.append({"cls": "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800",
                       "text": f"Emailed{suffix}"})
    return badges


async def _h_post_edit_data(slug=None, **kw) -> dict:
    await _ensure_post_data(slug)
    from quart import g, request as qrequest
    from models.ghost_content import Post
    from sqlalchemy import select as sa_select
    from sqlalchemy.orm import selectinload
    from shared.infrastructure.data_client import fetch_data
    from shared.browser.app.csrf import generate_csrf_token
    from bp.post.admin.routes import _post_to_edit_dict

    post_id = g.post_data["post"]["id"]
    db_post = (await g.s.execute(
        sa_select(Post)
        .where(Post.id == post_id)
        .options(selectinload(Post.tags))
    )).scalar_one_or_none()
    ghost_post = _post_to_edit_dict(db_post) if db_post else {}
    save_success = qrequest.args.get("saved") == "1"
    save_error = qrequest.args.get("error", "")
    raw_newsletters = await fetch_data("account", "newsletters", required=False) or []
    from types import SimpleNamespace
    newsletters = [SimpleNamespace(**nl) for nl in raw_newsletters]

    csrf = generate_csrf_token()
    urls = _editor_urls()

    post = g.post_data.get("post", {}) if hasattr(g, "post_data") else {}
    is_page = post.get("is_page", False)

    feature_image = ghost_post.get("feature_image") or ""
    feature_image_caption = ghost_post.get("feature_image_caption") or ""
    title_val = ghost_post.get("title") or ""
    excerpt_val = ghost_post.get("custom_excerpt") or ""
    updated_at = ghost_post.get("updated_at") or ""
    status = ghost_post.get("status") or "draft"
    lexical_json = ghost_post.get("lexical") or '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'
    sx_content = ghost_post.get("sx_content") or ""
    has_sx = bool(sx_content)

    already_emailed = bool(ghost_post and ghost_post.get("email") and
                           (ghost_post["email"] if isinstance(ghost_post["email"], dict) else {}).get("status"))
    email_obj = ghost_post.get("email")
    if email_obj and not isinstance(email_obj, dict):
        already_emailed = bool(getattr(email_obj, "status", None))

    title_placeholder = "Page title..." if is_page else "Post title..."

    # Return newsletter data as list of dicts (composed in SX)
    nl_options = _extract_newsletter_options(newsletters)

    # Return footer badge data as list of dicts (composed in SX)
    publish_requested = bool(qrequest.args.get("publish_requested")) if hasattr(qrequest, 'args') else False
    badges = _extract_footer_badges(ghost_post, post, save_success,
                                     publish_requested, already_emailed)

    init_js = _editor_init_js(urls, form_id="post-edit-form", has_initial_json=True)

    return {
        "csrf": csrf,
        "updated-at": str(updated_at),
        "title-val": title_val,
        "excerpt-val": excerpt_val,
        "feature-image": feature_image,
        "feature-image-caption": feature_image_caption,
        "sx-content-val": sx_content,
        "lexical-json": lexical_json,
        "has-sx": has_sx,
        "title-placeholder": title_placeholder,
        "status": status,
        "already-emailed": already_emailed,
        "newsletters": nl_options,
        "badges": badges,
        "css-href": urls["css_href"],
        "js-src": urls["js_src"],
        "sx-editor-js-src": urls["sx_editor_js_src"],
        "init-js": init_js,
        "save-error": save_error or None,
    }
