import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import nunjucks from 'nunjucks'
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { markdown } from '@codemirror/lang-markdown'
import {
    AbstractNodeProps,
    createAbstractNodeComponent,
    useAbstractNode,
} from '@/lib/graph/abstractNode'
import { getConfigContent, getConfigType, getConfigTypeId } from '@/lib/configTypes'
import { getSourceRenderingLogic } from '@/lib/sourceRenderingLogic'
import { useSyncConnectionStatus } from '@/lib/graph/nodeLifecycle'
import { useResizeHeight } from '@/hooks/useResizeHeight'
import { plantumlLanguage } from '@/lib/plantumlLanguage'
import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia } from '@/components/ui/empty'
import {
    BaseNode,
    BaseNodeContent,
    BaseNodeFooter,
    BaseNodeHeaderRow,
} from '@/components/graph/BaseNode'
import { getDefaultDataForType, getNextNodeId } from '@/lib/graph/flowUtils'
import FlowContext from '@/lib/graph/flowContext'
import { getDefaultStyle, getNodeType } from '@/lib/graph/nodeRegistry'
import { NodeFooterEdgeIndicators } from '@/components/graph/NodeFooterEdgeIndicators'
import { NodeHeaderTitle } from '@/components/graph/NodeHeaderTitle'
import { NodeMenubar } from '@/components/graph/NodeMenubar'
import { NodeStatusIndicator } from '@/components/graph/NodeStatusIndicator'
import { MenubarItem, MenubarSeparator, MenubarSub, MenubarSubContent, MenubarSubTrigger } from '@/components/ui/menubar'
import { Sparkles, ZoomIn, ZoomOut, RotateCcw, RotateCw, Copy, Play, ChevronDown } from 'lucide-react'
import { InputHandle } from '@/components/graph/NodeHandles'
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import {
    DropdownMenu,
    DropdownMenuCheckboxItem,
    DropdownMenuContent,
    DropdownMenuLabel,
    DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { useTheme } from '@/lib/themeContext'
import { toast } from 'sonner'

export type RenderingNodeData = {
    viewportWidth?: number
    viewportHeight?: number
    /** When set, overrides the source node's default. 'auto' = re-render on upstream changes; 'manual' = only when user clicks Run. */
    updateMode?: 'auto' | 'manual'
    /** Incremented when user clicks Run (manual mode). Effect runs when this changes. */
    runTrigger?: number
    /** Signature of inputs used in the last successful render. Used in manual mode to show paused (yellow) when upstream changed. */
    lastRunSourceSignature?: string
}

const DEFAULT_VIEWPORT_WIDTH = 1200
const DEFAULT_VIEWPORT_HEIGHT = 800

type Props = AbstractNodeProps<RenderingNodeData>

type ViewMode = 'preview' | 'raw'

function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
    const flowContext = useContext(FlowContext)
    const setFullscreenNodeId = flowContext?.setFullscreenNodeId
    const supportsFullscreen = getNodeType('render')?.supportsFullscreen
    const [renderedContent, setRenderedContent] = useState<string | null>(null)
    const [resolvedContent, setResolvedContent] = useState<string | null>(null)
    const [error, setError] = useState<null | { kind: string; message: string }>(null)
    const [loading, setLoading] = useState(false)
    const [retryCount, setRetryCount] = useState(0)
    const [viewMode, setViewMode] = useState<ViewMode>('preview')
    const [viewportFocused, setViewportFocused] = useState(false)
    const runIdRef = useRef(0)
    const loadingStartedAtRef = useRef<number | null>(null)
    const minLoadingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
    const lastManualRunTriggerRef = useRef<number>(0)
    const { nodes, edges, setNodes, setEdges, sourceIds, updateData } = useAbstractNode<RenderingNodeData>(id, data ?? {})
    const viewportWidth = data?.viewportWidth ?? DEFAULT_VIEWPORT_WIDTH
    const viewportHeight = data?.viewportHeight ?? DEFAULT_VIEWPORT_HEIGHT

    const incomingIds = sourceIds
    const srcId = incomingIds.length > 0 ? incomingIds[0] : null
    const srcNode = nodes.find((n: any) => n.id === srcId)
    const sourceLogic = useMemo(() => (srcNode?.type ? getSourceRenderingLogic(srcNode.type) : null), [srcNode?.type])
    const effectiveUpdateMode = data?.updateMode ?? sourceLogic?.defaultUpdateMode ?? 'auto'
    const runTrigger = data?.runTrigger ?? 0
    const isAgentSource = srcNode?.type === 'agent'
    const agentOutputMarkdown = isAgentSource ? ((srcNode.data as { outputMarkdown?: string })?.outputMarkdown ?? '') : ''
    const configTypeId = srcNode?.type === 'config' ? getConfigTypeId((srcNode.data ?? undefined) as Record<string, unknown> | undefined) : isAgentSource ? 'markdown' : 'plantuml'
    const configType = getConfigType(configTypeId)
    const outputType = configType.outputType
    const sourceContent = srcNode?.type === 'config' ? getConfigContent((srcNode.data ?? undefined) as Record<string, unknown> | undefined) : isAgentSource ? agentOutputMarkdown : ''
    const srcData = srcNode?.data ?? {}

    const triggerNodeIds = flowContext?.connectionPathTriggerNodeIds ?? []

    /** Set of node IDs that can affect this render node (configs in the chain + variables/functions feeding them) */
    const connectedNodeIds = useMemo(() => {
        const out = new Set<string>()
        const isReachable = (startId: string, targetId: string) => {
            const q: string[] = [startId]
            const seen = new Set<string>([startId])
            while (q.length) {
                const cur = q.shift()!
                if (cur === targetId) return true
                for (const e of edges) {
                    if (e.source === cur && !seen.has(e.target)) {
                        seen.add(e.target)
                        q.push(e.target)
                    }
                }
            }
            return false
        }
        const resolveRef = (name: string) => {
            const refName = name.replace(/\.(puml|html)$/, '').trim()
            return nodes.find((n: any) => n.id === refName || n.data?.title === refName)?.id ?? refName
        }
        const getTemplateRefs = (content: string): string[] => {
            const refs: string[] = []
            const extendMatch = content.match(/\{\%\s*extends\s+["']([^"']+)["']\s*\%\}/)
            if (extendMatch) refs.push(extendMatch[1].trim())
            const includeRegex = /\{\%\s*include\s+["']([^"']+)["']\s*\%\}/g
            let m
            while ((m = includeRegex.exec(content)) !== null) refs.push(m[1].trim())
            const importRegex = /\{\%\s*import\s+["']([^"']+)["']\s+as\s+\w+\s*\%\}/g
            while ((m = importRegex.exec(content)) !== null) refs.push(m[1].trim())
            return refs
        }
        const addConfigRefs = (nodeId: string, visited: Set<string>) => {
            if (visited.has(nodeId)) return
            const node = nodes.find((n: any) => n.id === nodeId && n.type === 'config')
            if (!node) return
            visited.add(nodeId)
            out.add(nodeId)
            const content = getConfigContent((node.data ?? undefined) as Record<string, unknown> | undefined)
            for (const ref of getTemplateRefs(content)) {
                const refId = resolveRef(ref)
                if (refId && nodes.some((n: any) => n.id === refId && n.type === 'config') && isReachable(refId, id))
                    addConfigRefs(refId, visited)
            }
        }
        const configVisited = new Set<string>()
        for (const nid of incomingIds) {
            const node = nodes.find((n: any) => n.id === nid)
            if (node?.type === 'config') addConfigRefs(nid, configVisited)
            else out.add(nid)
        }
        for (const e of edges) {
            if (out.has(e.target)) out.add(e.source)
        }
        return out
    }, [nodes, edges, id, incomingIds])

    const configSignature = useMemo(
        () =>
            nodes
                .filter((n: any) => n.type === 'config' && connectedNodeIds.has(n.id))
                .map((n: any) => `${n.id}:${n.data?.title ?? ''}:${getConfigContent(n.data)}`)
                .sort()
                .join('|'),
        [nodes, connectedNodeIds]
    )

    const edgesSignature = useMemo(
        () =>
            edges
                .filter((e: any) => connectedNodeIds.has(e.source) && (connectedNodeIds.has(e.target) || e.target === id))
                .map((e: any) => `${e.source}->${e.target}`)
                .sort()
                .join('|'),
        [edges, connectedNodeIds, id]
    )

    const variablesSignature = useMemo(
        () =>
            nodes
                .filter((n: any) => n.type === 'variable' && connectedNodeIds.has(n.id))
                .map((n: any) => `${n.id}:${n.data?.value}`)
                .sort()
                .join('|'),
        [nodes, connectedNodeIds]
    )

    const functionsSignature = useMemo(
        () =>
            nodes
                .filter((n: any) => n.type === 'function' && connectedNodeIds.has(n.id))
                .map((n: any) => `${n.id}:${n.data?.body ?? ''}`)
                .sort()
                .join('|'),
        [nodes, connectedNodeIds]
    )

    const dataSignature = useMemo(
        () =>
            nodes
                .filter((n: any) => n.type === 'data' && connectedNodeIds.has(n.id))
                .map((n: any) => `${n.id}:${JSON.stringify(n.data?.rows ?? [])}:${JSON.stringify(n.data?.hiddenColumns ?? [])}`)
                .sort()
                .join('|'),
        [nodes, connectedNodeIds]
    )

    /** Single signature of all inputs that affect this render. Stored on successful render for manual-mode paused state. */
    const sourceSignature = useMemo(
        () =>
            JSON.stringify({
                configSignature,
                edgesSignature,
                variablesSignature,
                functionsSignature,
                dataSignature,
            }),
        [configSignature, edgesSignature, variablesSignature, functionsSignature, dataSignature]
    )

    const lastRunSourceSignature = data?.lastRunSourceSignature
    /**
     * In manual mode, yellow = "dirty": inputs changed since the last manual run (or never run).
     * We report paused when dirty so we get added to connectionPathPausedNodeIds; the path is
     * then computed as downstream(trigger) ∩ upstream(paused), so we must not require
     * pathNodeIds.has(id) here (that would be a chicken-and-egg).
     */
    const hasPendingInputs =
        effectiveUpdateMode === 'manual' &&
        !loading &&
        triggerNodeIds.length > 0 &&
        incomingIds.length > 0 &&
        sourceSignature !== lastRunSourceSignature

    useSyncConnectionStatus(id, { updating: loading, error: error != null, paused: hasPendingInputs })

    const RENDER_DEBOUNCE_MS = 250

    useEffect(() => {
        if (incomingIds.length === 0) {
            setRenderedContent(null)
            setResolvedContent(null)
            setError(null)
            setLoading(false)
            return
        }
        if (!srcId || !srcNode) {
            setRenderedContent(null)
            setResolvedContent(null)
            setError(null)
            setLoading(false)
            return
        }
        const logic = getSourceRenderingLogic(srcNode.type ?? '')
        if (!logic) {
            setRenderedContent(null)
            setResolvedContent(null)
            setError({ kind: 'render', message: `Unsupported source type: ${srcNode.type}` })
            setLoading(false)
            return
        }
        if (effectiveUpdateMode === 'manual' && runTrigger === 0) {
            setRenderedContent(null)
            setResolvedContent(null)
            setError({
                kind: 'no-content',
                message: isAgentSource ? 'Run the Agent node to generate output, then click Run here.' : 'Click Run to render.',
            })
            setLoading(false)
            return
        }
        if (effectiveUpdateMode === 'manual' && runTrigger === lastManualRunTriggerRef.current) {
            return
        }
        if (effectiveUpdateMode === 'manual') lastManualRunTriggerRef.current = runTrigger

        runIdRef.current += 1
        const thisRunId = runIdRef.current
        const signatureForThisRun = sourceSignature
        const isManualMode = effectiveUpdateMode === 'manual'
        let cancelled = false

        const run = async () => {
            loadingStartedAtRef.current = Date.now()
            setLoading(true)
            setError(null)
            try {
                const context = {
                    nodes,
                    edges,
                    sourceNodeId: srcId,
                    renderNodeId: id,
                    viewportWidth,
                    viewportHeight,
                }
                const { resolved, outputTypeId } = await logic.getResolvedContent(context)
                if (cancelled || thisRunId !== runIdRef.current) return
                setResolvedContent(resolved)
                const typeRenderer = getConfigType(outputTypeId)
                const renderOptions = outputTypeId === 'wireframe' ? { width: viewportWidth, height: viewportHeight } : undefined
                const htmlOrSvg = await typeRenderer.render(resolved, renderOptions)
                if (cancelled || thisRunId !== runIdRef.current) return
                setRenderedContent(htmlOrSvg)
                setError(null)
                if (isManualMode) {
                    updateData({ lastRunSourceSignature: signatureForThisRun })
                }
            } catch (err: any) {
                if (!cancelled && thisRunId === runIdRef.current) {
                    setRenderedContent(null)
                    setError({ kind: 'render', message: err?.message ?? 'Render error' })
                }
            } finally {
                if (!cancelled && thisRunId === runIdRef.current) {
                    const startedAt = loadingStartedAtRef.current ?? 0
                    const elapsed = Date.now() - startedAt
                    const remaining = Math.max(0, 1000 - elapsed)
                    if (remaining > 0) {
                        minLoadingTimeoutRef.current = setTimeout(() => {
                            minLoadingTimeoutRef.current = null
                            if (!cancelled && thisRunId === runIdRef.current) {
                                setLoading(false)
                            }
                        }, remaining)
                    } else {
                        setLoading(false)
                    }
                }
            }
        }

        if (effectiveUpdateMode === 'auto') {
            const debounceTimer = setTimeout(run, RENDER_DEBOUNCE_MS)
            return () => {
                cancelled = true
                clearTimeout(debounceTimer)
                if (minLoadingTimeoutRef.current != null) {
                    clearTimeout(minLoadingTimeoutRef.current)
                    minLoadingTimeoutRef.current = null
                }
                setLoading(false)
            }
        }
        run()
        return () => {
            cancelled = true
            if (minLoadingTimeoutRef.current != null) {
                clearTimeout(minLoadingTimeoutRef.current)
                minLoadingTimeoutRef.current = null
            }
            setLoading(false)
        }
    }, [id, srcId, srcNode?.type, effectiveUpdateMode, runTrigger, sourceContent, sourceSignature, configSignature, edgesSignature, variablesSignature, functionsSignature, dataSignature, viewportWidth, viewportHeight, retryCount, nodes, edges, updateData])


    const dimensions =
        width != null && height != null && width > 0 && height > 0
            ? { width, height }
            : undefined

    // Kroki and other SVG sources may prepend <?xml ... ?> so we detect by presence of <svg> tag
    const isSvgOutput = Boolean(renderedContent?.trim() && /<svg[\s>]/i.test(renderedContent.trim()))

    /** Strip remaining Nunjucks tags from resolved content for display in raw view (so tags don't show as literal lines). */
    const rawDisplayContent = useMemo(() => {
        if (resolvedContent == null) return ''
        return resolvedContent
            .replace(/\{%[\s\S]*?%\}/g, '')
            .replace(/\{\{[\s\S]*?\}\}/g, '')
            .replace(/\{#[\s\S]*?#\}/g, '')
            .replace(/(\r?\n)\s*(\r?\n)/g, '$1$2')
            .replace(/^\s*\n|\n\s*$/g, (m) => (m === '\n' ? '\n' : ''))
            .trim()
    }, [resolvedContent])

    /** Process SVG HTML so it keeps aspect ratio and fills the viewport (used only for display, not download). */
    const displayContent = useMemo(() => {
        if (!renderedContent || !isSvgOutput) return renderedContent
        let html = renderedContent
        // Force preserve aspect ratio so the diagram is not stretched (Kroki often returns preserveAspectRatio="none")
        html = html.replace(/\bpreserveAspectRatio\s*=\s*["']none["']/gi, 'preserveAspectRatio="xMidYMid meet"')
        // Make root SVG fill container so it scales uniformly with meet
        html = html.replace(/\bwidth\s*=\s*["'][^"']*["']/i, 'width="100%"')
        html = html.replace(/\bheight\s*=\s*["'][^"']*["']/i, 'height="100%"')
        // Override inline style width/height so they don't override the attributes
        html = html.replace(/\bstyle\s*=\s*["']([^"']*)["']/i, (_, style) => {
            const overridden = style.replace(/\b(width|height):[^;]+/gi, '$1:100%')
            return `style="${overridden}"`
        })
        return html
    }, [renderedContent, isSvgOutput])

    const downloadSvg = useCallback(() => {
        if (!renderedContent || !isSvgOutput) return
        const blob = new Blob([renderedContent], { type: 'image/svg+xml' })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = `${id}.svg`
        a.click()
        URL.revokeObjectURL(url)
    }, [id, renderedContent, isSvgOutput])

    const downloadPng = useCallback(() => {
        if (!renderedContent || !isSvgOutput) return
        const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(renderedContent)
        const img = new Image()
        img.onload = () => {
            const canvas = document.createElement('canvas')
            canvas.width = img.naturalWidth
            canvas.height = img.naturalHeight
            const ctx = canvas.getContext('2d')
            if (!ctx) return
            ctx.drawImage(img, 0, 0)
            const pngUrl = canvas.toDataURL('image/png')
            const a = document.createElement('a')
            a.href = pngUrl
            a.download = `${id}.png`
            a.click()
        }
        img.onerror = () => { }
        img.src = dataUrl
    }, [id, renderedContent, isSvgOutput])

    const copyPng = useCallback(() => {
        if (!renderedContent || !isSvgOutput) return
        const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(renderedContent)
        const img = new Image()
        img.onload = () => {
            const canvas = document.createElement('canvas')
            canvas.width = img.naturalWidth
            canvas.height = img.naturalHeight
            const ctx = canvas.getContext('2d')
            if (!ctx) return
            ctx.drawImage(img, 0, 0)
            canvas.toBlob((blob) => {
                if (blob) navigator.clipboard?.write([new ClipboardItem({ 'image/png': blob })]).catch(() => { })
            }, 'image/png')
        }
        img.onerror = () => { }
        img.src = dataUrl
    }, [renderedContent, isSvgOutput])

    const copySvg = useCallback(() => {
        if (!renderedContent || !isSvgOutput) return
        const blob = new Blob([renderedContent], { type: 'image/svg+xml' })
        navigator.clipboard?.write([new ClipboardItem({ 'image/svg+xml': blob })]).catch(() => { })
    }, [renderedContent, isSvgOutput])

    const status = loading ? 'loading' : error ? 'error' : renderedContent ? 'success' : 'initial'

    const [viewportDraft, setViewportDraft] = useState({ width: viewportWidth, height: viewportHeight })
    useEffect(() => {
        setViewportDraft({ width: viewportWidth, height: viewportHeight })
    }, [viewportWidth, viewportHeight])

    const onViewportDraftChange = useCallback((field: 'width' | 'height', value: number) => {
        setViewportDraft((prev) => ({ ...prev, [field]: Math.min(4000, Math.max(200, value)) }))
    }, [])
    const onViewportApply = useCallback(() => {
        updateData({ viewportWidth: viewportDraft.width, viewportHeight: viewportDraft.height })
    }, [updateData, viewportDraft.width, viewportDraft.height])

    const { theme } = useTheme()
    const [rawEditorHeight, rawEditorContainerRef] = useResizeHeight(180, [viewMode])
    const rawExtensions = useMemo(() => {
        const lang =
            configTypeId === 'wireframe'
                ? javascript()
                : configType.language === 'plantuml'
                    ? plantumlLanguage.extension
                    : markdown()
        return [lang]
    }, [configTypeId, configType.language])

    return (
        <NodeStatusIndicator status={status} variant="overlay" width={dimensions?.width} height={dimensions?.height}>
            <BaseNode className="min-w-96 min-h-[320px]" dimensions={dimensions} resizable nodeId={id} selected={selected} handles={<InputHandle id="ain" nodeId={id} />}>
                <BaseNodeHeaderRow
                    icon={<Sparkles className="size-4" />}
                    title={<NodeHeaderTitle nodeId={id} displayTitle={id} />}
                    onHeaderDoubleClick={supportsFullscreen && setFullscreenNodeId ? () => setFullscreenNodeId(id) : undefined}
                    right={
                        incomingIds.length > 0 ? (
                            <div className="flex items-center gap-2 shrink-0">
                                <ButtonGroup className="nodrag nopan">
                                    <Button
                                        type="button"
                                        size="sm"
                                        variant="outline"
                                        className="h-7 gap-1.5 rounded-r-none border-r-0 px-2.5 text-xs"
                                        onClick={(e) => {
                                            e.stopPropagation()
                                            updateData({ runTrigger: (data?.runTrigger ?? 0) + 1 })
                                        }}
                                    >
                                        <Play className="size-3.5" />
                                        Run
                                    </Button>
                                    <DropdownMenu>
                                        <DropdownMenuTrigger asChild>
                                            <Button
                                                type="button"
                                                size="sm"
                                                variant="outline"
                                                className="h-7 min-w-[4.5rem] gap-1 rounded-l-none pl-2 pr-1.5 text-xs font-normal"
                                                aria-label="Update mode"
                                                onClick={(e) => e.stopPropagation()}
                                            >
                                                <span className="text-muted-foreground">
                                                    {effectiveUpdateMode === 'manual' ? 'Manual' : 'Auto'}
                                                </span>
                                                <ChevronDown className="size-3.5 shrink-0 opacity-70" />
                                            </Button>
                                        </DropdownMenuTrigger>
                                        <DropdownMenuContent align="end" className="w-64" onClick={(e) => e.stopPropagation()}>
                                            <DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
                                                When to re-render
                                            </DropdownMenuLabel>
                                            <DropdownMenuCheckboxItem
                                                checked={effectiveUpdateMode === 'auto'}
                                                onCheckedChange={(checked) => checked && updateData({ updateMode: 'auto' })}
                                                className="flex flex-col items-start gap-0.5 py-2"
                                            >
                                                <span className="font-medium">Auto</span>
                                                <span className="text-muted-foreground text-xs font-normal">
                                                    Re-renders when upstream content changes
                                                </span>
                                            </DropdownMenuCheckboxItem>
                                            <DropdownMenuCheckboxItem
                                                checked={effectiveUpdateMode === 'manual'}
                                                onCheckedChange={(checked) => checked && updateData({ updateMode: 'manual' })}
                                                className="flex flex-col items-start gap-0.5 py-2"
                                            >
                                                <span className="font-medium">Manual</span>
                                                <span className="text-muted-foreground text-xs font-normal">
                                                    Re-renders only when you click Run
                                                </span>
                                            </DropdownMenuCheckboxItem>
                                        </DropdownMenuContent>
                                    </DropdownMenu>
                                </ButtonGroup>
                                <ToggleGroup
                                    type="single"
                                    value={viewMode}
                                    onValueChange={(v) => {
                                        if (v === 'preview' || v === 'raw') setViewMode(v)
                                    }}
                                    aria-label="View mode"
                                    variant="outline"
                                    size="sm"
                                    className="gap-0 rounded-md p-0.5 [&>button]:rounded-none [&>button:first-child]:rounded-l-md [&>button:last-child]:rounded-r-md [&>button:not(:first-child)]:border-l-0"
                                >
                                    <ToggleGroupItem
                                        value="preview"
                                        aria-label="Preview"
                                        className="gap-1.5 px-2.5 h-7"
                                        onClick={(e) => {
                                            e.stopPropagation()
                                            setViewMode('preview')
                                        }}
                                    >
                                        Preview
                                    </ToggleGroupItem>
                                    <ToggleGroupItem
                                        value="raw"
                                        aria-label="Raw config"
                                        className="gap-1.5 px-2.5 h-7"
                                        onClick={(e) => {
                                            e.stopPropagation()
                                            setViewMode('raw')
                                        }}
                                    >
                                        Raw
                                    </ToggleGroupItem>
                                </ToggleGroup>
                            </div>
                        ) : undefined
                    }
                />

                <BaseNodeContent>
                    <div className="shrink-0 w-full">
                        <NodeMenubar
                            nodeId={id}
                            nodeType="render"
                            nodeMenuExtraContent={
                                <>
                                    {outputType === 'image' && (
                                        <MenubarSub>
                                            <MenubarSubTrigger className="text-xs">Viewport</MenubarSubTrigger>
                                            <MenubarSubContent className="min-w-[12rem] p-2">
                                                <div className="grid gap-2">
                                                    <div className="flex items-center gap-2">
                                                        <label className="text-xs text-muted-foreground shrink-0">Width</label>
                                                        <Input
                                                            type="number"
                                                            min={200}
                                                            max={4000}
                                                            value={viewportDraft.width}
                                                            onChange={(e) => {
                                                                const v = parseInt(e.target.value, 10)
                                                                if (!Number.isNaN(v)) onViewportDraftChange('width', v)
                                                            }}
                                                            className="h-7 text-xs"
                                                        />
                                                    </div>
                                                    <div className="flex items-center gap-2">
                                                        <label className="text-xs text-muted-foreground shrink-0">Height</label>
                                                        <Input
                                                            type="number"
                                                            min={200}
                                                            max={4000}
                                                            value={viewportDraft.height}
                                                            onChange={(e) => {
                                                                const v = parseInt(e.target.value, 10)
                                                                if (!Number.isNaN(v)) onViewportDraftChange('height', v)
                                                            }}
                                                            className="h-7 text-xs"
                                                        />
                                                    </div>
                                                    <button
                                                        type="button"
                                                        onClick={onViewportApply}
                                                        className="mt-1 w-full rounded bg-primary px-2 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90"
                                                    >
                                                        Apply
                                                    </button>
                                                </div>
                                            </MenubarSubContent>
                                        </MenubarSub>
                                    )}
                                    <MenubarSub>
                                        <MenubarSeparator />
                                        <MenubarSubTrigger className="text-xs" disabled={!isSvgOutput}>
                                            Export / Copy
                                        </MenubarSubTrigger>
                                        <MenubarSubContent className="min-w-[10rem]" aria-label="Export or copy diagram">
                                            <MenubarItem className="text-xs" onClick={downloadSvg} disabled={!isSvgOutput}>
                                                Download SVG
                                            </MenubarItem>
                                            <MenubarItem className="text-xs" onClick={downloadPng} disabled={!isSvgOutput}>
                                                Download PNG
                                            </MenubarItem>
                                            <MenubarItem className="text-xs" onClick={copySvg} disabled={!isSvgOutput}>
                                                Copy SVG
                                            </MenubarItem>
                                            <MenubarItem className="text-xs" onClick={copyPng} disabled={!isSvgOutput}>
                                                Copy image
                                            </MenubarItem>
                                        </MenubarSubContent>
                                    </MenubarSub>
                                </>
                            }
                        />
                    </div>
                    <div className="min-h-0 flex-1 flex flex-col">
                        {incomingIds.length === 0 ? (
                            <Empty className="min-h-0 flex-1">
                                <EmptyHeader>
                                    <EmptyMedia variant="icon">
                                        <Sparkles className="size-6" />
                                    </EmptyMedia>
                                    <EmptyTitle>No configuration connected</EmptyTitle>
                                    <EmptyDescription>Connect a Configuration node or create one. The renderer will display the diagram or document.</EmptyDescription>
                                </EmptyHeader>
                                <EmptyContent>
                                    <button
                                        className="inline-flex items-center rounded bg-primary px-3 py-1 text-xs text-primary-foreground"
                                        onClick={() => {
                                            if (!setNodes || !setEdges) return
                                            const nid = getNextNodeId('config', nodes.map((n: any) => n.id))
                                            const thisNode = nodes.find((n: any) => n.id === id)
                                            const pos = thisNode?.position ?? { x: 0, y: 0 }
                                            const newPos = { x: pos.x - 220, y: pos.y }
                                            const newNode = { id: nid, type: 'config', position: newPos, data: getDefaultDataForType('config', nid), style: getDefaultStyle('config') }
                                            setNodes((nds: any[]) => nds.concat(newNode))
                                            setEdges((eds: any[]) => eds.concat({ id: `e-${nid}-${id}`, source: nid, target: id }))
                                        }}
                                    >
                                        Create Config
                                    </button>
                                </EmptyContent>
                            </Empty>
                        ) : error ? (
                            (srcData as { renderError?: (err: { kind: string; message: string }) => React.ReactNode; errorHtml?: string })?.renderError ? (
                                (srcData as { renderError: (err: { kind: string; message: string }) => React.ReactNode }).renderError(error)
                            ) : (srcData as { errorHtml?: string })?.errorHtml ? (
                                <div className="p-3 text-xs text-red-700 dark:text-red-400" dangerouslySetInnerHTML={{ __html: String((srcData as { errorHtml: string }).errorHtml) }} />
                            ) : (
                                <div className="flex flex-col gap-2 p-3">
                                    <p className="text-xs text-red-700 dark:text-red-400">{error.message}</p>
                                    <Button
                                        type="button"
                                        variant="outline"
                                        size="sm"
                                        className="w-fit"
                                        onClick={() => setRetryCount((c) => c + 1)}
                                    >
                                        <RotateCw className="size-3 mr-1" />
                                        Retry
                                    </Button>
                                </div>
                            )
                        ) : loading ? (
                            <div className="flex min-h-0 flex-1 items-center justify-center text-xs text-muted-foreground">Rendering…</div>
                        ) : viewMode === 'raw' ? (
                            <div ref={rawEditorContainerRef} className="relative min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input">
                                <Button
                                    type="button"
                                    variant="secondary"
                                    size="icon"
                                    className="absolute top-2 right-2 z-10 h-7 w-7 shrink-0 rounded-md shadow-sm"
                                    onClick={() => {
                                        const text = rawDisplayContent
                                        if (text) {
                                            navigator.clipboard?.writeText(text).then(() => toast.success('Copied to clipboard')).catch(() => { })
                                        }
                                    }}
                                    disabled={!rawDisplayContent}
                                    title="Copy raw output"
                                >
                                    <Copy className="size-3.5" />
                                </Button>
                                <CodeMirror
                                    value={rawDisplayContent}
                                    height={`${rawEditorHeight}px`}
                                    theme={theme}
                                    extensions={rawExtensions}
                                    readOnly
                                    editable={false}
                                    basicSetup={{ lineNumbers: true, foldGutter: false }}
                                    className="text-xs [&_.cm-editor]:outline-none [&_.cm-editor]:cursor-text [&_.cm-gutters]:border-0 [&_.cm-scroller]:min-h-0"
                                />
                            </div>
                        ) : renderedContent ? (
                            outputType === 'image' ? (
                                <div
                                    className="rendering-viewport nodrag nopan relative min-h-0 flex-1 w-full min-w-0 overflow-hidden bg-white dark:bg-secondary rounded outline-none"
                                    tabIndex={0}
                                    onFocus={() => setViewportFocused(true)}
                                    onBlur={() => setViewportFocused(false)}
                                >
                                    <TransformWrapper
                                        initialScale={1}
                                        initialPositionX={0}
                                        initialPositionY={0}
                                        minScale={0.2}
                                        maxScale={4}
                                        centerOnInit={true}
                                        onInit={(ctx) => {
                                            if (!ctx?.instance?.wrapperComponent || !ctx?.instance?.contentComponent) return
                                            const fitToView = () => {
                                                const wrapper = ctx.instance.wrapperComponent
                                                const content = ctx.instance.contentComponent
                                                if (!wrapper || !content) return
                                                const wW = wrapper.clientWidth
                                                const wH = wrapper.clientHeight
                                                const cW = content.scrollWidth || content.clientWidth
                                                const cH = content.scrollHeight || content.clientHeight
                                                if (cW > 0 && cH > 0) {
                                                    const scale = Math.min(wW / cW, wH / cH, 1)
                                                    const posX = (wW - cW * scale) / 2
                                                    const posY = (wH - cH * scale) / 2
                                                    ctx.setTransform(posX, posY, scale, 0)
                                                }
                                            }
                                            requestAnimationFrame(() => {
                                                requestAnimationFrame(fitToView)
                                            })
                                        }}
                                        panning={{ disabled: !selected && !viewportFocused }}
                                        wheel={{ disabled: !selected && !viewportFocused }}
                                        doubleClick={{ disabled: !selected && !viewportFocused }}
                                    >
                                        {({ zoomIn, zoomOut, resetTransform }) => (
                                            <>
                                                <div className="react-flow__controls absolute bottom-2 left-2 z-10 nodrag nopan">
                                                    <button
                                                        type="button"
                                                        onClick={() => zoomIn()}
                                                        className="react-flow__controls-button"
                                                        title="Zoom in"
                                                    >
                                                        <ZoomIn className="size-3 max-w-[12px] max-h-[12px]" />
                                                    </button>
                                                    <button
                                                        type="button"
                                                        onClick={() => zoomOut()}
                                                        className="react-flow__controls-button"
                                                        title="Zoom out"
                                                    >
                                                        <ZoomOut className="size-3 max-w-[12px] max-h-[12px]" />
                                                    </button>
                                                    <button
                                                        type="button"
                                                        onClick={() => resetTransform()}
                                                        className="react-flow__controls-button"
                                                        title="Reset view (fit all)"
                                                    >
                                                        <RotateCcw className="size-3 max-w-[12px] max-h-[12px]" />
                                                    </button>
                                                </div>
                                                <div className="absolute inset-0 nodrag nopan overflow-hidden flex flex-col">
                                                    <TransformComponent
                                                        wrapperClass="!w-full !h-full"
                                                        contentClass="inline-flex flex-col nodrag nopan !w-full !min-h-0 !flex-1"
                                                    >
                                                        <div
                                                            className="rendering-diagram inline-flex w-full min-h-0 flex-1 p-4 nodrag nopan"
                                                            dangerouslySetInnerHTML={{ __html: displayContent ?? '' }}
                                                        />
                                                    </TransformComponent>
                                                </div>
                                            </>
                                        )}
                                    </TransformWrapper>
                                </div>
                            ) : (
                                <div
                                    className="rendering-markdown min-h-0 flex-1 w-full overflow-auto p-3 text-sm [&_h1]:text-xl [&_h2]:text-lg [&_h3]:text-base [&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 [&_p]:my-2 [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded"
                                    dangerouslySetInnerHTML={{ __html: renderedContent }}
                                />
                            )
                        ) : null}
                    </div>
                </BaseNodeContent>

                <BaseNodeFooter>
                    <NodeFooterEdgeIndicators nodeId={id} nodeType="render">
                        {viewMode === 'raw'
                            ? (rawDisplayContent ? `Raw · ${rawDisplayContent.length} chars` : '—')
                            : renderedContent
                                ? `${configType.label} · ${renderedContent.length} chars`
                                : error
                                    ? 'Error'
                                    : '—'}
                    </NodeFooterEdgeIndicators>
                </BaseNodeFooter>
            </BaseNode>
        </NodeStatusIndicator>
    )
}

export const RenderingNode = createAbstractNodeComponent<RenderingNodeData>(
    'RenderingNode',
    RenderingNodeComponent
)

export default RenderingNode
