import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import nunjucks from 'nunjucks'
import {
    AbstractNodeProps,
    createAbstractNodeComponent,
    useAbstractNode,
} from '../../lib/abstractNode'
import { getConfigContent, getConfigType, getConfigTypeId } from '../../lib/configTypes'
import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia } from '../ui/empty'
import {
    BaseNode,
    BaseNodeContent,
    BaseNodeFooter,
    BaseNodeHeaderRow,
} from '../base/BaseNode'
import { getDefaultDataForType, getNextNodeId } from '../../lib/flowUtils'
import { getDefaultStyle } from '../../lib/nodeRegistry'
import { NodeFooterEdgeIndicators } from '../base/NodeFooterEdgeIndicators'
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
import { NodeMenubar } from '../base/NodeMenubar'
import { NodeStatusIndicator } from '../base/NodeStatusIndicator'
import { MenubarItem, MenubarSeparator, MenubarSub, MenubarSubContent, MenubarSubTrigger } from '../ui/menubar'
import { Sparkles, ZoomIn, ZoomOut, RotateCcw, RotateCw } from 'lucide-react'
import { InputHandle } from '../base/NodeHandles'
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'
import { Input } from '../ui/input'
import { Button } from '../ui/button'

export type RenderingNodeData = {
    viewportWidth?: number
    viewportHeight?: number
}

const DEFAULT_VIEWPORT_WIDTH = 1200
const DEFAULT_VIEWPORT_HEIGHT = 800

type Props = AbstractNodeProps<RenderingNodeData>

function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
    const [renderedContent, setRenderedContent] = 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 runIdRef = useRef(0)
    const loadingStartedAtRef = useRef<number | null>(null)
    const minLoadingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
    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 configTypeId = srcNode?.type === 'config' ? getConfigTypeId((srcNode.data ?? undefined) as Record<string, unknown> | undefined) : 'plantuml'
    const sourceContent = srcNode?.type === 'config' ? getConfigContent((srcNode.data ?? undefined) as Record<string, unknown> | undefined) : ''
    const srcData = srcNode?.data ?? {}

    /** 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 RENDER_DEBOUNCE_MS = 250

    useEffect(() => {
        if (!sourceContent && incomingIds.length > 0) {
            setRenderedContent(null)
            setError({ kind: 'no-content', message: 'No content on connected configuration node' })
            setLoading(false)
            return
        }
        if (incomingIds.length === 0) {
            setRenderedContent(null)
            setError(null)
            setLoading(false)
            return
        }

        runIdRef.current += 1
        const thisRunId = runIdRef.current
        let cancelled = false

        const run = async () => {
            loadingStartedAtRef.current = Date.now()
            setLoading(true)
            setError(null)
            try {
                const configIdsUsed = 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 resolveExtendsRef = (name: string): string => {
                    const refName = name.replace(/\.(puml|html)$/, '').trim()
                    return nodes.find((n: any) => n.id === refName || n.data?.title === refName)?.id ?? refName
                }

                /** Collect refs from {% extends %}, {% include %}, {% import %} in template content */
                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 addConfigAndRefs = (templateName: string, visited = new Set<string>()) => {
                    const refId = resolveExtendsRef(templateName)
                    if (visited.has(refId)) throw new Error(`Circular reference detected: ${templateName}`)
                    const node = nodes.find((n: any) => n.id === refId && n.type === 'config')
                    if (!node) throw new Error(`Config not found: ${templateName}`)
                    if (refId !== srcId && !isReachable(refId, id))
                        throw new Error(`Referenced config not connected to renderer: ${templateName}`)
                    visited.add(refId)
                    configIdsUsed.add(refId)
                    const content = getConfigContent((node.data ?? undefined) as Record<string, unknown> | undefined)
                    for (const ref of getTemplateRefs(content)) addConfigAndRefs(ref, visited)
                }

                if (srcId && srcNode?.type === 'config') addConfigAndRefs(srcId)

                // Loader for Nunjucks {% extends %}, {% include %}, {% import %}: resolve template name to config node's plantuml
                const configLoader = {
                    getSource: (name: string): { src: string; path: string } | null => {
                        const refId = resolveExtendsRef(name)
                        const node = nodes.find((n: any) => n.id === refId && n.type === 'config')
                        if (!node) return null
                        if (refId !== srcId && !isReachable(refId, id))
                            throw new Error(`Referenced config not connected to renderer: ${name}`)
                        return {
                            src: getConfigContent((node.data ?? undefined) as Record<string, unknown> | undefined),
                            path: name,
                        }
                    },
                }

                // Context: variables connected to configs, plus variables connected to functions that feed configs (so they can be injected as constants).
                const nunjucksContext = Object.create(null) as Record<string, unknown>
                const setVarInContext = (src: any) => {
                    const v = src.data?.value
                    const str = v === undefined || v === null ? '' : String(v)
                    nunjucksContext[src.id] =
                        v === undefined || v === null ? '' : typeof v === 'boolean' || typeof v === 'number' ? v : str
                }
                for (const e of edges) {
                    if (!configIdsUsed.has(e.target)) continue
                    const src = nodes.find((n: any) => n.id === e.source)
                    if (src?.type === 'variable') setVarInContext(src)
                }
                // All function node ids that feed (directly or transitively) into config — need to register them and collect their variables
                const functionIdsToRegister = new Set<string>()
                let added = true
                while (added) {
                    added = false
                    for (const e of edges) {
                        const src = nodes.find((n: any) => n.id === e.source)
                        if (src?.type !== 'function') continue
                        const targetInScope = configIdsUsed.has(e.target) || functionIdsToRegister.has(e.target)
                        if (!targetInScope) continue
                        if (!functionIdsToRegister.has(src.id)) {
                            functionIdsToRegister.add(src.id)
                            added = true
                        }
                    }
                }
                for (const e of edges) {
                    if (!configIdsUsed.has(e.target) && !functionIdsToRegister.has(e.target)) continue
                    const src = nodes.find((n: any) => n.id === e.source)
                    if (src?.type === 'function') {
                        for (const e2 of edges) {
                            if (e2.target !== src.id) continue
                            const vNode = nodes.find((n: any) => n.id === e2.source)
                            if (vNode?.type === 'variable') setVarInContext(vNode)
                        }
                    }
                }

                const env = new nunjucks.Environment([configLoader], { autoescape: false })

                // Register each connected function node as a Nunjucks custom filter (async so sync and async user code both work).
                // Supports either named params: function(num, x, y, kwargs) { return num + (kwargs.bar || 10); } or legacy: args array.
                const formatFilterResult = (r: unknown): string => {
                    if (r === undefined || r === null) return ''
                    if (typeof r === 'string' || typeof r === 'number' || typeof r === 'boolean') return String(r)
                    return String(r)
                }
                /** Parse function(num, x, y, kwargs) { body } or (num, x, y, kwargs) => body to get param names and inner body. */
                const parseFunctionSignature = (body: string): { paramNames: string[]; innerBody: string } | null => {
                    const withCommentsStripped = body.replace(/^\s*\/\/[^\n]*\n?/gm, '').trim()
                    const trimmed = withCommentsStripped.trim()
                    const fnMatch = trimmed.match(/^function\s*\(([^)]*)\)\s*\{([\s\S]*)\}\s*$/)
                    if (fnMatch) {
                        const paramNames = fnMatch[1].split(',').map((p) => p.trim()).filter(Boolean)
                        return { paramNames, innerBody: fnMatch[2].trim() }
                    }
                    const arrowBlockMatch = trimmed.match(/^\(([^)]*)\)\s*=>\s*\{([\s\S]*)\}\s*$/)
                    if (arrowBlockMatch) {
                        const paramNames = arrowBlockMatch[1].split(',').map((p) => p.trim()).filter(Boolean)
                        return { paramNames, innerBody: arrowBlockMatch[2].trim() }
                    }
                    const arrowExprMatch = trimmed.match(/^\(([^)]*)\)\s*=>\s*(.+)\s*$/s)
                    if (arrowExprMatch) {
                        const paramNames = arrowExprMatch[1].split(',').map((p) => p.trim()).filter(Boolean)
                        return { paramNames, innerBody: 'return ' + arrowExprMatch[2].trim() }
                    }
                    return null
                }
                const isPlainObject = (v: unknown): v is Record<string, unknown> =>
                    typeof v === 'object' && v !== null && !Array.isArray(v)

                // For each registered function: which variable/function node ids are connected to it?
                const functionConnectedVariableIds = Object.create(null) as Record<string, string[]>
                const functionConnectedFunctionIds = Object.create(null) as Record<string, string[]>
                for (const fid of functionIdsToRegister) {
                    for (const e of edges) {
                        if (e.target !== fid) continue
                        const src = nodes.find((n: any) => n.id === e.source)
                        if (src?.type === 'variable') {
                            if (!functionConnectedVariableIds[fid]) functionConnectedVariableIds[fid] = []
                            functionConnectedVariableIds[fid].push(src.id)
                        } else if (src?.type === 'function') {
                            if (!functionConnectedFunctionIds[fid]) functionConnectedFunctionIds[fid] = []
                            functionConnectedFunctionIds[fid].push(src.id)
                        }
                    }
                }

                for (const fid of functionIdsToRegister) {
                    const src = nodes.find((n: any) => n.id === fid)
                    if (!src || src.type !== 'function') continue
                    const body = (src.data as { body?: string } | undefined)?.body ?? 'return args[0];'
                    const parsed = parseFunctionSignature(body)
                    const connectedVarIds = new Set<string>(functionConnectedVariableIds[fid] ?? [])
                    const connectedFuncIds = functionConnectedFunctionIds[fid] ?? []
                    env.addFilter(
                        src.id,
                        (value: unknown, ...args: unknown[]) => {
                            const callback = args[args.length - 1] as (err: Error | null, res: string) => void
                            const raw = [value, ...args.slice(0, -1)]
                            const hasKwargs = raw.length > 0 && isPlainObject(raw[raw.length - 1])
                            const positionals = hasKwargs ? raw.slice(0, -1) : raw
                            const kwargs = hasKwargs ? (raw[raw.length - 1] as Record<string, unknown>) : Object.create(null)

                            // Cache for nested filter results so sync-looking code like `num + fn_003(4)` works:
                            // callable throws Suspend when result isn't ready; we await, cache, then re-run.
                            // Cached values are strings (Nunjucks); coerce to number when numeric so 2 + fn_003(4) => 10 not "28".
                            const nestedCache = new Map<string, string>()
                            const coerceCached = (s: string): string | number => {
                                const n = Number(s)
                                return s.trim() !== '' && !Number.isNaN(n) ? n : s
                            }
                            const makeCallable = (filterId: string) => (input: unknown) => {
                                const key = `${filterId}::${JSON.stringify(input)}`
                                if (nestedCache.has(key)) return coerceCached(nestedCache.get(key)!)
                                const p = new Promise<string>((resolve, reject) => {
                                    env.getFilter(filterId)(input, (err: Error | null, res: string) =>
                                        err ? reject(err) : resolve(res)
                                    )
                                })
                                p.then((res) => nestedCache.set(key, res))
                                const suspend = { __suspend: true as const, promise: p, key }
                                throw suspend
                            }

                            let invoke: () => unknown
                            if (parsed) {
                                const { paramNames, innerBody } = parsed
                                const lastParam = paramNames[paramNames.length - 1]
                                const invocationArgs = paramNames.map((name, i) => {
                                    if (name === lastParam && lastParam === 'kwargs') return kwargs
                                    if (connectedVarIds.has(name) && name in nunjucksContext)
                                        return nunjucksContext[name]
                                    if (connectedFuncIds.includes(name)) return makeCallable(name)
                                    return positionals[i]
                                })
                                const extraVarIds = [...connectedVarIds].filter((vid) => !paramNames.includes(vid))
                                const extraFuncIds = connectedFuncIds.filter((fid2) => !paramNames.includes(fid2))
                                const allParamNames = [...paramNames, ...extraVarIds, ...extraFuncIds]
                                const allArgs = [
                                    ...invocationArgs,
                                    ...extraVarIds.map((vid) => nunjucksContext[vid]),
                                    ...extraFuncIds.map((fid2) => makeCallable(fid2)),
                                ]
                                const fn = new Function(...allParamNames, innerBody)
                                invoke = () => fn(...allArgs)
                            } else {
                                const fn = new Function('args', body)
                                invoke = () => fn(positionals)
                            }

                            const done = (err: Error | null, res: string) => {
                                callback(err, res)
                            }
                            const runInvoke = () => {
                                try {
                                    const result = invoke()
                                    if (result != null && typeof (result as Promise<unknown>).then === 'function') {
                                        (result as Promise<unknown>).then(
                                            (r) => done(null, formatFilterResult(r)),
                                            (err) => done(err instanceof Error ? err : new Error(String(err)), '')
                                        )
                                    } else {
                                        done(null, formatFilterResult(result))
                                    }
                                } catch (e: unknown) {
                                    const s = e as { __suspend?: boolean; promise?: Promise<string>; key?: string }
                                    if (s?.__suspend && s.promise) {
                                        s.promise.then(() => runInvoke(), (err) =>
                                            done(err instanceof Error ? err : new Error(String(err)), '')
                                        )
                                    } else {
                                        done(e instanceof Error ? e : new Error(String(e)), '')
                                    }
                                }
                            }
                            runInvoke()
                        },
                        true
                    )
                }

                env.render(srcId!, nunjucksContext, async (nunjucksErr: Error | null, afterNunjucks: string) => {
                    if (cancelled || thisRunId !== runIdRef.current) return
                    if (nunjucksErr) {
                        setRenderedContent(null)
                        setError({ kind: 'render', message: `Nunjucks: ${nunjucksErr.message}` })
                        setLoading(false)
                        return
                    }

                    const resolvedContent = afterNunjucks
                    const typeRenderer = getConfigType(configTypeId)

                    try {
                        const renderOptions = configTypeId === 'wireframe' ? { width: viewportWidth, height: viewportHeight } : undefined
                        const htmlOrSvg = await typeRenderer.render(resolvedContent, renderOptions)
                        if (cancelled || thisRunId !== runIdRef.current) return
                        setRenderedContent(htmlOrSvg)
                        setError(null)
                    } catch (err: any) {
                        if (cancelled || thisRunId !== runIdRef.current) return
                        const msg = err?.message ?? 'Render error'
                        setRenderedContent(null)
                        setError({ kind: 'render', message: msg })
                    } 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)
                            }
                        }
                    }
                })
            } catch (err: any) {
                if (!cancelled && thisRunId === runIdRef.current) {
                    setRenderedContent(null)
                    setError({ kind: 'render', message: err?.message ?? 'Render error' })
                    setLoading(false)
                }
            }
        }

        const debounceTimer = setTimeout(run, RENDER_DEBOUNCE_MS)
        return () => {
            cancelled = true
            clearTimeout(debounceTimer)
            if (minLoadingTimeoutRef.current != null) {
                clearTimeout(minLoadingTimeoutRef.current)
                minLoadingTimeoutRef.current = null
            }
        }
        // Only re-run when inputs that affect the resolved output change (signatures + source). Debounced to avoid excessive re-renders while typing. retryCount triggers re-run on Retry.
    }, [id, sourceContent, configTypeId, configSignature, edgesSignature, variablesSignature, functionsSignature, viewportWidth, viewportHeight, retryCount])

    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()))

    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])

    return (
        <NodeStatusIndicator status={status} variant="border" 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} />} />

                <BaseNodeContent>
                    <div className="shrink-0 w-full">
                        <NodeMenubar
                            nodeId={id}
                            nodeType="render"
                            nodeMenuExtraContent={
                                <>
                                    {isSvgOutput && (
                                        <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>
                        ) : renderedContent ? (
                            isSvgOutput ? (
                                <div className="rendering-viewport nodrag nopan relative min-h-0 flex-1 w-full min-w-0 overflow-hidden bg-white dark:bg-secondary">
                                    <TransformWrapper
                                        initialScale={1}
                                        minScale={0.2}
                                        maxScale={4}
                                        centerOnInit
                                        onInit={(ref) => ref?.centerView(1, 200, 'easeOut')}
                                        panning={{ disabled: true }}
                                        wheel={{ disabled: true }}
                                        doubleClick={{ disabled: true }}
                                    >
                                        {({ 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">
                                                    <TransformComponent
                                                        wrapperClass="!w-full !h-full"
                                                        contentClass="!w-full !h-full flex items-center justify-center nodrag nopan"
                                                    >
                                                        <div
                                                            className="rendering-diagram flex items-center justify-center min-h-full min-w-full p-4 nodrag nopan"
                                                            dangerouslySetInnerHTML={{ __html: renderedContent }}
                                                        />
                                                    </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">
                        {renderedContent
                            ? `${getConfigType(configTypeId).label} · ${renderedContent.length} chars`
                            : error
                                ? 'Error'
                                : '—'}
                    </NodeFooterEdgeIndicators>
                </BaseNodeFooter>
            </BaseNode>
        </NodeStatusIndicator>
    )
}

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

export default RenderingNode
