import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { router, useForm, usePage } from '@inertiajs/react'
import StudioLayout from '../../Layouts/StudioLayout'
import RichTextEditor from '../../components/forum/RichTextEditor'
import WorldMediaUploadField from '../../components/worlds/editor/WorldMediaUploadField'
import DateTimePicker from '../../components/ui/DateTimePicker'
import NovaSelect from '../../components/ui/NovaSelect'
import { Checkbox } from '../../components/ui'

// ── Minimal toast system ────────────────────────────────────────────────────
let _toastId = 0

function useToast() {
  const [toasts, setToasts] = useState([])

  const push = useCallback((message, type = 'info') => {
    const id = ++_toastId
    setToasts((prev) => [...prev, { id, message, type }])
    setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), 5000)
  }, [])

  const dismiss = useCallback((id) => setToasts((prev) => prev.filter((t) => t.id !== id)), [])

  return { toasts, push, dismiss }
}

function ToastStack({ toasts, onDismiss }) {
  if (!toasts.length) return null
  return (
    <div className="fixed bottom-6 right-6 z-[9999] flex flex-col gap-2" aria-live="polite">
      {toasts.map((t) => (
        <div
          key={t.id}
          className={[
            'flex items-start gap-3 rounded-2xl border px-4 py-3 text-sm shadow-2xl backdrop-blur-sm',
            t.type === 'success'
              ? 'border-emerald-400/30 bg-emerald-950/90 text-emerald-100'
              : t.type === 'error'
                ? 'border-rose-400/30 bg-rose-950/90 text-rose-100'
                : 'border-white/15 bg-slate-900/90 text-slate-100',
          ].join(' ')}
        >
          <span className="mt-0.5 text-base leading-none">
            {t.type === 'success' ? '✓' : t.type === 'error' ? '✕' : 'ℹ'}
          </span>
          <span className="flex-1 leading-5">{t.message}</span>
          <button
            type="button"
            onClick={() => onDismiss(t.id)}
            className="ml-2 opacity-60 hover:opacity-100"
            aria-label="Dismiss"
          >×</button>
        </div>
      ))}
    </div>
  )
}
// ─────────────────────────────────────────────────────────────────────────────

function SearchResultList({ items, onSelect, emptyLabel = 'No matches yet.' }) {
  if (!Array.isArray(items) || items.length === 0) {
    return <div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] p-3 text-xs text-slate-500">{emptyLabel}</div>
  }

  return (
    <div className="grid gap-2">
      {items.map((item) => (
        <button
          key={`${item.entity_type}-${item.id}`}
          type="button"
          onClick={() => onSelect(item)}
          className="flex items-center gap-3 rounded-2xl border border-white/10 bg-black/20 px-3 py-3 text-left transition hover:border-white/20"
        >
          {item.avatar ? <img src={item.avatar} alt={item.title} className="h-10 w-10 rounded-2xl border border-white/10 object-cover" /> : null}
          <div className="min-w-0 flex-1">
            <div className="text-sm font-semibold text-white">{item.title}</div>
            {item.subtitle ? <div className="text-xs uppercase tracking-[0.14em] text-slate-500">{item.subtitle}</div> : null}
            {item.description ? <div className="mt-1 line-clamp-2 text-xs text-slate-400">{item.description}</div> : null}
          </div>
        </button>
      ))}
    </div>
  )
}

function FieldError({ message }) {
  if (!message) return null
  return <p className="text-xs text-rose-300">{message}</p>
}

function normalizeNewTagName(value) {
  return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 80)
}

function normalizeNewsTagKey(value) {
  return normalizeNewTagName(value).toLowerCase()
}

function parseNewsTagList(input) {
  return String(input || '')
    .split(/[\n,]+/)
    .map((item) => normalizeNewTagName(item))
    .filter(Boolean)
}

function analyzePastedNewsTags(rawText, selectedKeys) {
  const parts = parseNewsTagList(rawText)
  const tagsToAdd = []
  const skippedDuplicates = []
  const skippedInvalid = []

  for (const part of parts) {
    const normalized = normalizeNewTagName(part)
    const key = normalizeNewsTagKey(normalized)

    if (!normalized || !key) {
      skippedInvalid.push(String(part || '').trim())
      continue
    }

    if (selectedKeys.includes(key) || tagsToAdd.some((item) => item.key === key)) {
      skippedDuplicates.push(normalized)
      continue
    }

    tagsToAdd.push({ key, name: normalized })
  }

  return {
    parsedCount: parts.length,
    tagsToAdd,
    skippedDuplicates,
    skippedInvalid,
  }
}

function SectionCard({ eyebrow, title, description, actions, children, tone = 'default' }) {
  const toneClass = tone === 'feature'
    ? 'bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.16),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] shadow-[0_24px_70px_rgba(2,6,23,0.28)]'
    : 'bg-white/[0.03]'

  return (
    <section className={`rounded-[28px] border border-white/10 p-5 ${toneClass}`}>
      <div className="flex flex-wrap items-start justify-between gap-4">
        <div className="max-w-3xl">
          {eyebrow ? <p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/75">{eyebrow}</p> : null}
          <h2 className="mt-2 text-xl font-semibold tracking-[-0.03em] text-white">{title}</h2>
          {description ? <p className="mt-2 text-sm leading-6 text-slate-400">{description}</p> : null}
        </div>
        {actions ? <div className="flex flex-wrap gap-2">{actions}</div> : null}
      </div>
      <div className="mt-5">{children}</div>
    </section>
  )
}

function NewsTagInputDialog({ open, preview, onClose, onConfirm }) {
  const backdropRef = useRef(null)

  useEffect(() => {
    if (!open) return undefined

    const handleKeyDown = (event) => {
      if (event.key === 'Escape') {
        onClose?.()
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [onClose, open])

  if (!open || !preview) return null

  return createPortal(
    <div
      ref={backdropRef}
      className="fixed inset-0 z-[9999] flex items-center justify-center bg-[#04070dcc] px-4 backdrop-blur-md"
      onClick={(event) => {
        if (event.target === backdropRef.current) {
          onClose?.()
        }
      }}
      role="presentation"
    >
      <div
        role="dialog"
        aria-modal="true"
        aria-labelledby="news-tag-import-title"
        className="w-full max-w-lg overflow-hidden rounded-3xl border border-white/10 bg-[linear-gradient(180deg,rgba(16,22,34,0.98),rgba(8,12,19,0.98))] shadow-[0_30px_80px_rgba(0,0,0,0.55)]"
      >
        <div className="border-b border-white/[0.06] bg-white/[0.02] px-6 py-5">
          <p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-white/35">Tag Import</p>
          <h3 id="news-tag-import-title" className="mt-2 text-lg font-semibold text-white">
            Add {preview.tagsToAdd.length} pasted tag{preview.tagsToAdd.length === 1 ? '' : 's'}?
          </h3>
          <p className="mt-2 text-sm leading-6 text-white/65">
            Parsed {preview.parsedCount} item{preview.parsedCount === 1 ? '' : 's'} from your paste. Confirm before adding them to the article.
          </p>
        </div>

        <div className="space-y-4 px-6 py-5">
          {(preview.skippedDuplicates.length > 0 || preview.skippedInvalid.length > 0) ? (
            <div className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-white/70">
              {preview.skippedDuplicates.length > 0 ? `${preview.skippedDuplicates.length} duplicate tag${preview.skippedDuplicates.length === 1 ? '' : 's'} ignored.` : ''}
              {preview.skippedDuplicates.length > 0 && preview.skippedInvalid.length > 0 ? ' ' : ''}
              {preview.skippedInvalid.length > 0 ? `${preview.skippedInvalid.length} invalid tag${preview.skippedInvalid.length === 1 ? '' : 's'} ignored.` : ''}
            </div>
          ) : null}

          <div>
            <p className="mb-3 text-xs font-semibold uppercase tracking-[0.18em] text-white/40">Tags to add</p>
            <div className="max-h-56 overflow-auto rounded-2xl border border-white/10 bg-white/[0.03] p-3">
              <div className="flex flex-wrap gap-2">
                {preview.tagsToAdd.map((tag) => (
                  <span
                    key={tag.key}
                    className="inline-flex items-center rounded-full border border-sky-300/25 bg-sky-400/10 px-3 py-1.5 text-xs font-medium text-sky-100"
                  >
                    {tag.name}
                  </span>
                ))}
              </div>
            </div>
          </div>
        </div>

        <div className="flex items-center justify-end gap-3 border-t border-white/[0.06] px-6 py-4">
          <button
            type="button"
            onClick={() => onClose?.()}
            className="inline-flex items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition hover:bg-white/[0.08] hover:text-white"
          >
            Cancel
          </button>
          <button
            type="button"
            onClick={() => onConfirm?.()}
            className="inline-flex items-center justify-center rounded-full border border-sky-300/25 bg-sky-400/90 px-4 py-2 text-sm font-semibold text-slate-950 transition hover:brightness-110"
          >
            Add tags
          </button>
        </div>
      </div>
    </div>,
    document.body,
  )
}

function NewsTagInput({ options, selectedIds, newTagNames, onSelectedIdsChange, onNewTagNamesChange, manageUrl, maxNewTags = NEWS_NEW_TAG_LIMIT }) {
  const [query, setQuery] = useState('')
  const [isOpen, setIsOpen] = useState(false)
  const [highlightedIndex, setHighlightedIndex] = useState(-1)
  const [error, setError] = useState('')
  const [pastePreview, setPastePreview] = useState(null)

  const selectedIdSet = useMemo(() => new Set((selectedIds || []).map((id) => Number(id))), [selectedIds])
  const existingTags = useMemo(() => (Array.isArray(options) ? options : []).filter((tag) => selectedIdSet.has(Number(tag.id))), [options, selectedIdSet])
  const pendingTags = useMemo(() => (Array.isArray(newTagNames) ? newTagNames : []).map((name) => ({ key: normalizeNewsTagKey(name), name: normalizeNewTagName(name) })).filter((tag) => tag.key), [newTagNames])
  const combinedNames = useMemo(() => [...existingTags.map((tag) => String(tag.name || '')), ...pendingTags.map((tag) => tag.name)], [existingTags, pendingTags])
  const combinedKeys = useMemo(() => combinedNames.map((name) => normalizeNewsTagKey(name)).filter(Boolean), [combinedNames])
  const newTagLimit = Math.max(0, Number(maxNewTags || NEWS_NEW_TAG_LIMIT))
  const remainingNewTagSlots = Math.max(0, newTagLimit - pendingTags.length)
  const normalizedQuery = useMemo(() => normalizeNewTagName(query), [query])

  const syncNames = useCallback((names) => {
    const seen = new Set()
    const nextIds = []
    const nextNewNames = []

    names.forEach((rawName) => {
      const nextName = normalizeNewTagName(rawName)
      const key = normalizeNewsTagKey(nextName)

      if (!key || seen.has(key)) {
        return
      }

      seen.add(key)

      const existing = (Array.isArray(options) ? options : []).find((tag) => normalizeNewsTagKey(tag.name) === key)
      if (existing) {
        nextIds.push(Number(existing.id))
        return
      }

      nextNewNames.push(nextName)
    })

    onSelectedIdsChange(nextIds)
    onNewTagNamesChange(nextNewNames)
  }, [onNewTagNamesChange, onSelectedIdsChange, options])

  const exactMatch = useMemo(() => {
    if (!normalizedQuery) return null
    return (Array.isArray(options) ? options : []).find((tag) => normalizeNewsTagKey(tag.name) === normalizeNewsTagKey(normalizedQuery)) || null
  }, [normalizedQuery, options])

  const suggestions = useMemo(() => {
    const source = Array.isArray(options) ? options : []
    const lowerQuery = normalizeNewsTagKey(query)

    return source
      .filter((tag) => !selectedIdSet.has(Number(tag.id)))
      .filter((tag) => !pendingTags.some((pendingTag) => pendingTag.key === normalizeNewsTagKey(tag.name)))
      .filter((tag) => (lowerQuery === '' ? true : normalizeNewsTagKey(tag.name).includes(lowerQuery)))
      .slice(0, 8)
  }, [options, pendingTags, query, selectedIdSet])

  useEffect(() => {
    setHighlightedIndex(suggestions.length > 0 ? 0 : -1)
  }, [suggestions])

  const addCandidate = useCallback((rawName) => {
    const nextName = normalizeNewTagName(rawName)
    if (!nextName) {
      return
    }

    const key = normalizeNewsTagKey(nextName)
    if (combinedKeys.includes(key)) {
      setError('Duplicate tag')
      return
    }

    if (pendingTags.length >= newTagLimit) {
      setError(`You can add up to ${newTagLimit} new tags per article.`)
      return
    }

    setError('')
    syncNames([...combinedNames, nextName])
    setQuery('')
    setIsOpen(false)
  }, [combinedKeys, combinedNames, newTagLimit, pendingTags.length, syncNames])

  const removeExisting = useCallback((tagId) => {
    onSelectedIdsChange((selectedIds || []).filter((id) => Number(id) !== Number(tagId)))
  }, [onSelectedIdsChange, selectedIds])

  const removePending = useCallback((tagName) => {
    onNewTagNamesChange((newTagNames || []).filter((name) => normalizeNewsTagKey(name) !== normalizeNewsTagKey(tagName)))
  }, [newTagNames, onNewTagNamesChange])

  const handlePaste = useCallback((event) => {
    const raw = event.clipboardData?.getData('text')
    if (!raw) return

    const parts = parseNewsTagList(raw)
    if (parts.length <= 1) return

    event.preventDefault()
    const preview = analyzePastedNewsTags(raw, combinedKeys)
    if (preview.tagsToAdd.length === 0) {
      setError('No new tags found in pasted text')
      return
    }

    if (preview.tagsToAdd.length > remainingNewTagSlots) {
      setError(`You can only add ${remainingNewTagSlots} more new tag${remainingNewTagSlots === 1 ? '' : 's'} to this article.`)
      return
    }

    setError('')
    setPastePreview(preview)
  }, [combinedKeys, remainingNewTagSlots])

  const handleConfirmPaste = useCallback(() => {
    if (!pastePreview) return
    syncNames([...combinedNames, ...pastePreview.tagsToAdd.map((tag) => tag.name)])
    setPastePreview(null)
    setQuery('')
    setIsOpen(false)
  }, [combinedNames, pastePreview, syncNames])

  const handleKeyDown = useCallback((event) => {
    if (event.key === 'Escape') {
      setIsOpen(false)
      return
    }

    if (event.key === 'Backspace' && query.length === 0 && combinedNames.length > 0) {
      const lastPending = pendingTags[pendingTags.length - 1]
      if (lastPending) {
        removePending(lastPending.name)
        return
      }

      const lastExisting = existingTags[existingTags.length - 1]
      if (lastExisting) {
        removeExisting(lastExisting.id)
      }
      return
    }

    if (event.key === 'ArrowDown') {
      event.preventDefault()
      if (suggestions.length === 0) return
      setIsOpen(true)
      setHighlightedIndex((current) => Math.min(current + 1, suggestions.length - 1))
      return
    }

    if (event.key === 'ArrowUp') {
      event.preventDefault()
      if (suggestions.length === 0) return
      setIsOpen(true)
      setHighlightedIndex((current) => Math.max(current - 1, 0))
      return
    }

    if (!['Enter', ',', 'Tab'].includes(event.key)) {
      return
    }

    if (event.key === 'Tab' && !isOpen && normalizedQuery === '') {
      return
    }

    event.preventDefault()

    if ((event.key === 'Enter' || event.key === 'Tab') && isOpen && highlightedIndex >= 0 && suggestions[highlightedIndex]) {
      addCandidate(suggestions[highlightedIndex].name)
      return
    }

    if (!normalizedQuery) {
      return
    }

    if (exactMatch) {
      addCandidate(exactMatch.name)
      return
    }

    addCandidate(normalizedQuery)
  }, [addCandidate, combinedNames.length, exactMatch, existingTags, highlightedIndex, isOpen, normalizedQuery, pendingTags, query.length, removeExisting, removePending, suggestions])

  return (
    <>
      <div className="grid gap-4">
        <div className="rounded-[24px] border border-white/10 bg-black/20 p-4">
          <div className="flex items-center justify-between gap-3">
            <div>
              <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Selected tags</div>
              <div className="mt-1 text-sm text-slate-400">Attach article topics with the same chip-based flow used on artwork tags.</div>
            </div>
            {manageUrl ? <a href={manageUrl} className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.14em] text-white">Manage tags</a> : null}
          </div>

          <div className="mt-4 min-h-[3rem] rounded-xl border border-white/10 bg-white/5 p-2">
            <div className="flex flex-wrap gap-2">
              {existingTags.length === 0 && pendingTags.length === 0 ? <span className="px-2 py-1 text-xs text-white/50">No tags selected</span> : null}
              {existingTags.map((tag) => (
                <span key={tag.id} className="group inline-flex max-w-full items-center gap-2 rounded-full border border-white/20 bg-slate-900/80 px-3 py-1.5 text-xs text-slate-100">
                  <span className="truncate">{tag.name}</span>
                  <button type="button" onClick={() => removeExisting(tag.id)} className="rounded-full p-0.5 text-slate-300 transition hover:bg-white/10 hover:text-white" aria-label={`Remove tag ${tag.name}`}>
                    ✕
                  </button>
                </span>
              ))}
              {pendingTags.map((tag) => (
                <span key={tag.key} className="group inline-flex max-w-full items-center gap-2 rounded-full border border-emerald-300/25 bg-emerald-400/10 px-3 py-1.5 text-xs text-emerald-50">
                  <span className="truncate">{tag.name}</span>
                  <span className="rounded-full border border-emerald-200/30 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-emerald-100/80">New</span>
                  <button type="button" onClick={() => removePending(tag.name)} className="rounded-full p-0.5 text-emerald-100/75 transition hover:bg-white/10 hover:text-white" aria-label={`Remove new tag ${tag.name}`}>
                    ✕
                  </button>
                </span>
              ))}
            </div>
          </div>

          <div className={`mt-3 rounded-2xl border px-3 py-2 text-xs ${remainingNewTagSlots === 0 ? 'border-amber-300/25 bg-amber-400/10 text-amber-100' : remainingNewTagSlots <= 3 ? 'border-amber-300/20 bg-amber-400/8 text-amber-100' : 'border-white/10 bg-white/[0.03] text-slate-400'}`}>
            {remainingNewTagSlots === 0
              ? `New tag limit reached: up to ${newTagLimit} new tags can be staged for one article.`
              : `${pendingTags.length}/${newTagLimit} new tags staged. ${remainingNewTagSlots} slot${remainingNewTagSlots === 1 ? '' : 's'} left.`}
          </div>
        </div>

        <div className="rounded-[24px] border border-white/10 bg-black/20 p-4">
          <label className="grid gap-2 text-sm text-slate-300">
            <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Find tags</span>
            <input
              value={query}
              onChange={(event) => {
                setQuery(event.target.value)
                setError('')
                setIsOpen(true)
              }}
              onFocus={() => setIsOpen(true)}
              onKeyDown={handleKeyDown}
              onPaste={handlePaste}
              placeholder="Search existing tags or type a new one"
              className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2 text-sm text-white placeholder:text-white/45 focus:border-sky-400 focus:outline-none"
              aria-label="Search or add news tags"
            />
          </label>

          {isOpen ? (
            <div className="mt-3 overflow-hidden rounded-xl bg-slate-950/98 shadow-xl shadow-black/50 ring-1 ring-white/10">
              <ul role="listbox" className="max-h-56 overflow-auto py-1">
                {suggestions.length > 0 ? suggestions.map((tag, index) => {
                  const active = index === highlightedIndex
                  return (
                    <li
                      key={tag.id}
                      role="option"
                      aria-selected={active}
                      className={`flex cursor-pointer items-center justify-between gap-2 px-3 py-2 text-sm transition ${active ? 'bg-sky-500/20 text-white' : 'text-white/85 hover:bg-white/10'}`}
                      onMouseDown={(event) => {
                        event.preventDefault()
                        addCandidate(tag.name)
                      }}
                    >
                      <span className="truncate">{tag.name}</span>
                    </li>
                  )
                }) : (
                  <li className="px-3 py-2 text-xs text-white/50">No suggestions</li>
                )}
              </ul>
            </div>
          ) : null}

          <div className="mt-3 flex items-center justify-between gap-3 text-xs">
            <span className={error ? 'text-amber-200' : 'text-white/55'} role="status" aria-live="polite">
              {error || `Type and press Enter, comma, or Tab to add. Paste a comma-separated list to review multiple tags. Up to ${newTagLimit} new tags can be staged.`}
            </span>
            <span className="text-white/50">{existingTags.length + pendingTags.length} selected</span>
          </div>
        </div>
      </div>

      <NewsTagInputDialog
        open={Boolean(pastePreview)}
        preview={pastePreview}
        onClose={() => setPastePreview(null)}
        onConfirm={handleConfirmPaste}
      />
    </>
  )
}

function RelationCard({ relation, index, onChange, onRemove, onSearch, results, relationTypeOptions }) {
  const isSourceRelation = String(relation.entity_type || '').trim().toLowerCase() === 'source'

  return (
    <div className="rounded-[24px] border border-white/10 bg-black/20 p-4">
        <div className="grid gap-4 lg:grid-cols-[180px_minmax(0,1fr)_auto] lg:items-end">
        <div className="grid gap-2 text-sm text-slate-300">
          <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Type</span>
          <NovaSelect value={relation.entity_type} onChange={(val) => onChange(index, { ...relation, entity_type: val, entity_id: '', external_url: '', preview: null, query: '' })} options={relationTypeOptions} searchable={false} />
        </div>
        {isSourceRelation ? (
          <label className="grid gap-2 text-sm text-slate-300">
            <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Source URL</span>
            <input
              value={relation.external_url || ''}
              onChange={(event) => onChange(index, { ...relation, external_url: event.target.value, query: event.target.value, entity_id: '' })}
              placeholder="https://example.com/original-article"
              className="min-w-0 flex-1 rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none"
            />
          </label>
        ) : (
          <label className="grid gap-2 text-sm text-slate-300">
            <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Search entity</span>
            <div className="flex gap-2">
              <input value={relation.query || ''} onChange={(event) => onChange(index, { ...relation, query: event.target.value })} placeholder="Search by name, slug, or title" className="min-w-0 flex-1 rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
              <button type="button" onClick={() => onSearch(index)} className="rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm font-semibold text-white">Search</button>
            </div>
          </label>
        )}
        <button type="button" onClick={() => onRemove(index)} className="rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100">Remove</button>
      </div>

      {!isSourceRelation && relation.preview ? (
        <div className="mt-4 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 p-4 text-sm text-emerald-50">
          <div className="font-semibold">Linked: {relation.preview.title}</div>
          {relation.preview.subtitle ? <div className="mt-1 text-xs uppercase tracking-[0.14em] text-emerald-100/70">{relation.preview.subtitle}</div> : null}
        </div>
      ) : null}

      {isSourceRelation ? (
        <div className="mt-4 rounded-2xl border border-sky-300/15 bg-sky-400/10 px-4 py-3 text-sm text-sky-100/90">
          Source relations store a direct external URL instead of an internal Nova entity ID.
        </div>
      ) : (
        <div className="mt-4">
          <SearchResultList items={results} onSelect={(item) => onChange(index, { ...relation, entity_id: item.id, preview: item, query: item.title })} emptyLabel="Search to attach a related entity." />
        </div>
      )}

      <label className="mt-4 grid gap-2 text-sm text-slate-300">
        <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Context label</span>
        <input value={relation.context_label || ''} onChange={(event) => onChange(index, { ...relation, context_label: event.target.value })} placeholder="Featured release, Meet the creator, Join this challenge…" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
      </label>
    </div>
  )
}

function stripHtml(value) {
  return String(value || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
}

function unwrapMarkdownLinkUrl(value) {
  const raw = String(value || '').trim()
  if (!raw) return ''

  const markdownMatch = raw.match(/^\[[^\]]+\]\((https?:\/\/[^)]+)\)$/i)
  if (markdownMatch) {
    return String(markdownMatch[1] || '').trim()
  }

  return raw
}

function isSourceRelationType(entityType) {
  return String(entityType || '').trim().toLowerCase() === 'source'
}

const NEWS_NEW_TAG_LIMIT = 30

function slugifyNewsTitle(value) {
  return String(value || '')
    .normalize('NFKD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 180)
}

function selectOptionsFromValues(options, emptyLabel = null) {
  const base = Array.isArray(options)
    ? options.map((option) => ({
      value: String(option.value ?? option.id),
      label: option.label ?? option.name,
    }))
    : []

  return emptyLabel ? [{ value: '', label: emptyLabel }, ...base] : base
}

function buildSubmitPayload(data) {
  return {
    title: String(data.title || '').trim(),
    slug: String(data.slug || '').trim(),
    excerpt: String(data.excerpt || ''),
    content: String(data.content || ''),
    cover_image: String(data.cover_image || '').trim(),
    type: String(data.type || ''),
    category_id: data.category_id === '' || data.category_id == null ? null : Number(data.category_id),
    author_id: data.author_id === '' || data.author_id == null ? null : Number(data.author_id),
    editorial_status: String(data.editorial_status || ''),
    published_at: data.published_at ? String(data.published_at) : null,
    is_featured: Boolean(data.is_featured),
    is_pinned: Boolean(data.is_pinned),
    comments_enabled: Boolean(data.comments_enabled),
    tag_ids: Array.isArray(data.tag_ids) ? data.tag_ids.map((id) => Number(id)).filter(Boolean) : [],
    new_tag_names: Array.isArray(data.new_tag_names) ? data.new_tag_names.map((name) => normalizeNewTagName(name)).filter(Boolean) : [],
    meta_title: String(data.meta_title || ''),
    meta_description: String(data.meta_description || ''),
    meta_keywords: String(data.meta_keywords || ''),
    og_title: String(data.og_title || ''),
    og_description: String(data.og_description || ''),
    og_image: String(data.og_image || '').trim(),
    relations: Array.isArray(data.relations)
      ? data.relations.map((relation) => ({
        entity_type: String(relation.entity_type || '').trim(),
        entity_id: isSourceRelationType(relation.entity_type) || relation.entity_id === '' || relation.entity_id == null ? '' : Number(relation.entity_id),
        external_url: isSourceRelationType(relation.entity_type) ? unwrapMarkdownLinkUrl(relation.external_url || relation.entity_id || relation.query || '') : '',
        context_label: String(relation.context_label || '').trim(),
      }))
      : [],
  }
}

function hasRequiredCategory(categoryId) {
  if (categoryId === '' || categoryId == null) {
    return false
  }

  return Number(categoryId) > 0
}

function getDraftValue(source, key, fallback = '') {
  if (source && Object.prototype.hasOwnProperty.call(source, key)) {
    const val = source[key]
    return val != null ? val : fallback
  }

  return fallback
}

function buildInitialFormData(article, defaultAuthor, defaultPublishedAt, typeOptions, oldInput = {}) {
  return {
    title: String(getDraftValue(oldInput, 'title', article.title || '')),
    slug: String(getDraftValue(oldInput, 'slug', article.slug || '')),
    excerpt: String(getDraftValue(oldInput, 'excerpt', article.excerpt || '')),
    content: String(getDraftValue(oldInput, 'content', article.content || '')),
    cover_image: String(getDraftValue(oldInput, 'cover_image', article.cover_image || '')),
    type: String(getDraftValue(oldInput, 'type', article.type || (typeOptions?.[0]?.value || 'announcement'))),
    category_id: String(getDraftValue(oldInput, 'category_id', article.category_id ? String(article.category_id) : '')),
    author_id: getDraftValue(oldInput, 'author_id', article.author_id || defaultAuthor?.id || ''),
    editorial_status: String(getDraftValue(oldInput, 'editorial_status', article.editorial_status || 'draft')),
    published_at: String(getDraftValue(oldInput, 'published_at', article.published_at ? String(article.published_at).slice(0, 16) : (defaultPublishedAt || ''))),
    is_featured: parseBooleanish(getDraftValue(oldInput, 'is_featured', Boolean(article.is_featured))),
    is_pinned: parseBooleanish(getDraftValue(oldInput, 'is_pinned', Boolean(article.is_pinned))),
    comments_enabled: parseBooleanish(getDraftValue(oldInput, 'comments_enabled', article.id ? Boolean(article.comments_enabled) : true)),
    tag_ids: Array.isArray(getDraftValue(oldInput, 'tag_ids', article.tag_ids)) ? getDraftValue(oldInput, 'tag_ids', article.tag_ids) : [],
    new_tag_names: Array.isArray(getDraftValue(oldInput, 'new_tag_names', [])) ? getDraftValue(oldInput, 'new_tag_names', []) : [],
    meta_title: String(getDraftValue(oldInput, 'meta_title', article.meta_title || '')),
    meta_description: String(getDraftValue(oldInput, 'meta_description', article.meta_description || '')),
    meta_keywords: String(getDraftValue(oldInput, 'meta_keywords', article.meta_keywords || '')),
    og_title: String(getDraftValue(oldInput, 'og_title', article.og_title || '')),
    og_description: String(getDraftValue(oldInput, 'og_description', article.og_description || '')),
    og_image: String(getDraftValue(oldInput, 'og_image', article.og_image || '')),
    relations: Array.isArray(getDraftValue(oldInput, 'relations', article.relations)) ? getDraftValue(oldInput, 'relations', article.relations).map((relation) => ({
      entity_type: relation.entity_type || 'group',
      entity_id: isSourceRelationType(relation.entity_type) ? '' : (relation.entity_id || ''),
      external_url: isSourceRelationType(relation.entity_type) ? unwrapMarkdownLinkUrl(relation.external_url || relation.entity_id || relation.query || '') : '',
      context_label: relation.context_label || '',
      preview: relation.preview || null,
      query: isSourceRelationType(relation.entity_type)
        ? unwrapMarkdownLinkUrl(relation.external_url || relation.entity_id || relation.query || '')
        : (relation.preview?.title || relation.query || ''),
    })) : [],
  }
}

const NEWS_EDITOR_TABS = [
  {
    id: 'content',
    label: 'Main content',
    description: 'Headline, excerpt, cover, and article body.',
  },
  {
    id: 'publishing',
    label: 'Publishing',
    description: 'Category, author, scheduling, and visibility.',
  },
  {
    id: 'discoverability',
    label: 'Social + SEO',
    description: 'Tags, metadata, and social preview fields.',
  },
  {
    id: 'connections',
    label: 'Connections',
    description: 'Related entities and structured import.',
  },
]

function parseBooleanish(value) {
  if (typeof value === 'boolean') return value
  if (typeof value === 'number') return value !== 0

  const normalized = String(value || '').trim().toLowerCase()
  if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
  if (['0', 'false', 'no', 'off'].includes(normalized)) return false
  return Boolean(value)
}

function normalizeImportedStringArray(value) {
  if (Array.isArray(value)) {
    return value.map((item) => String(item || '').trim()).filter(Boolean)
  }

  return String(value || '')
    .split(/[\n,]+/)
    .map((item) => item.trim())
    .filter(Boolean)
}

function normalizeImportedTagList(value) {
  if (!Array.isArray(value)) {
    return normalizeImportedStringArray(value)
  }

  return value
    .map((item) => {
      if (typeof item === 'string' || typeof item === 'number') {
        return normalizeNewTagName(item)
      }

      if (item && typeof item === 'object') {
        return normalizeNewTagName(item.name ?? item.title ?? item.label ?? item.slug ?? '')
      }

      return normalizeNewTagName(item)
    })
    .filter(Boolean)
}

function normalizeImportedDateTime(value) {
  const raw = String(value || '').trim()
  if (!raw) return ''

  const dateTimeMatch = raw.match(/^(\d{4}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2})(?::\d{2})?)?$/)
  if (dateTimeMatch) {
    return dateTimeMatch[2] ? `${dateTimeMatch[1]}T${dateTimeMatch[2]}` : dateTimeMatch[1]
  }

  const parsed = new Date(raw)
  if (Number.isNaN(parsed.getTime())) {
    return raw
  }

  const pad = (input) => String(input).padStart(2, '0')

  return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())}T${pad(parsed.getHours())}:${pad(parsed.getMinutes())}`
}

function parseStructuredNewsImport(rawValue, context) {
  const parsed = JSON.parse(String(rawValue || '').trim())
  const categoryOptions = Array.isArray(context.categoryOptions) ? context.categoryOptions : []
  const tagOptions = Array.isArray(context.tagOptions) ? context.tagOptions : []
  const typeOptions = Array.isArray(context.typeOptions) ? context.typeOptions : []
  const statusOptions = Array.isArray(context.statusOptions) ? context.statusOptions : []
  const next = {}
  const applied = []

  const applyString = (inputKey, formKey = inputKey) => {
    if (parsed[inputKey] == null) return
    next[formKey] = String(parsed[inputKey])
    applied.push(formKey)
  }

  const applyBoolean = (inputKey, formKey = inputKey) => {
    if (parsed[inputKey] == null) return
    next[formKey] = parseBooleanish(parsed[inputKey])
    applied.push(formKey)
  }

  applyString('title')
  applyString('slug')
  applyString('excerpt')
  applyString('content')
  applyString('cover_image')
  if (parsed.published_at != null) {
    next.published_at = normalizeImportedDateTime(parsed.published_at)
    applied.push('published_at')
  }
  applyString('meta_title')
  applyString('meta_description')
  applyString('meta_keywords')
  applyString('og_title')
  applyString('og_description')

  if (parsed.type != null) {
    const requested = String(parsed.type).trim().toLowerCase()
    const match = typeOptions.find((option) => String(option.value ?? option.id ?? '').trim().toLowerCase() === requested || String(option.label ?? option.name ?? '').trim().toLowerCase() === requested)
    next.type = match ? String(match.value ?? match.id ?? '') : String(parsed.type)
    applied.push('type')
  }

  if (parsed.editorial_status != null) {
    const requested = String(parsed.editorial_status).trim().toLowerCase()
    const match = statusOptions.find((option) => String(option.value ?? option.id ?? '').trim().toLowerCase() === requested || String(option.label ?? option.name ?? '').trim().toLowerCase() === requested)
    next.editorial_status = match ? String(match.value ?? match.id ?? '') : String(parsed.editorial_status)
    applied.push('editorial_status')
  }

  if (parsed.category_id != null || parsed.category != null || parsed.category_slug != null) {
    const requested = String(parsed.category_id ?? parsed.category_slug ?? parsed.category).trim().toLowerCase()
    const match = categoryOptions.find((option) => [option.id, option.value, option.slug, option.name, option.label]
      .map((candidate) => String(candidate ?? '').trim().toLowerCase())
      .includes(requested))

    if (match) {
      next.category_id = String(match.id ?? match.value ?? '')
      applied.push('category_id')
    }
  }

  if (parsed.author_id != null) {
    next.author_id = String(parsed.author_id)
    applied.push('author_id')
  }

  applyBoolean('is_featured')
  applyBoolean('is_pinned')
  applyBoolean('comments_enabled')

  const tagNames = normalizeImportedTagList(parsed.tags ?? parsed.tag_names)
  const tagIds = Array.isArray(parsed.tag_ids) ? parsed.tag_ids.map((item) => Number(item)).filter(Boolean) : []
  if (tagNames.length > 0 || tagIds.length > 0) {
    const existingIds = new Set(tagIds)
    const newTagNames = []

    tagNames.forEach((tagName) => {
      const normalized = normalizeNewsTagKey(tagName)
      const match = tagOptions.find((option) => normalizeNewsTagKey(option.name) === normalized)

      if (match) {
        existingIds.add(Number(match.id))
      } else {
        newTagNames.push(normalizeNewTagName(tagName))
      }
    })

    next.tag_ids = Array.from(existingIds)
    next.new_tag_names = newTagNames
    applied.push('tag_ids', 'new_tag_names')
  }

  if (Array.isArray(parsed.relations)) {
    next.relations = parsed.relations
      .map((relation) => {
        const entityType = String(relation?.entity_type || relation?.type || 'group').trim()
        const externalUrl = isSourceRelationType(entityType)
          ? unwrapMarkdownLinkUrl(relation?.external_url || relation?.url || relation?.entity_id || relation?.query || relation?.title || '')
          : ''

        return {
          entity_type: entityType,
          entity_id: isSourceRelationType(entityType) || relation?.entity_id == null || relation?.entity_id === '' ? '' : Number(relation.entity_id),
          external_url: externalUrl,
          context_label: String(relation?.context_label || relation?.label || '').trim(),
          preview: null,
          query: isSourceRelationType(entityType) ? externalUrl : String(relation?.query || relation?.title || '').trim(),
        }
      })
      .filter((relation) => relation.entity_type)
    applied.push('relations')
  }

  return {
    next,
    applied,
    authorQuery: parsed.author_query != null ? String(parsed.author_query) : (parsed.author_name != null ? String(parsed.author_name) : null),
  }
}

let newsMarkdownTurndown = null
let newsMarkdownTurndownPromise = null

async function loadNewsMarkdownTurndown() {
  if (newsMarkdownTurndown) {
    return newsMarkdownTurndown
  }

  if (typeof window === 'undefined') {
    return null
  }

  if (!newsMarkdownTurndownPromise) {
    newsMarkdownTurndownPromise = import('turndown')
      .then(({ default: TurndownService }) => new TurndownService({
        headingStyle: 'atx',
        codeBlockStyle: 'fenced',
        bulletListMarker: '-',
      }))
      .then((service) => {
        newsMarkdownTurndown = service
        return service
      })
      .catch(() => null)
  }

  return newsMarkdownTurndownPromise
}

function findNewsOptionById(options, value) {
  const normalized = String(value || '').trim()
  if (!normalized) return null

  return (Array.isArray(options) ? options : []).find((option) => String(option.id ?? option.value ?? '').trim() === normalized) || null
}

function findNewsTagsByIds(options, ids) {
  const idSet = new Set((Array.isArray(ids) ? ids : []).map((id) => Number(id)))

  return (Array.isArray(options) ? options : [])
    .filter((option) => idSet.has(Number(option.id)))
    .map((option) => ({
      id: Number(option.id),
      name: String(option.name || option.label || ''),
      slug: String(option.slug || ''),
    }))
}

function buildStructuredPlainTextExport(data) {
  const lines = []

  if (data.title) lines.push(`Title: ${data.title}`)
  if (data.excerpt) lines.push(`Excerpt: ${data.excerpt}`)
  if (data.date) lines.push(`Date: ${data.date}`)
  if (data.category) lines.push(`Category: ${data.category}`)

  if (data.body) {
    lines.push('')
    lines.push('Body:')
    lines.push(data.body)
  }

  return lines.join('\n').trim()
}

function convertNewsHtmlToMarkdown(value) {
  const html = String(value || '').trim()
  if (!html) return ''

  if (!newsMarkdownTurndown) {
    return stripHtml(html)
  }

  return newsMarkdownTurndown.turndown(html).trim()
}

function buildNewsMarkdownExport(data) {
  const lines = []

  if (data.title) {
    lines.push(`# ${data.title}`)
  }

  if (data.excerpt) {
    lines.push(data.excerpt)
  }

  if (data.date) {
    lines.push(`- Date: ${data.date}`)
  }

  if (data.category) {
    lines.push(`- Category: ${data.category}`)
  }

  const bodyMarkdown = convertNewsHtmlToMarkdown(data.body_html)
  if (bodyMarkdown) {
    lines.push(bodyMarkdown)
  }

  return lines.join('\n\n').trim()
}

// ── News image prompt builder ────────────────────────────────────────────────

const NEWS_PROMPT_TYPE_MOODS = {
  announcement: 'Futuristic',
  release: 'Software Release',
  editorial: 'Editorial',
  opinion: 'Editorial',
  tutorial: 'Clean Instructional',
  platform_update: 'Modern Tech',
  event: 'Futuristic',
  challenge: 'Futuristic',
  interview: 'Editorial',
  spotlight: 'Editorial',
  archive: 'Retro Tech',
  industry_news: 'Modern Tech',
  review: 'Modern Tech',
  roundup: 'Modern Tech',
}

const NEWS_PROMPT_TYPE_ADDONS = {
  release: 'Use a glossy software-release poster style with product UI panels, feature highlights, and a polished launch atmosphere.',
  announcement: 'Use a clean announcement-poster style with a strong headline, clear hero image, and supporting modules that communicate the main update quickly.',
  editorial: 'Use a refined editorial magazine-cover style with a strong visual concept, cleaner composition, and slightly more cinematic atmosphere.',
  opinion: 'Use a refined editorial magazine-cover style with a strong visual concept, cleaner composition, and slightly more cinematic atmosphere.',
  event: 'Use a conference or event-poster style with keynote energy, glowing screens, stage-like lighting, and a premium event atmosphere.',
  tutorial: 'Use a clear structured instructional poster style with organized UI panels, workflow callouts, and helpful visual hierarchy.',
  platform_update: 'Use a modern platform-update style with system UI visuals, feature modules, and a polished ecosystem presentation.',
  archive: 'Use a retro-tech editorial style inspired by early 2000s computer magazines, with classic hardware, vintage UI influences, and modern polished lighting.',
}

const NEWS_PROMPT_KEYWORD_PATTERNS = [
  {
    keywords: ['apple', 'wwdc', 'ios', 'macos', 'iphone', 'ipad', 'swift'],
    addon: 'Use a sleek developer-conference atmosphere with modern device screens, app ecosystem visuals, and a premium keynote mood.',
  },
  {
    keywords: ['google', 'gemini', 'google i/o', 'android', 'pixel', 'tensorflow'],
    addon: 'Use a colorful futuristic creative AI studio style with glowing panels, image and video creation tools, search elements, and generative media visuals.',
  },
  {
    keywords: ['intel', 'amd', 'processor', 'cpu', 'gpu', 'nvidia', 'radeon', 'chip'],
    addon: 'Use a retro computing hardware feature style with processor chips, technical callouts, old-school PC references, and magazine-cover energy.',
  },
  {
    keywords: ['skin', 'theme', 'desktop', 'customize', 'customization', 'rainmeter', 'widget'],
    addon: 'Use a desktop customization promo style with theme previews, icon panels, widget windows, and a glossy desktop software aesthetic.',
  },
  {
    keywords: ['ai', 'artificial intelligence', 'llm', 'chatgpt', 'openai', 'midjourney', 'stable diffusion', 'generative'],
    addon: 'Use a colorful futuristic creative AI studio style with glowing panels, generative media outputs, neural network visuals, and advanced AI tool interfaces.',
  },
]

function resolveNewsPromptHeadline(data) {
  return String(data.title || data.meta_title || '').trim() || 'Skinbase News'
}

function resolveNewsPromptSubheadline(data) {
  const raw = String(data.excerpt || data.meta_description || '').replace(/<[^>]*>/g, '').trim()
  if (raw) {
    const words = raw.split(/\s+/)
    return words.slice(0, 18).join(' ') + (words.length > 18 ? '…' : '')
  }
  const plain = String(data.content || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
  if (plain) {
    const sentence = plain.split(/[.!?]/)[0].trim()
    if (sentence.length > 10) {
      const words = sentence.split(/\s+/)
      return words.slice(0, 18).join(' ')
    }
  }
  return 'Latest technology and creative industry update'
}

function resolveNewsPromptTopic(data) {
  const parts = []
  const cat = String(data.category || '').trim()
  if (cat) parts.push(cat)
  const tagList = (Array.isArray(data.tag_names) ? data.tag_names : []).slice(0, 5).filter(Boolean)
  if (tagList.length) parts.push(tagList.join(', '))
  if (!parts.length) {
    const words = String(data.title || '').split(/\s+/).filter((w) => w.length > 3).slice(0, 4)
    if (words.length) parts.push(words.join(' '))
  }
  return parts.join(' · ') || 'Technology and digital culture news'
}

function resolveNewsPromptType(data) {
  const raw = String(data.type || '').trim()
  if (!raw) return 'News'
  return raw.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
}

function resolveNewsPromptHeroSubject(data) {
  const title = String(data.title || '').toLowerCase()
  const tags = (Array.isArray(data.tag_names) ? data.tag_names : []).join(' ').toLowerCase()
  const combined = `${title} ${tags}`
  const type = String(data.type || '').toLowerCase()

  if (/apple|wwdc|ios|macos/.test(combined)) return 'sleek developer conference scene with modern Apple devices, app ecosystem screens, and a keynote stage atmosphere'
  if (/google|gemini|google i\/o/.test(combined)) return 'futuristic creative AI workspace with Google AI tools, image and video generation screens, and colorful generative panels'
  if (/intel|amd|cpu|processor|gpu|nvidia|radeon/.test(combined)) return 'high-detail processor chip and PC hardware setup with technical callouts and magazine-style editorial framing'
  if (/\bai\b|artificial intelligence|llm|chatgpt|openai|midjourney|stable diffusion/.test(combined)) return 'futuristic AI creative studio with generative media outputs, neural network interfaces, and glowing AI panels'
  if (/skin|theme|desktop|customiz|rainmeter|widget/.test(combined)) return 'polished desktop customization interface with theme previews, icon panels, and widget windows on a dark desktop'
  if (/game|gaming/.test(combined)) return 'immersive gaming setup or game UI with dynamic lighting, modern peripherals, and a premium game atmosphere'
  if (/microsoft|windows/.test(combined)) return 'modern Windows interface with system UI panels, taskbar, settings, and a polished OS environment'
  if (type === 'tutorial') return 'organized instructional workflow panel with step-by-step UI callouts and visual hierarchy'
  if (type === 'event') return 'keynote conference stage with large screens, glowing hall, and event atmosphere'
  if (type === 'archive') return 'retro computing hardware from the early 2000s with classic monitors and vintage PC aesthetic'
  return 'professional editorial tech workspace with software screens, feature panels, and a polished digital newsroom atmosphere'
}

function resolveNewsPromptSupportingModules(data) {
  const type = String(data.type || '').toLowerCase()
  const title = String(data.title || '').toLowerCase()
  const tags = (Array.isArray(data.tag_names) ? data.tag_names : []).join(' ').toLowerCase()
  const combined = `${title} ${tags}`

  if (type === 'release' || /release|launch|version/.test(combined)) return 'version badge, feature highlight cards, changelog strip, UI screenshots, product icon panels'
  if (type === 'tutorial') return 'step-by-step panels, UI callouts, workflow arrows, numbered feature blocks'
  if (type === 'event') return 'schedule panels, speaker cards, keynote countdown, location badge, feature preview cards'
  if (type === 'archive') return 'retro spec badges, vintage hardware panels, timeline strip, era-appropriate UI screenshots'
  if (/\bai\b|artificial intelligence|generative/.test(combined)) return 'AI feature cards, generative output previews, glowing interface panels, model capability badges'
  if (/hardware|chip|cpu|gpu/.test(combined)) return 'performance charts, spec comparison cards, hardware close-ups, benchmark badges'
  return 'feature cards, interface panels, product highlights, mini screenshots, icon blocks'
}

function resolveNewsPromptMood(data) {
  const type = String(data.type || '').toLowerCase().replace(/\s+/g, '_')
  return NEWS_PROMPT_TYPE_MOODS[type] || 'Modern Tech'
}

function resolveNewsPromptTypeAddon(data) {
  const type = String(data.type || '').toLowerCase().replace(/\s+/g, '_')
  return NEWS_PROMPT_TYPE_ADDONS[type] || ''
}

function resolveNewsPromptKeywordAddon(data) {
  const title = String(data.title || '').toLowerCase()
  const tags = (Array.isArray(data.tag_names) ? data.tag_names : []).join(' ').toLowerCase()
  const category = String(data.category || '').toLowerCase()
  const combined = `${title} ${tags} ${category}`
  const addons = []
  for (const pattern of NEWS_PROMPT_KEYWORD_PATTERNS) {
    if (pattern.keywords.some((kw) => combined.includes(kw))) {
      addons.push(pattern.addon)
    }
  }
  return [...new Set(addons)].join('\n')
}

function buildNewsImagePrompt(data) {
  const headline = resolveNewsPromptHeadline(data)
  const subheadline = resolveNewsPromptSubheadline(data)
  const topic = resolveNewsPromptTopic(data)
  const newsType = resolveNewsPromptType(data)
  const heroSubject = resolveNewsPromptHeroSubject(data)
  const supportingModules = resolveNewsPromptSupportingModules(data)
  const mood = resolveNewsPromptMood(data)
  const typeAddon = resolveNewsPromptTypeAddon(data)
  const keywordAddon = resolveNewsPromptKeywordAddon(data)

  const lines = [
    'Create a premium Skinbase news cover image in 16:9 aspect ratio.',
    '',
    'Design it as a professional editorial tech poster for a digital culture, software, hardware, AI, creative tools, desktop customization, or retro computing news article.',
    '',
    'ARTICLE DETAILS:',
    `Headline: "${headline}"`,
    `Subheadline: "${subheadline}"`,
    `Topic: ${topic}`,
    `News type: ${newsType}`,
    `Hero subject: ${heroSubject}`,
    `Supporting modules: ${supportingModules}`,
    `Mood: ${mood}`,
    '',
    'LAYOUT:',
    'Use a structured 16:9 news hero composition with:',
    '- Large bold headline in the upper-left or top-center',
    '- Smaller subtitle directly below the headline',
    '- One strong central hero visual',
    '- Supporting side panels, feature cards, icons, UI windows, diagrams, or mini screenshots',
    '- A bottom strip with 3 to 6 small highlight blocks or visual details',
    '- Clean spacing and a strong visual hierarchy',
    '',
    'VISUAL STYLE:',
    'Use a dark premium background with blue, cyan, violet, neon, or topic-matching accent colors. Add glossy highlights, subtle glow, cinematic depth, crisp lighting, and a polished high-tech editorial look.',
    '',
    'The image should feel like a professional magazine cover, software release poster, tech conference banner, or retro computing feature graphic. It should be visually rich, but still clean, readable, and organized.',
    '',
    'TEXT STYLE:',
    'Use bold clean sans-serif typography. Keep all visible text short and readable. Avoid long paragraphs inside the image. Use only short labels, feature names, or headline-style phrases.',
    '',
    'CONTENT DIRECTION:',
    'Represent the topic clearly through the central visual. Use relevant objects such as:',
    '- software windows',
    '- futuristic workstations',
    '- creative AI panels',
    '- computer chips',
    '- retro hardware',
    '- desktop customization elements',
    '- conference screens',
    '- app interface mockups',
    '- glowing diagrams',
    '- feature cards',
    '- product-style panels',
    '',
    'QUALITY RULES:',
    'Make it sharp, premium, polished, high detail, thumbnail-friendly, and suitable as a Skinbase news article cover image.',
    '',
    'Avoid clutter, random filler objects, unreadable microtext, messy typography, distorted UI, weak composition, watermarks, fake signatures, low-quality stock-photo style, and irrelevant logos.',
  ]

  if (typeAddon) {
    lines.push('', typeAddon)
  }
  if (keywordAddon) {
    lines.push('', keywordAddon)
  }

  return lines.join('\n')
}

// ─────────────────────────────────────────────────────────────────────────────

function buildNewsExportPayloads(data, context = {}) {
  const normalized = buildSubmitPayload(data || {})
  const category = findNewsOptionById(context.categoryOptions, normalized.category_id)
  const existingTags = findNewsTagsByIds(context.tagOptions, normalized.tag_ids)
  const author = context.author || null

  const full = {
    title: normalized.title,
    slug: normalized.slug,
    excerpt: normalized.excerpt,
    content: normalized.content,
    cover_image: normalized.cover_image,
    type: normalized.type,
    category_id: normalized.category_id,
    category: category?.name ?? category?.label ?? '',
    category_slug: category?.slug ?? '',
    author_id: normalized.author_id,
    author_name: author?.title ?? author?.name ?? '',
    editorial_status: normalized.editorial_status,
    published_at: normalized.published_at,
    is_featured: normalized.is_featured,
    is_pinned: normalized.is_pinned,
    comments_enabled: normalized.comments_enabled,
    tags: [
      ...existingTags,
      ...normalized.new_tag_names.map((name) => ({ name, slug: '' })),
    ],
    tag_names: [
      ...existingTags.map((tag) => tag.name),
      ...normalized.new_tag_names,
    ],
    tag_ids: normalized.tag_ids,
    new_tag_names: normalized.new_tag_names,
    meta_title: normalized.meta_title,
    meta_description: normalized.meta_description,
    meta_keywords: normalized.meta_keywords,
    og_title: normalized.og_title,
    og_description: normalized.og_description,
    og_image: normalized.og_image,
    relations: normalized.relations,
  }

  const structured = {
    title: normalized.title,
    excerpt: normalized.excerpt,
    date: normalized.published_at,
    body: stripHtml(normalized.content),
    category: category?.name ?? category?.label ?? '',
  }

  const markdown = {
    title: normalized.title,
    excerpt: normalized.excerpt,
    date: normalized.published_at,
    category: category?.name ?? category?.label ?? '',
    body_html: normalized.content,
  }

  return {
    full: JSON.stringify(full, null, 2),
    structured: JSON.stringify(structured, null, 2),
    structuredPlain: buildStructuredPlainTextExport(structured),
    markdown: buildNewsMarkdownExport(markdown),
    markdownInput: markdown,
  }
}

function JsonImportDialog({ open, value, error, onChange, onClose, onApply, exportPayloads, articleData = {}, newTagLimit = NEWS_NEW_TAG_LIMIT }) {
  const backdropRef = useRef(null)
  const [activeImportTab, setActiveImportTab] = useState('input')
  const [copyFeedback, setCopyFeedback] = useState('')
  const [exportMode, setExportMode] = useState('full')
  const [markdownExportText, setMarkdownExportText] = useState(String(exportPayloads?.markdown || ''))
  const [promptText, setPromptText] = useState('')
  const [promptIsManual, setPromptIsManual] = useState(false)

  const importTabs = [
    { id: 'input', label: 'Input', description: 'Paste JSON and apply it to the editor.' },
    { id: 'structure', label: 'Structure example', description: 'A working example of the expected payload.' },
    { id: 'docs', label: 'Documentation', description: 'Field notes and mapping rules.' },
    { id: 'prompts', label: 'AI prompts', description: 'Prompt examples for generating structured news.' },
    { id: 'export', label: 'Export', description: 'Copy the current article out as JSON, text, or Markdown.' },
    { id: 'image_prompt', label: 'Image Prompt', description: 'Auto-generate a cover image prompt from article data.' },
  ]

  const structureExample = {
    title: 'Sample News Title',
    slug: 'sample-news-title',
    excerpt: 'This is a sample news excerpt that demonstrates the structured import format.',
    content: '<p>This is sample news content written in HTML.</p><p>You can replace it with your own editorial copy.</p>',
    cover_image: 'sample-news-cover.webp',
    type: 'Announcement',
    category_id: 1,
    category: 'General',
    category_slug: 'general',
    editorial_status: 'draft',
    published_at: '2026-05-03 09:00:00',
    author_id: 1,
    author_name: 'Sample Author',
    is_featured: false,
    is_pinned: false,
    comments_enabled: true,
    tags: [
      { name: 'Sample Tag', slug: 'sample-tag' },
      { name: 'News Import', slug: 'news-import' },
    ],
    tag_names: ['Sample Tag', 'News Import'],
    tag_ids: [],
    relations: {
      related_articles: [],
      related_artworks: [],
      related_users: [],
      source_urls: ['https://example.com/sample-news-source'],
    },
    meta_title: 'Sample News Title - Skinbase Example',
    meta_description: 'This is a sample news meta description for the structured import example.',
    meta_keywords: 'sample news, structured import, editorial example',
    og_title: 'Sample News Title',
    og_description: 'This is a sample news OG description for the structured import example.',
  }

  const newsJsonSchemaSummary = `You are generating a Skinbase news article JSON object.

Return only valid JSON. No markdown, no commentary, no code fences.

Required fields:
- title: string
- content: HTML string

Recommended fields:
- slug: SEO slug
- excerpt: short summary
- cover_image: image path or URL
- type: one of the editor's news types
- category_id: preferred category id
- editorial_status: draft|review|scheduled|published|archived
- published_at: YYYY-MM-DD HH:MM:SS
- author_id or author_name
- comments_enabled: boolean
- is_featured: boolean
- is_pinned: boolean
- meta_title, meta_description, meta_keywords
- og_title, og_description
- tags: array of strings or objects with name/title/label/slug
- tag_names: array of strings
- tag_ids: array of ids if you already know them
- relations: array of objects with entity_type, entity_id, context_label

Rules:
- Use HTML paragraphs in content.
- Keep excerpt concise.
- Prefer category_id when you know it; otherwise include category/category_slug for matching.
- If a tag is an object, keep the name field readable.
- If source URLs are available, include them in a relations-related field or source notes field.
`

  const aiPromptExamples = [
    {
      title: 'Blog-to-news generator',
      prompt: `${newsJsonSchemaSummary}

Transform the following article into a news payload for the editor.
- Preserve the factual meaning and the editorial tone.
- Choose a concise title and an SEO-friendly slug.
- Write content as HTML paragraphs.
- Include 8 to 14 highly relevant tags.
- Include category_id when possible, otherwise use category_slug or category to help matching.
- Fill meta_title, meta_description, og_title, and og_description when available.
- Make comments_enabled true unless the source clearly says otherwise.

Input article text:
{{ARTICLE_TEXT}}`,
    },
    {
      title: 'Release announcement prompt',
      prompt: `${newsJsonSchemaSummary}

Create a structured release announcement JSON object.
- Use a direct product/news style headline.
- Keep the excerpt short and easy to scan.
- Write the content as 3 to 6 HTML paragraphs.
- Include a realistic published_at timestamp in local time.
- Set editorial_status to published if the article is already live, otherwise draft.
- Set comments_enabled to true unless the release is sensitive or comments should be disabled.
- Add 8 to 12 tags.

Release notes:
{{RELEASE_NOTES}}`,
    },
    {
      title: 'Migration import prompt',
      prompt: `${newsJsonSchemaSummary}

Convert the source article into Skinbase news JSON.
- Preserve the factual content and keep the article structure readable.
- Return only JSON.
- Normalize tags into an array of objects with name and slug.
- If the source contains article links, place them in a source_urls field inside the relations object.
- If the source provides an author, category, or publish date, map those into the matching editor fields.
- Use sensible defaults for any missing metadata.

Source article:
{{SOURCE_ARTICLE}}`,
    },
  ]

  function tabButtonClass(active) {
    return `flex-1 rounded-2xl border px-4 py-3 text-left transition ${active ? 'border-sky-300/25 bg-sky-400/10 text-white' : 'border-white/10 bg-white/[0.03] text-slate-400 hover:border-white/20 hover:bg-white/[0.05] hover:text-slate-200'}`
  }

  const activeExportText = exportMode === 'structured'
    ? String(exportPayloads?.structured || '')
    : exportMode === 'markdown'
      ? markdownExportText
      : String(exportPayloads?.full || '')

  const copyText = async (text, label) => {
    try {
      await navigator.clipboard.writeText(String(text))
      setCopyFeedback(`${label} copied`)
      window.setTimeout(() => setCopyFeedback(''), 1800)
    } catch (copyError) {
      setCopyFeedback('Copy failed')
      window.setTimeout(() => setCopyFeedback(''), 1800)
    }
  }

  useEffect(() => {
    if (!open) return undefined

    const handleKeyDown = (event) => {
      if (event.key === 'Escape') {
        onClose?.()
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [onClose, open])

  useEffect(() => {
    setMarkdownExportText(String(exportPayloads?.markdown || ''))
  }, [exportPayloads])

  useEffect(() => {
    if (!open || activeImportTab !== 'export' || exportMode !== 'markdown') {
      return undefined
    }

    let cancelled = false

    loadNewsMarkdownTurndown().then(() => {
      if (cancelled) {
        return
      }

      setMarkdownExportText(buildNewsMarkdownExport(exportPayloads?.markdownInput || {}))
    })

    return () => {
      cancelled = true
    }
  }, [activeImportTab, exportMode, exportPayloads, open])

  // Auto-generate image prompt when the tab opens, or when article data changes
  // (unless the editor has manually modified the prompt text).
  useEffect(() => {
    if (!open || activeImportTab !== 'image_prompt') return
    if (promptIsManual) return
    setPromptText(buildNewsImagePrompt(articleData))
  }, [open, activeImportTab, articleData, promptIsManual])

  const handleRegeneratePrompt = useCallback(() => {
    setPromptIsManual(false)
    setPromptText(buildNewsImagePrompt(articleData))
  }, [articleData])

  const handleResetPrompt = useCallback(() => {
    setPromptIsManual(false)
    setPromptText(buildNewsImagePrompt(articleData))
  }, [articleData])

  if (!open) return null

  return createPortal(
    <div
      ref={backdropRef}
      className="fixed inset-0 z-[9999] flex items-center justify-center bg-[#04070dcc] px-4 backdrop-blur-md"
      onClick={(event) => {
        if (event.target === backdropRef.current) {
          onClose?.()
        }
      }}
      role="presentation"
    >
      <div
        role="dialog"
        aria-modal="true"
        aria-labelledby="news-json-import-title"
        className="flex h-[min(90vh,780px)] w-full max-w-5xl flex-col overflow-hidden rounded-3xl border border-white/10 bg-[linear-gradient(180deg,rgba(16,22,34,0.98),rgba(8,12,19,0.98))] shadow-[0_30px_80px_rgba(0,0,0,0.55)]"
      >
        <div className="border-b border-white/[0.06] bg-white/[0.02] px-6 py-5">
          <p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-white/35">Structured import</p>
          <h3 id="news-json-import-title" className="mt-2 text-lg font-semibold text-white">Import or export article JSON</h3>
          <p className="mt-2 text-sm leading-6 text-white/65">Use this for migrations, AI-assisted drafting, bulk handoff from another editorial system, or copying the current article into reusable JSON.</p>
        </div>

        <div className="border-b border-white/[0.06] px-4 py-4">
          <div className="grid gap-2 grid-cols-2 md:grid-cols-3 lg:grid-cols-6">
            {importTabs.map((tab) => (
              <button
                key={tab.id}
                type="button"
                onClick={() => setActiveImportTab(tab.id)}
                className={tabButtonClass(activeImportTab === tab.id)}
              >
                <div className="text-sm font-semibold">{tab.label}</div>
                <div className="mt-1 text-xs leading-5 text-current/70">{tab.description}</div>
              </button>
            ))}
          </div>
        </div>

        <div className="nova-scrollbar flex-1 min-h-0 overflow-y-auto px-6 py-5">
          {activeImportTab === 'input' ? (
            <div className="grid h-full min-h-0 gap-5 xl:grid-cols-[minmax(0,1.2fr)_360px]">
              <div className="grid gap-3">
                <textarea
                  value={value}
                  onChange={(event) => onChange?.(event.target.value)}
                  rows={18}
                  placeholder={'{\n  "title": "My news title",\n  "slug": "my-news-title",\n  "excerpt": "Short summary",\n  "tags": ["release", "community"]\n}'}
                  className="nova-scrollbar w-full rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 font-mono text-sm text-white outline-none placeholder:text-white/30"
                />
                {error ? <div className="rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100">{error}</div> : null}
              </div>

              <div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-4 text-sm text-slate-300">
                <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Recognized keys</div>
                <div className="mt-3 space-y-2 leading-6 text-slate-400">
                  <p>`title`, `slug`, `excerpt`, `content`, `cover_image`</p>
                  <p>`type`, `category_id`, `category`, `category_slug`</p>
                  <p>`editorial_status`, `published_at`, `author_id`, `author_name`</p>
                  <p>`is_featured`, `is_pinned`, `comments_enabled`</p>
                  <p>`tags`, `tag_names`, `tag_ids`, `relations`</p>
                  <p>`new_tag_names` is capped at {newTagLimit} items per article.</p>
                  <p>`meta_title`, `meta_description`, `meta_keywords`, `og_title`, `og_description`</p>
                </div>
              </div>
            </div>
          ) : null}

          {activeImportTab === 'structure' ? (
            <div className="grid h-full min-h-0 gap-5 xl:grid-cols-[minmax(0,1.2fr)_360px]">
              <div className="rounded-[24px] border border-white/10 bg-black/20 p-4">
                <div className="mb-3 flex items-center justify-between gap-3">
                  <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Structure example</div>
                  <button
                    type="button"
                    onClick={() => copyText(JSON.stringify(structureExample, null, 2), 'Structure example')}
                    className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-white/[0.08]"
                  >
                    Copy example
                  </button>
                </div>
                <pre className="nova-scrollbar max-h-[52vh] overflow-auto rounded-[20px] border border-white/10 bg-slate-950/80 p-4 text-xs leading-6 text-slate-200">{JSON.stringify(structureExample, null, 2)}</pre>
              </div>

              <div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-4 text-sm text-slate-300">
                <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Notes</div>
                <div className="mt-3 space-y-3 leading-6 text-slate-400">
                  <p>Tags can be an array of strings or objects with `name`, `title`, `label`, or `slug`.</p>
                  <p>`content` accepts HTML. Keep paragraph tags and inline formatting in the JSON string.</p>
                  <p>`relations` may contain structured links and source URLs, but only direct entity fields are currently applied automatically.</p>
                </div>
              </div>
            </div>
          ) : null}

          {activeImportTab === 'docs' ? (
            <div className="grid h-full min-h-0 gap-5 xl:grid-cols-[minmax(0,1.2fr)_360px]">
              <div className="rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm leading-7 text-slate-300">
                <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Field guide</div>
                <div className="mt-3 space-y-3 text-slate-400">
                  <p><strong className="text-slate-200">title</strong> - article headline. If `slug` is omitted the editor can derive it from the title.</p>
                  <p><strong className="text-slate-200">excerpt</strong> - short summary shown in listings and metadata.</p>
                  <p><strong className="text-slate-200">content</strong> - HTML body, usually paragraph tags plus basic formatting.</p>
                  <p><strong className="text-slate-200">category_id</strong> - preferred category selector value. `category_slug` and `category` are accepted for matching, but `category_id` is the reliable one.</p>
                  <p><strong className="text-slate-200">tags</strong> - array of strings or objects. Existing tags are matched by name; new ones are staged automatically.</p>
                  <p><strong className="text-slate-200">new_tag_names</strong> - capped at {newTagLimit} items per article. Use existing tags where possible to stay within the limit.</p>
                  <p><strong className="text-slate-200">meta_keywords</strong> - max 255 characters. Keep it concise or the save will fail validation.</p>
                </div>
              </div>

              <div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-4 text-sm text-slate-300">
                <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Import rules</div>
                <div className="mt-3 space-y-2 leading-6 text-slate-400">
                  <p>Boolean fields accept `true`/`false`, `1`/`0`, `yes`/`no`, and `on`/`off`.</p>
                  <p>Date values should be in `YYYY-MM-DD HH:MM:SS` format for scheduled stories.</p>
                  <p>Keep `new_tag_names` at or below {newTagLimit} items, which matches the editor validation.</p>
                  <p>Unknown keys are ignored, so you can paste a broader object safely.</p>
                </div>
              </div>
            </div>
          ) : null}

          {activeImportTab === 'prompts' ? (
            <div className="grid h-full min-h-0 gap-5 xl:grid-cols-[minmax(0,1.2fr)_360px]">
              <div className="grid gap-4">
                {aiPromptExamples.map((example) => (
                  <div key={example.title} className="rounded-[24px] border border-white/10 bg-black/20 p-4">
                    <div className="flex items-center justify-between gap-3">
                      <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-sky-200/70">{example.title}</div>
                      <button
                        type="button"
                        onClick={() => copyText(example.prompt, example.title)}
                        className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-white/[0.08]"
                      >
                        Copy prompt
                      </button>
                    </div>
                    <pre className="nova-scrollbar mt-3 max-h-56 overflow-auto whitespace-pre-wrap rounded-[18px] border border-white/10 bg-slate-950/80 p-4 text-sm leading-6 text-slate-200">{example.prompt}</pre>
                  </div>
                ))}
              </div>

              <div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-4 text-sm text-slate-300">
                <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Prompt tips</div>
                <div className="mt-3 space-y-2 leading-6 text-slate-400">
                  <p>Tell the model to return JSON only, with no explanation text.</p>
                  <p>Ask for `tags` as an array of objects when you want the most compatible import shape.</p>
                  <p>Include `source_urls` or reference links in the source instruction if you want them copied into the story notes.</p>
                </div>
              </div>
            </div>
          ) : null}

          {activeImportTab === 'export' ? (
            <div className="grid h-full min-h-0 gap-5 xl:grid-cols-[minmax(0,1.2fr)_360px]">
              <div className="grid gap-3">
                <div className="flex flex-wrap gap-2">
                  <button
                    type="button"
                    onClick={() => setExportMode('full')}
                    className={tabButtonClass(exportMode === 'full')}
                  >
                    <div className="text-sm font-semibold">Full news JSON</div>
                    <div className="mt-1 text-xs leading-5 text-current/70">Exports the current article with metadata, tags, and relations.</div>
                  </button>
                  <button
                    type="button"
                    onClick={() => setExportMode('structured')}
                    className={tabButtonClass(exportMode === 'structured')}
                  >
                    <div className="text-sm font-semibold">Structured JSON</div>
                    <div className="mt-1 text-xs leading-5 text-current/70">Exports only title, excerpt, date, body, and category.</div>
                  </button>
                  <button
                    type="button"
                    onClick={() => setExportMode('markdown')}
                    className={tabButtonClass(exportMode === 'markdown')}
                  >
                    <div className="text-sm font-semibold">Markdown</div>
                    <div className="mt-1 text-xs leading-5 text-current/70">Exports the current article as Markdown with heading, summary, and body.</div>
                  </button>
                </div>

                <textarea
                  readOnly
                  value={activeExportText}
                  rows={18}
                  className="nova-scrollbar w-full rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 font-mono text-sm text-white outline-none"
                />
              </div>

              <div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-4 text-sm text-slate-300">
                <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Export options</div>
                <div className="mt-3 space-y-3 leading-6 text-slate-400">
                  <p><strong className="text-slate-200">Full news JSON</strong> includes the current editable article state: slug, status, tags, metadata, and relations.</p>
                  <p><strong className="text-slate-200">Structured JSON</strong> keeps the reduced handoff shape: title, excerpt, date, body, and category.</p>
                  <p><strong className="text-slate-200">Markdown</strong> converts the current article body into Markdown and includes the title plus summary fields for external reuse.</p>
                  <p>The export uses the live editor state, so unsaved changes are included immediately.</p>
                </div>
              </div>
            </div>
          ) : null}

          {activeImportTab === 'image_prompt' ? (
            <div className="grid h-full min-h-0 gap-5 xl:grid-cols-[minmax(0,1.2fr)_320px]">
              <div className="grid gap-3">
                <div className="flex flex-wrap items-center gap-2">
                  <span className="flex-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
                    Generated cover image prompt
                    {promptIsManual ? <span className="ml-2 rounded-full border border-amber-300/20 bg-amber-400/10 px-2 py-0.5 text-[10px] text-amber-100">Manually edited</span> : null}
                  </span>
                  <button
                    type="button"
                    onClick={handleRegeneratePrompt}
                    className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-white/[0.08]"
                  >
                    Regenerate
                  </button>
                  {promptIsManual ? (
                    <button
                      type="button"
                      onClick={handleResetPrompt}
                      className="rounded-full border border-amber-300/20 bg-amber-400/10 px-3 py-1.5 text-xs font-semibold text-amber-100 transition hover:bg-amber-400/20"
                    >
                      Reset to auto
                    </button>
                  ) : null}
                  <button
                    type="button"
                    onClick={() => copyText(promptText, 'Image prompt')}
                    className="rounded-full border border-sky-300/25 bg-sky-400/90 px-3 py-1.5 text-xs font-semibold text-slate-950 transition hover:brightness-110"
                  >
                    Copy prompt
                  </button>
                </div>
                <textarea
                  value={promptText}
                  onChange={(event) => {
                    setPromptText(event.target.value)
                    setPromptIsManual(true)
                  }}
                  rows={22}
                  spellCheck={false}
                  className="nova-scrollbar w-full rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 font-mono text-sm leading-6 text-white outline-none placeholder:text-white/30"
                  placeholder="Opening the tab will generate a prompt automatically…"
                />
              </div>

              <div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-4 text-sm text-slate-300">
                <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">How it works</div>
                <div className="mt-3 space-y-3 leading-6 text-slate-400">
                  <p>The prompt is built automatically from the current article fields: title, excerpt, type, category, and tags.</p>
                  <p>You can edit the prompt freely. It will be marked as <span className="rounded border border-amber-300/20 bg-amber-400/10 px-1 text-amber-100">Manually edited</span> once you change it.</p>
                  <p>Click <strong className="text-slate-200">Regenerate</strong> or <strong className="text-slate-200">Reset to auto</strong> to rebuild from the current article state.</p>
                  <p>Copy the prompt and paste it into an AI image generator such as Midjourney, DALL-E, Stable Diffusion, or Flux.</p>
                </div>
                <div className="mt-5 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Auto-filled from</div>
                <ul className="mt-2 space-y-1 text-xs leading-6 text-slate-400">
                  <li><span className="text-slate-300">Headline</span> — title, meta_title</li>
                  <li><span className="text-slate-300">Subheadline</span> — excerpt, meta_description, content</li>
                  <li><span className="text-slate-300">Topic</span> — category, tags</li>
                  <li><span className="text-slate-300">Type</span> — article type</li>
                  <li><span className="text-slate-300">Hero / Mood</span> — inferred from title, tags, type</li>
                  <li><span className="text-slate-300">Addons</span> — type-based and keyword-based style blocks</li>
                </ul>
              </div>
            </div>
          ) : null}
        </div>

        {copyFeedback ? (
          <div className="px-6 pb-2 text-right text-xs font-medium text-sky-200/80">{copyFeedback}</div>
        ) : null}

        <div className="flex items-center justify-end gap-3 border-t border-white/[0.06] px-6 py-4">
          <button
            type="button"
            onClick={() => onClose?.()}
            className="inline-flex items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition hover:bg-white/[0.08] hover:text-white"
          >
            Cancel
          </button>
          {activeImportTab === 'export' ? (
            <>
              {exportMode === 'structured' ? (
                <button
                  type="button"
                  onClick={() => copyText(String(exportPayloads?.structuredPlain || ''), 'Structured plain text export')}
                  className="inline-flex items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition hover:bg-white/[0.08] hover:text-white"
                >
                  Copy plain text
                </button>
              ) : null}
              <button
                type="button"
                onClick={() => copyText(activeExportText, exportMode === 'structured' ? 'Structured export' : exportMode === 'markdown' ? 'Markdown export' : 'Full news export')}
                className="inline-flex items-center justify-center rounded-full border border-sky-300/25 bg-sky-400/90 px-4 py-2 text-sm font-semibold text-slate-950 transition hover:brightness-110"
              >
                Copy export
              </button>
            </>
          ) : activeImportTab === 'image_prompt' ? null : (
            <button
              type="button"
              onClick={() => onApply?.()}
              className="inline-flex items-center justify-center rounded-full border border-sky-300/25 bg-sky-400/90 px-4 py-2 text-sm font-semibold text-slate-950 transition hover:brightness-110"
            >
              Apply JSON
            </button>
          )}
        </div>
      </div>
    </div>,
    document.body,
  )
}

export default function StudioNewsEditor() {
  const { props } = usePage()
  const { toasts, push: pushToast, dismiss: dismissToast } = useToast()
  const article = props.article || {}
  const initialFormData = useMemo(() => buildInitialFormData(article, props.defaultAuthor, props.defaultPublishedAt, props.typeOptions, props.oldInput || {}), [article, props.defaultAuthor, props.defaultPublishedAt, props.oldInput, props.typeOptions])
  const articleSyncKey = useMemo(() => JSON.stringify(initialFormData), [initialFormData])
  const [authorResults, setAuthorResults] = useState([])
  const [authorQuery, setAuthorQuery] = useState(article.author?.title || article.author?.subtitle?.replace(/^@/, '') || '')
  const [selectedAuthor, setSelectedAuthor] = useState(article.author || props.defaultAuthor || null)
  const [relationResults, setRelationResults] = useState({})
  const [coverPreviewUrl, setCoverPreviewUrl] = useState(article.cover_url || (String(article.cover_image || '').startsWith('http') ? article.cover_image : ''))
  const [stagedCoverPath, setStagedCoverPath] = useState('')
  const [activeTab, setActiveTab] = useState('content')
  const [jsonImportOpen, setJsonImportOpen] = useState(false)
  const [jsonImportValue, setJsonImportValue] = useState('')
  const [jsonImportError, setJsonImportError] = useState('')
  const lastSyncedArticleKeyRef = useRef(articleSyncKey)
  const slugTouchedRef = useRef(Boolean(String(article.slug || '').trim()))

  const form = useForm(initialFormData)
  const normalizedInitialPayload = useMemo(() => JSON.stringify(buildSubmitPayload(initialFormData)), [initialFormData])
  const normalizedCurrentPayload = useMemo(() => JSON.stringify(buildSubmitPayload(form.data)), [form.data])
  const hasUnsavedChanges = normalizedCurrentPayload !== normalizedInitialPayload
  const frontendArticleUrl = String(article.canonical_url || '').trim()

  useEffect(() => {
    if (lastSyncedArticleKeyRef.current === articleSyncKey) {
      return
    }

    lastSyncedArticleKeyRef.current = articleSyncKey
    form.setData(initialFormData)
    form.clearErrors()
    setSelectedAuthor(article.author || props.defaultAuthor || null)
    setAuthorQuery(article.author?.title || article.author?.subtitle?.replace(/^@/, '') || '')
    setRelationResults({})
    setCoverPreviewUrl(article.cover_url || (String(article.cover_image || '').startsWith('http') ? article.cover_image : ''))
    setStagedCoverPath('')
    slugTouchedRef.current = Boolean(String((article.slug || initialFormData.slug || '')).trim())
  }, [article, articleSyncKey, form, initialFormData, props.defaultAuthor])

  useEffect(() => {
    if (slugTouchedRef.current) {
      return
    }

    const nextSlug = slugifyNewsTitle(form.data.title)
    if (nextSlug === String(form.data.slug || '')) {
      return
    }

    form.setData('slug', nextSlug)
  }, [form, form.data.slug, form.data.title])

  useEffect(() => {
    if (!hasUnsavedChanges || form.processing) {
      return undefined
    }

    const message = 'You have unsaved article changes. Leave without saving?'

    const handleBeforeUnload = (event) => {
      event.preventDefault()
      event.returnValue = message
      return message
    }

    const handleDocumentClick = (event) => {
      if (event.defaultPrevented) {
        return
      }

      const anchor = event.target instanceof Element ? event.target.closest('a[href]') : null
      if (!anchor) {
        return
      }

      const href = anchor.getAttribute('href')
      if (!href || href.startsWith('#') || anchor.getAttribute('download') != null) {
        return
      }

      if (anchor.target && anchor.target.toLowerCase() === '_blank') {
        return
      }

      if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
        return
      }

      const destination = new URL(anchor.href, window.location.href)
      const current = new URL(window.location.href)
      if (destination.href === current.href) {
        return
      }

      if (!window.confirm(message)) {
        event.preventDefault()
      }
    }

    window.addEventListener('beforeunload', handleBeforeUnload)
    document.addEventListener('click', handleDocumentClick, true)

    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload)
      document.removeEventListener('click', handleDocumentClick, true)
    }
  }, [form.processing, hasUnsavedChanges])

  const excerptLength = String(form.data.excerpt || '').trim().length
  const bodyWordCount = useMemo(() => {
    const plain = stripHtml(form.data.content)
    return plain === '' ? 0 : plain.split(/\s+/).length
  }, [form.data.content])
  const typeOptions = useMemo(() => selectOptionsFromValues(props.typeOptions || []), [props.typeOptions])
  const statusOptions = useMemo(() => selectOptionsFromValues(props.statusOptions || []), [props.statusOptions])
  const categoryOptions = useMemo(() => selectOptionsFromValues(props.categoryOptions || [], 'Select category'), [props.categoryOptions])
  const currentTabIndex = Math.max(0, NEWS_EDITOR_TABS.findIndex((tab) => tab.id === activeTab))
  const currentTab = NEWS_EDITOR_TABS[currentTabIndex] || NEWS_EDITOR_TABS[0]
  const tabErrorCounts = useMemo(() => ({
    content: ['title', 'slug', 'excerpt', 'content', 'cover_image'].filter((key) => Boolean(form.errors[key])).length,
    publishing: ['type', 'category_id', 'author_id', 'editorial_status', 'published_at', 'comments_enabled'].filter((key) => Boolean(form.errors[key])).length,
    discoverability: ['tag_ids', 'new_tag_names', 'meta_title', 'meta_description', 'meta_keywords', 'og_title', 'og_description', 'og_image'].filter((key) => Boolean(form.errors[key])).length,
    connections: ['relations'].filter((key) => Boolean(form.errors[key])).length,
  }), [form.errors])
  const overviewItems = useMemo(() => ([
    { label: 'Headline', done: String(form.data.title || '').trim().length > 0 },
    { label: 'Category', done: hasRequiredCategory(form.data.category_id) },
    { label: 'Excerpt', done: String(form.data.excerpt || '').trim().length > 0 },
    { label: 'Body', done: bodyWordCount > 0 },
    { label: 'Cover image', done: String(form.data.cover_image || '').trim().length > 0 },
    { label: 'Author', done: Boolean(form.data.author_id) },
  ]), [bodyWordCount, form.data.author_id, form.data.category_id, form.data.cover_image, form.data.excerpt, form.data.title])
  const completedCount = overviewItems.filter((item) => item.done).length
  const jsonExportPayloads = useMemo(() => buildNewsExportPayloads(form.data, {
    categoryOptions: props.categoryOptions,
    tagOptions: props.tagOptions,
    author: selectedAuthor,
  }), [form.data, props.categoryOptions, props.tagOptions, selectedAuthor])

  const imagePromptArticleData = useMemo(() => {
    const category = findNewsOptionById(props.categoryOptions, form.data.category_id)
    const existingTags = findNewsTagsByIds(props.tagOptions, form.data.tag_ids)
    return {
      title: form.data.title,
      excerpt: form.data.excerpt,
      content: form.data.content,
      type: form.data.type,
      category: category?.name ?? category?.label ?? '',
      category_slug: category?.slug ?? '',
      tag_names: [
        ...existingTags.map((t) => t.name),
        ...(Array.isArray(form.data.new_tag_names) ? form.data.new_tag_names : []),
      ],
      meta_title: form.data.meta_title,
      meta_description: form.data.meta_description,
    }
  }, [form.data, props.categoryOptions, props.tagOptions])

  useEffect(() => {
    const firstErrorTab = NEWS_EDITOR_TABS.find((tab) => tabErrorCounts[tab.id] > 0)
    if (firstErrorTab) {
      setActiveTab(firstErrorTab.id)
    }
  }, [tabErrorCounts])

  const searchEntities = async (type, query) => {
    const url = new URL(props.entitySearchUrl, window.location.origin)
    url.searchParams.set('type', type)
    url.searchParams.set('q', query)

    const response = await fetch(url.toString(), {
      headers: {
        Accept: 'application/json',
      },
      credentials: 'same-origin',
    })

    if (!response.ok) {
      return []
    }

    const payload = await response.json()
    return Array.isArray(payload.items) ? payload.items : []
  }

  const runAuthorSearch = async () => {
    const items = await searchEntities('user', authorQuery)
    setAuthorResults(items)
  }

  const addRelation = () => {
    form.setData('relations', [
      ...form.data.relations,
      {
        entity_type: props.relationTypeOptions?.[0]?.value || 'group',
        entity_id: '',
        external_url: '',
        context_label: '',
        preview: null,
        query: '',
      },
    ])
  }

  const updateRelation = (index, nextRelation) => {
    form.setData('relations', form.data.relations.map((relation, relationIndex) => (relationIndex === index ? nextRelation : relation)))
  }

  const removeRelation = (index) => {
    form.setData('relations', form.data.relations.filter((_, relationIndex) => relationIndex !== index))
    setRelationResults((current) => {
      const next = { ...current }
      delete next[index]
      return next
    })
  }

  const runRelationSearch = async (index) => {
    const relation = form.data.relations[index]
    if (!relation) return
    const items = await searchEntities(relation.entity_type, relation.query || '')
    setRelationResults((current) => ({ ...current, [index]: items }))
  }

  const handleManualCoverChange = (nextValue) => {
    form.setData('cover_image', nextValue)

    if (stagedCoverPath && nextValue !== stagedCoverPath) {
      setStagedCoverPath('')
    }

    if (!nextValue) {
      setCoverPreviewUrl('')
      return
    }

    if (String(nextValue).startsWith('http://') || String(nextValue).startsWith('https://')) {
      setCoverPreviewUrl(nextValue)
      return
    }

    setCoverPreviewUrl(`${props.coverCdnBaseUrl}/${String(nextValue).replace(/^\/+/, '')}`)
  }

  const submit = (event) => {
    event.preventDefault()

    if (!hasRequiredCategory(form.data.category_id)) {
      form.setError('category_id', 'Choose a category before saving the article.')
      pushToast('Choose a category before saving the article.', 'error')
      return
    }

    const options = {
      preserveScroll: true,
      preserveState: 'errors',
      onSuccess: () => {
        setStagedCoverPath('')
        pushToast('Article saved successfully.', 'success')
      },
      onError: (errors) => {
        const errorMessages = Object.values(errors)
        const first = errorMessages[0] || 'The article could not be saved.'
        const extra = errorMessages.length > 1 ? ` (${errorMessages.length - 1} more field${errorMessages.length > 2 ? 's' : ''})` : ''
        const firstErrorTab = NEWS_EDITOR_TABS.find((tab) => tabErrorCounts[tab.id] > 0)

        if (firstErrorTab) {
          setActiveTab(firstErrorTab.id)
        }

        pushToast(first + extra, 'error')
      },
    }

    form.transform((data) => buildSubmitPayload(data))

    if (props.updateUrl) {
      form.patch(props.updateUrl, options)
      return
    }

    form.post(props.storeUrl, options)
  }

  const deleteArticle = () => {
    if (!props.destroyUrl) return
    if (!window.confirm('Move this article to trash? This uses soft delete so the record stays in the database.')) return

    router.delete(props.destroyUrl, {
      preserveScroll: true,
    })
  }

  const goToNextTab = (direction) => {
    const next = NEWS_EDITOR_TABS[currentTabIndex + direction]
    if (!next) return
    setActiveTab(next.id)
    window.scrollTo({ top: 0, behavior: 'smooth' })
  }

  const applyJsonImport = () => {
    try {
      const parsed = parseStructuredNewsImport(jsonImportValue, {
        categoryOptions: props.categoryOptions,
        tagOptions: props.tagOptions,
        typeOptions: props.typeOptions,
        statusOptions: props.statusOptions,
      })

      Object.entries(parsed.next).forEach(([key, value]) => {
        form.setData(key, value)
      })

      if (parsed.authorQuery) {
        setAuthorQuery(parsed.authorQuery)
      }

      if (parsed.next.cover_image != null) {
        handleManualCoverChange(parsed.next.cover_image)
      }

      if (parsed.next.slug != null) {
        slugTouchedRef.current = true
      }

      setJsonImportError('')
      setJsonImportOpen(false)
      pushToast(`Applied ${parsed.applied.length} field${parsed.applied.length === 1 ? '' : 's'} from JSON.`, 'success')
    } catch (error) {
      setJsonImportError(error instanceof Error ? error.message : 'Could not parse JSON.')
    }
  }

  return (
    <StudioLayout title={props.title} subtitle={props.description}>
      <ToastStack toasts={toasts} onDismiss={dismissToast} />
      <div className="space-y-6 pb-24">
        <section className="overflow-hidden rounded-[28px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_34%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.94))] shadow-[0_24px_70px_rgba(2,6,23,0.34)] backdrop-blur">
          <div className="grid gap-5 border-b border-white/10 px-5 py-4 lg:grid-cols-[minmax(0,1fr)_360px] lg:items-start">
            <div className="min-w-0">
              <div className="flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">
                {props.indexUrl ? <a href={props.indexUrl} className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-white transition hover:bg-white/[0.08]">Back to news list</a> : null}
                <span>{article.id ? `Article #${article.id}` : 'New article'}</span>
                <span className="rounded-full border border-white/10 px-3 py-1.5 text-sky-100/80">{currentTab.label}</span>
              </div>
              <h1 className="mt-3 truncate text-2xl font-semibold tracking-[-0.03em] text-white">{String(form.data.title || '').trim() || (article.id ? 'Untitled article' : 'Create article')}</h1>
              <p className="mt-2 max-w-3xl text-sm leading-6 text-slate-400">Keep the draft flow simple: write the story in one place, handle publishing in one place, and keep promotion metadata nearby instead of buried below the fold.</p>
            </div>

            {coverPreviewUrl ? (
              <div className="overflow-hidden rounded-[24px] border border-white/10 bg-black/20 shadow-[0_18px_40px_rgba(2,6,23,0.35)]">
                <div className="relative aspect-[16/9] bg-black/30">
                  <img src={coverPreviewUrl} alt={String(form.data.title || '').trim() || 'News cover preview'} className="h-full w-full object-cover" />
                  <div className="absolute inset-0 bg-gradient-to-t from-[#020611d9] via-[#02061144] to-transparent" />
                  <div className="absolute inset-x-0 bottom-0 p-4">
                    <div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100/80">Header cover preview</div>
                    <div className="mt-1 line-clamp-2 text-sm font-semibold text-white">{String(form.data.title || '').trim() || 'Cover image preview'}</div>
                  </div>
                </div>
              </div>
            ) : null}
          </div>

          <div className="sticky top-16 z-30 border-y border-white/10 bg-[linear-gradient(180deg,rgba(9,14,24,0.98),rgba(6,10,18,0.98))] px-4 py-3 backdrop-blur">
            <div className="flex justify-end gap-2 overflow-x-auto">
              {frontendArticleUrl ? <a href={frontendArticleUrl} target="_blank" rel="noreferrer" className="rounded-2xl border border-cyan-300/20 bg-cyan-400/10 px-4 py-2.5 text-sm font-semibold text-cyan-100 transition hover:bg-cyan-400/15">Frontend link</a> : null}
              {props.previewUrl ? <a href={props.previewUrl} target="_blank" rel="noreferrer" className="rounded-2xl border border-indigo-300/20 bg-indigo-400/10 px-4 py-2.5 text-sm font-semibold text-indigo-100 transition hover:bg-indigo-400/15">Preview</a> : null}
              <button type="button" onClick={() => setJsonImportOpen(true)} className="rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.08]">Import JSON</button>
              <button type="submit" form="studio-news-editor-form" disabled={form.processing} className="inline-flex items-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-2.5 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15 disabled:opacity-60">
                {hasUnsavedChanges && !form.processing ? <span className="h-2.5 w-2.5 rounded-full bg-rose-400 shadow-[0_0_10px_rgba(251,113,133,0.9)] animate-pulse" aria-hidden="true" /> : null}
                <span>{form.processing ? 'Saving…' : 'Save article'}</span>
              </button>
              {props.publishUrl ? <button type="button" onClick={() => router.post(props.publishUrl)} className="rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-2.5 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-400/15">Publish now</button> : null}
            </div>
          </div>

          <div className="flex gap-2 overflow-x-auto px-4 py-3">
            {NEWS_EDITOR_TABS.map((tab) => {
              const active = tab.id === activeTab
              const errorCount = tabErrorCounts[tab.id] || 0

              return (
                <button
                  key={tab.id}
                  type="button"
                  onClick={() => setActiveTab(tab.id)}
                  className={`flex min-w-[170px] flex-col rounded-[22px] border px-4 py-3 text-left transition ${active ? 'border-sky-300/30 bg-sky-400/12 text-white' : 'border-white/10 bg-white/[0.03] text-slate-300 hover:bg-white/[0.06]'}`}
                >
                  <span className="flex items-center justify-between gap-3 text-sm font-semibold">
                    <span>{tab.label}</span>
                    {errorCount > 0 ? <span className="rounded-full border border-rose-300/20 bg-rose-400/10 px-2 py-0.5 text-[10px] uppercase tracking-[0.16em] text-rose-100">{errorCount}</span> : null}
                  </span>
                  <span className="mt-1 text-xs leading-5 text-slate-400">{tab.description}</span>
                </button>
              )
            })}
          </div>
        </section>

        <form id="studio-news-editor-form" onSubmit={submit} className="grid gap-6 xl:grid-cols-[minmax(0,1.1fr)_360px] xl:items-start">
          <div className="space-y-6">
            {activeTab === 'content' ? (
              <>
                <SectionCard
                  eyebrow="Story workspace"
                  title={article.id ? 'Shape the full newsroom story before it goes live.' : 'Create a newsroom story that reads like an editorial feature, not a raw database form.'}
                  description="The cover, excerpt, body, and structure below are the core writing flow. Everything else is support, not a distraction."
                  tone="feature"
                >
                  <div className="grid gap-5">
              <label className="grid gap-2 text-sm text-slate-300">
                <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Title</span>
                <input value={form.data.title} onChange={(event) => form.setData('title', event.target.value)} placeholder="Headline that can carry the article alone" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
                <span className="text-xs leading-5 text-slate-500">Aim for a clear editorial headline that still makes sense in cards, notifications, and social previews.</span>
                <FieldError message={form.errors.title} />
              </label>

              <label className="grid gap-2 text-sm text-slate-300">
                <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Slug</span>
                <div className="flex gap-2">
                  <input
                    value={form.data.slug}
                    onChange={(event) => {
                      const nextValue = event.target.value
                      slugTouchedRef.current = String(nextValue).trim() !== ''
                      form.setData('slug', nextValue)
                    }}
                    placeholder="optional-manual-slug"
                    className="min-w-0 flex-1 rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none"
                  />
                  <button
                    type="button"
                    onClick={() => {
                      slugTouchedRef.current = false
                      form.setData('slug', slugifyNewsTitle(form.data.title))
                    }}
                    className="shrink-0 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]"
                  >
                    Sync
                  </button>
                </div>
                <span className="text-xs leading-5 text-slate-500">The slug now follows the title automatically until you edit it manually. Use Sync to regenerate it from the current headline.</span>
                <FieldError message={form.errors.slug} />
              </label>

              <label className="grid gap-2 text-sm text-slate-300">
                <span className="flex items-center justify-between gap-3 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
                  <span>Excerpt</span>
                  <span className="text-slate-500">{excerptLength}/800</span>
                </span>
                <textarea value={form.data.excerpt} onChange={(event) => form.setData('excerpt', event.target.value)} rows={6} placeholder="Write the concise summary used in listing cards, metadata, and archive previews." className="rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
                <span className="text-xs leading-5 text-slate-500">Lead with the update, why it matters, and the audience hook. Two to four punchy sentences usually land better than one dense paragraph.</span>
                <FieldError message={form.errors.excerpt} />
              </label>

              <div className="grid gap-4">
                <WorldMediaUploadField
                  label="Cover image"
                  slot="cover"
                  value={form.data.cover_image}
                  previewUrl={coverPreviewUrl}
                  emptyLabel="Drop a cover image"
                  helperText="Upload the hero image directly to object storage. A wide landscape image works best for cards, preview surfaces, and social sharing."
                  uploadUrl={props.coverUploadUrl}
                  deleteUrl={props.coverDeleteUrl}
                  onChange={({ path, url }) => {
                    setStagedCoverPath(path || '')
                    form.setData('cover_image', path || '')
                    setCoverPreviewUrl(url || '')
                  }}
                  isTemporaryValue={Boolean(stagedCoverPath) && form.data.cover_image === stagedCoverPath}
                />
                <FieldError message={form.errors.cover_image} />

                <label className="grid gap-2 text-sm text-slate-300">
                  <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Advanced cover path or URL</span>
                  <input value={form.data.cover_image} onChange={(event) => handleManualCoverChange(event.target.value)} placeholder="Optional external URL or stored object path" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
                  <span className="text-xs leading-5 text-slate-500">Keep this for migrations, imported legacy stories, or when you already have the exact asset URL you want to use.</span>
                </label>
              </div>
                  </div>
                </SectionCard>

                <SectionCard eyebrow="Full message" title="Body editor" description="Write the main article in a richer editing surface so the content reads like a polished story, not pasted plain text." actions={<div className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.14em] text-white">{bodyWordCount.toLocaleString()} words</div>}>
                  <div className="grid gap-3 text-sm text-slate-300">
                    <RichTextEditor
                      content={form.data.content}
                      onChange={(nextValue) => form.setData('content', nextValue)}
                      placeholder="Open with the update, add context, use links, pull quotes, headings, and imagery where the story needs structure."
                      error={form.errors.content}
                      minHeight={24}
                      autofocus={false}
                      advancedNews
                      searchEntities={searchEntities}
                      mediaSupport={{
                        uploadUrl: props.bodyMediaUploadUrl || props.coverUploadUrl,
                        deleteUrl: props.bodyMediaDeleteUrl || props.coverDeleteUrl,
                        slot: 'body',
                      }}
                    />
                    <div className="rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 text-xs leading-6 text-slate-400">
                      Story workflow suggestion: lead with the change, explain why it matters, add supporting detail, then end with a clear call to action or next step.
                    </div>
                  </div>
                </SectionCard>
              </>
            ) : null}

            {activeTab === 'publishing' ? (
              <SectionCard eyebrow="Editorial controls" title="Publishing" description="Set ownership, placement, timing, and surface behavior before the article leaves draft.">
                <div className="grid gap-4">
                  <div className="grid gap-4 md:grid-cols-2">
                    <div className="grid gap-2 text-sm text-slate-300">
                      <NovaSelect label="Type" value={form.data.type || null} onChange={(nextValue) => form.setData('type', String(nextValue || ''))} options={typeOptions} searchable={false} className="bg-black/20" error={form.errors.type} />
                    </div>
                    <div className="grid gap-2 text-sm text-slate-300">
                      <NovaSelect label="Category" value={form.data.category_id || ''} onChange={(nextValue) => {
                        form.setData('category_id', String(nextValue || ''))
                        if (nextValue) {
                          form.clearErrors('category_id')
                        }
                      }} options={categoryOptions} searchable={false} className="bg-black/20" error={form.errors.category_id} />
                    </div>
                  </div>

                  <div className="grid gap-4 md:grid-cols-2">
                    <div className="grid gap-2 text-sm text-slate-300">
                      <NovaSelect label="Workflow status" value={form.data.editorial_status || null} onChange={(nextValue) => form.setData('editorial_status', String(nextValue || ''))} options={statusOptions} searchable={false} className="bg-black/20" error={form.errors.editorial_status} />
                    </div>
                    <div className="grid gap-2 text-sm text-slate-300">
                      <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Publish at</span>
                      <DateTimePicker value={form.data.published_at || ''} onChange={(nextValue) => form.setData('published_at', nextValue)} placeholder="Pick a publish slot" clearable className="bg-black/20" />
                      <FieldError message={form.errors.published_at} />
                    </div>
                  </div>

                  <div className="grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4">
                    <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Author</div>
                    <div className="flex gap-2">
                      <input value={authorQuery} onChange={(event) => setAuthorQuery(event.target.value)} placeholder="Search for an author" className="min-w-0 flex-1 rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
                      <button type="button" onClick={runAuthorSearch} className="rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm font-semibold text-white">Search</button>
                    </div>
                    {selectedAuthor ? (
                      <div className="rounded-2xl border border-emerald-300/20 bg-emerald-400/10 p-4 text-sm text-emerald-50">
                        <div className="font-semibold">Selected author: {selectedAuthor.title}</div>
                        {selectedAuthor.subtitle ? <div className="mt-1 text-xs uppercase tracking-[0.14em] text-emerald-100/70">{selectedAuthor.subtitle}</div> : null}
                      </div>
                    ) : null}
                    <SearchResultList items={authorResults} onSelect={(item) => {
                      setSelectedAuthor(item)
                      setAuthorQuery(item.title)
                      form.setData('author_id', item.id)
                    }} emptyLabel="Search to choose an author profile." />
                    <FieldError message={form.errors.author_id} />
                  </div>

                  <div className="grid gap-3 md:grid-cols-2">
                    <div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
                      <Checkbox checked={form.data.is_featured} onChange={(event) => form.setData('is_featured', event.target.checked)} label="Feature on newsroom surfaces" size={20} variant="accent" />
                    </div>
                    <div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
                      <Checkbox checked={form.data.is_pinned} onChange={(event) => form.setData('is_pinned', event.target.checked)} label="Pin to the top of the newsroom" size={20} variant="accent" />
                    </div>
                  </div>

                  <div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
                    <Checkbox checked={form.data.comments_enabled} onChange={(event) => form.setData('comments_enabled', event.target.checked)} label="Allow comments on the article page" size={20} variant="accent" />
                    <FieldError message={form.errors.comments_enabled} />
                  </div>
                </div>
              </SectionCard>
            ) : null}

            {activeTab === 'discoverability' ? (
              <>
                <SectionCard eyebrow="Taxonomy" title="Tags" description="Search and apply tags quickly instead of scanning a wall of checkboxes.">
                  <NewsTagInput
                    options={Array.isArray(props.tagOptions) ? props.tagOptions : []}
                    selectedIds={form.data.tag_ids}
                    newTagNames={form.data.new_tag_names}
                    onSelectedIdsChange={(nextIds) => form.setData('tag_ids', nextIds)}
                    onNewTagNamesChange={(nextNames) => form.setData('new_tag_names', nextNames)}
                    manageUrl={props.tagsUrl}
                          maxNewTags={props.newsTagLimit || NEWS_NEW_TAG_LIMIT}
                  />
                  <div className="mt-3">
                    <FieldError message={form.errors.tag_ids || form.errors.new_tag_names} />
                  </div>
                </SectionCard>

                <SectionCard eyebrow="Metadata" title="SEO and social" description="Keep search and sharing fields aligned with the main editorial package.">
                  <div className="grid gap-4">
                    <label className="grid gap-2 text-sm text-slate-300">
                      <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Meta title</span>
                      <input value={form.data.meta_title} onChange={(event) => form.setData('meta_title', event.target.value)} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
                    </label>
                    <label className="grid gap-2 text-sm text-slate-300">
                      <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Meta description</span>
                      <textarea value={form.data.meta_description} onChange={(event) => form.setData('meta_description', event.target.value)} rows={3} className="rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
                    </label>
                    <label className="grid gap-2 text-sm text-slate-300">
                      <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Meta keywords</span>
                      <input
                        value={form.data.meta_keywords}
                        onChange={(event) => form.setData('meta_keywords', event.target.value)}
                        placeholder="creator-story, release, tutorial"
                        maxLength={255}
                        className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none"
                      />
                      <span className="text-xs leading-5 text-slate-500">Maximum 255 characters. The field now stops at the limit so it fails less often on save.</span>
                      <FieldError message={form.errors.meta_keywords} />
                    </label>
                    <div className="grid gap-4 md:grid-cols-2">
                      <label className="grid gap-2 text-sm text-slate-300">
                        <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">OG title</span>
                        <input value={form.data.og_title} onChange={(event) => form.setData('og_title', event.target.value)} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
                      </label>
                      <label className="grid gap-2 text-sm text-slate-300">
                        <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">OG image</span>
                        <input value={form.data.og_image} onChange={(event) => form.setData('og_image', event.target.value)} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
                      </label>
                    </div>
                    <label className="grid gap-2 text-sm text-slate-300">
                      <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">OG description</span>
                      <textarea value={form.data.og_description} onChange={(event) => form.setData('og_description', event.target.value)} rows={3} className="rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
                    </label>
                  </div>
                </SectionCard>
              </>
            ) : null}

            {activeTab === 'connections' ? (
              <>
                <SectionCard eyebrow="Context links" title="Related entities" description="Attach groups, artworks, collections, releases, projects, challenges, events, and profiles so the article becomes part of the rest of Nova instead of a dead-end page." actions={<button type="button" onClick={addRelation} className="rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-2 text-sm font-semibold text-sky-100">Add relation</button>}>
                  <div className="grid gap-4">
                    {form.data.relations.length > 0 ? form.data.relations.map((relation, index) => (
                      <RelationCard
                        key={`${relation.entity_type}-${index}`}
                        relation={relation}
                        index={index}
                        onChange={updateRelation}
                        onRemove={removeRelation}
                        onSearch={runRelationSearch}
                        results={relationResults[index] || []}
                        relationTypeOptions={Array.isArray(props.relationTypeOptions) ? props.relationTypeOptions : []}
                      />
                    )) : <div className="rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-5 text-sm text-slate-400">No related entities attached yet.</div>}
                  </div>
                </SectionCard>

                <SectionCard eyebrow="Structured workflows" title="Paste article JSON" description="Import known fields from structured data, then refine the result inside the normal editor flow." actions={<button type="button" onClick={() => setJsonImportOpen(true)} className="rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white">Open import</button>}>
                  <div className="rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm leading-6 text-slate-400">
                    This is useful when another tool already generated article structure for you. Paste JSON, map it into the form, then review copy, metadata, and publish controls before saving.
                  </div>
                </SectionCard>
              </>
            ) : null}
          </div>

          <div className="space-y-6 xl:sticky xl:top-[9.75rem]">
            <SectionCard eyebrow="Overview" title="Editing progress" description="A compact status rail so you can see what still needs attention without leaving the current tab.">
              <div className="rounded-[24px] border border-white/10 bg-black/20 p-4">
                {hasUnsavedChanges ? <div className="mb-4 rounded-2xl border border-amber-300/20 bg-amber-400/10 px-4 py-3 text-sm text-amber-100">You have unsaved changes.</div> : <div className="mb-4 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-100">All changes saved.</div>}
                <div className="flex items-end justify-between gap-3">
                  <div>
                    <div className="text-3xl font-semibold tracking-[-0.04em] text-white">{completedCount}/{overviewItems.length}</div>
                    <div className="mt-1 text-sm text-slate-400">Core article pieces ready</div>
                  </div>
                  <div className="rounded-full border border-white/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] text-slate-300">{bodyWordCount.toLocaleString()} words</div>
                </div>
                <div className="mt-4 space-y-2">
                  {overviewItems.map((item) => (
                    <div key={item.label} className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-3 py-2.5 text-sm">
                      <span className="text-slate-200">{item.label}</span>
                      <span className={`rounded-full px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] ${item.done ? 'border border-emerald-300/20 bg-emerald-400/10 text-emerald-100' : 'border border-white/10 bg-white/[0.04] text-slate-400'}`}>{item.done ? 'Ready' : 'Missing'}</span>
                    </div>
                  ))}
                </div>
              </div>
            </SectionCard>

            <SectionCard eyebrow="Actions" title="Always-visible controls" description="Primary article actions stay pinned here as a second access point while you write.">
              <div className="grid gap-3">
                {Object.keys(form.errors || {}).length > 0 ? <div className="rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100">The article was not saved. Fix the highlighted fields and try again.</div> : null}
                <button type="submit" form="studio-news-editor-form" disabled={form.processing} className="w-full rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 disabled:opacity-60">{form.processing ? 'Saving article…' : 'Save article'}</button>
                {props.previewUrl ? <a href={props.previewUrl} target="_blank" rel="noreferrer" className="w-full rounded-full border border-indigo-300/20 bg-indigo-400/10 px-4 py-3 text-center text-sm font-semibold text-indigo-100">Preview article</a> : null}
                {props.publishUrl ? <button type="button" onClick={() => router.post(props.publishUrl)} className="w-full rounded-full border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm font-semibold text-emerald-100">Publish now</button> : null}
                {props.archiveUrl ? <button type="button" onClick={() => router.post(props.archiveUrl)} className="w-full rounded-full border border-white/10 bg-white/[0.05] px-4 py-3 text-sm font-semibold text-white">Archive article</button> : null}
                {props.featureUrl ? <button type="button" onClick={() => router.post(props.featureUrl)} className="w-full rounded-full border border-white/10 bg-white/[0.05] px-4 py-3 text-sm font-semibold text-white">Toggle featured</button> : null}
                {props.pinUrl ? <button type="button" onClick={() => router.post(props.pinUrl)} className="w-full rounded-full border border-amber-300/20 bg-amber-400/10 px-4 py-3 text-sm font-semibold text-amber-100">Toggle pinned</button> : null}
                {props.destroyUrl ? <button type="button" onClick={deleteArticle} className="w-full rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100">Move to trash</button> : null}
              </div>
            </SectionCard>

            <SectionCard eyebrow="Utilities" title="Editor shortcuts" description="Keep taxonomy management and structured import nearby without leaving the writing flow.">
              <div className="grid gap-3">
                <button type="button" onClick={() => setJsonImportOpen(true)} className="rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">Paste structured JSON</button>
                {props.categoriesUrl ? <a href={props.categoriesUrl} className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm font-semibold text-slate-100 transition hover:bg-white/[0.06]">Manage categories</a> : null}
                {props.tagsUrl ? <a href={props.tagsUrl} className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm font-semibold text-slate-100 transition hover:bg-white/[0.06]">Manage tags</a> : null}
              </div>
            </SectionCard>
          </div>
        </form>

        <nav className="sticky bottom-0 z-20 border-t border-white/10 bg-[rgba(2,6,23,0.92)] px-4 py-3 backdrop-blur xl:hidden">
          <div className="mx-auto flex max-w-7xl items-center justify-between gap-3">
            <button type="button" onClick={() => goToNextTab(-1)} disabled={currentTabIndex === 0} className="rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.08] disabled:opacity-50">Back</button>
            <div className="text-center">
              <div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Step {currentTabIndex + 1} / {NEWS_EDITOR_TABS.length}</div>
              <div className="mt-0.5 text-sm font-semibold text-white">{currentTab.label}</div>
            </div>
            <button type="button" onClick={() => goToNextTab(1)} disabled={currentTabIndex >= NEWS_EDITOR_TABS.length - 1} className="rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-2.5 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15 disabled:opacity-50">Next</button>
          </div>
        </nav>
      </div>

      <JsonImportDialog
        open={jsonImportOpen}
        value={jsonImportValue}
        error={jsonImportError}
        exportPayloads={jsonExportPayloads}
        articleData={imagePromptArticleData}
        newTagLimit={props.newsTagLimit || NEWS_NEW_TAG_LIMIT}
        onChange={(nextValue) => {
          setJsonImportValue(nextValue)
          if (jsonImportError) {
            setJsonImportError('')
          }
        }}
        onClose={() => {
          setJsonImportOpen(false)
          setJsonImportError('')
        }}
        onApply={applyJsonImport}
      />
    </StudioLayout>
  )
}