import React, { useCallback, useContext, useMemo, useRef } from 'react'
import { autocompletion } from '@codemirror/autocomplete'
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { markdown } from '@codemirror/lang-markdown'
import {
    AbstractNodeProps,
    createAbstractNodeComponent,
    useAbstractNode,
    type FlowNode,
} from '@/lib/graph/abstractNode'
import { useResizeHeight } from '@/hooks/useResizeHeight'
import { nunjucksCompletionSource } from '@/lib/nunjucksAutocomplete'
import { plantumlLanguage } from '@/lib/plantumlLanguage'
import { useTheme } from '@/lib/themeContext'
import {
    getConfigTypes,
    getConfigContent,
    getConfigType,
    getConfigTypeId,
    isGroup,
    type ConfigTypeId,
    type InsertBlockGroup,
    type InsertBlock,
} from '@/lib/graph/configTypes'
import {
    BaseNode,
    BaseNodeContent,
    BaseNodeFooter,
    BaseNodeHeaderRow,
} from '@/components/graph/BaseNode'
import { Code2, Database, ScrollText, Variable } from 'lucide-react'
import {
    MenubarItem,
    MenubarSeparator,
    MenubarShortcut,
    MenubarSub,
    MenubarSubContent,
    MenubarSubTrigger,
} from '@/components/ui/menubar'
import { Kbd } from '@/components/ui/kbd'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { InputHandle, OutputHandle } from '@/components/graph/NodeHandles'
import { NodeFooterEdgeIndicators } from '@/components/graph/NodeFooterEdgeIndicators'
import { NodeHeaderTitle } from '@/components/graph/NodeHeaderTitle'
import { NodeMenubar } from '@/components/graph/NodeMenubar'
import { FlowUIContext } from '@/lib/graph/flowContext'
import { getNodeType } from '@/lib/graph/nodeRegistry'

export type ConfigNodeData = { configType?: ConfigTypeId; content?: string; title?: string; /** @deprecated use content */ plantuml?: string }

type Props = AbstractNodeProps<ConfigNodeData>

function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
    const flowUIContext = useContext(FlowUIContext)
    const setFullscreenNodeId = flowUIContext?.setFullscreenNodeId
    const supportsFullscreen = getNodeType('config')?.supportsFullscreen
    const configTypeId = getConfigTypeId(data ?? {})
    const configType = getConfigType(configTypeId)
    const content = getConfigContent(data ?? {})
    const { theme } = useTheme()
    const { nodes, sourceIds, updateData } = useAbstractNode<ConfigNodeData>(id, data ?? {})
    const editorRef = useRef<unknown>(null)

    const connectedConfigNodes = useMemo(
        () => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'config'),
        [nodes, sourceIds]
    )
    const connectedVariableNodes = useMemo(
        () => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'variable'),
        [nodes, sourceIds]
    )
    const connectedFunctionNodes = useMemo(
        () => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'function'),
        [nodes, sourceIds]
    )
    const connectedDataNodes = useMemo(
        () => (nodes as FlowNode[]).filter((n) => sourceIds.includes(n.id) && n.type === 'data'),
        [nodes, sourceIds]
    )
    const hasDependencies = connectedConfigNodes.length > 0 || connectedVariableNodes.length > 0 || connectedFunctionNodes.length > 0 || connectedDataNodes.length > 0

    const onChange = useCallback(
        (val: string) => updateData({ content: val, configType: configTypeId }),
        [updateData, configTypeId]
    )

    const setConfigType = useCallback(
        (newTypeId: ConfigTypeId) => {
            if (newTypeId === configTypeId) return
            updateData({
                configType: newTypeId,
                content: getConfigContent({ ...data, configType: newTypeId }) ?? '',
            })
        },
        [configTypeId, data, updateData]
    )

    const insertAt = useCallback(
        (insertText: string, mode: 'prepend' | 'append' | 'cursor') => {
            const ref = editorRef.current as { view: { state: { doc: { length: number; toString(): string }; selection: { main: { from: number } } }; dispatch: (arg: { changes: { from: number; to: number; insert: string } }) => void } } | null
            if (ref?.view) {
                const view = ref.view
                const doc = view.state.doc
                const len = doc.length
                let from: number
                if (mode === 'prepend') {
                    from = 0
                } else if (mode === 'append') {
                    from = len
                } else {
                    const main = view.state.selection.main
                    from = main.from
                }
                view.dispatch({ changes: { from, to: from, insert: insertText } })
                const newVal = view.state.doc.toString()
                onChange(newVal)
                return
            }
            if (mode === 'prepend') {
                onChange(insertText + content)
            } else {
                onChange(content + insertText)
            }
        },
        [onChange, content]
    )

    const insertExtendsFromNode = useCallback(
        (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
            insertAt(`{% extends "${sourceNode.id}" %}\n`, mode)
        },
        [insertAt]
    )

    const insertIncludeFromNode = useCallback(
        (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
            insertAt(`{% include "${sourceNode.id}" %}\n`, mode)
        },
        [insertAt]
    )

    const insertImportFromNode = useCallback(
        (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
            insertAt(`{% import "${sourceNode.id}" as ${sourceNode.id} %}\n`, mode)
        },
        [insertAt]
    )

    const insertVariableReference = useCallback(
        (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
            insertAt(`{{ ${sourceNode.id} }}`, mode)
        },
        [insertAt]
    )

    const insertFunctionCall = useCallback(
        (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
            insertAt(`{{ '' | ${sourceNode.id} }}`, mode)
        },
        [insertAt]
    )

    const insertDataReference = useCallback(
        (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
            insertAt(`{% for row in ${sourceNode.id} %}\n  \n{% endfor %}`, mode)
        },
        [insertAt]
    )

    const variableIds = useMemo(() => connectedVariableNodes.map((n: any) => n.id), [connectedVariableNodes])
    const functionIds = useMemo(() => connectedFunctionNodes.map((n: any) => n.id), [connectedFunctionNodes])
    const configTitles = useMemo(
        () => connectedConfigNodes.map((n: any) => n.data?.title ?? n.id),
        [connectedConfigNodes],
    )
    const dataIds = useMemo(() => connectedDataNodes.map((n: any) => n.id), [connectedDataNodes])
    const extensions = useMemo(() => {
        const lang =
            configTypeId === 'wireframe'
                ? javascript()
                : configType.language === 'plantuml'
                    ? plantumlLanguage.extension
                    : markdown()
        return [
            lang,
            autocompletion({
                override: [nunjucksCompletionSource(variableIds, configTitles, functionIds, dataIds)],
                activateOnTyping: true,
            }),
        ]
    }, [configTypeId, configType.language, variableIds, functionIds, configTitles, dataIds])
    const [editorHeight, editorContainerRef] = useResizeHeight(180)

    const insertBlocksContent = useMemo(() => {
        const templatingGroup = configType.insertBlocks.find(
            (b): b is InsertBlockGroup => isGroup(b) && b.label === 'Templating'
        )
        const typeBlocks = configType.insertBlocks.filter((b) => !(isGroup(b) && b.label === 'Templating'))
        const insertShortcut = <MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
        const typeItems = typeBlocks.flatMap((block) =>
            isGroup(block)
                ? block.items.map(({ label, snippet }: InsertBlock) => (
                    <MenubarItem
                        key={label}
                        className="text-xs flex items-center group"
                        onClick={() => insertAt(snippet, 'cursor')}
                    >
                        {label}
                        {insertShortcut}
                    </MenubarItem>
                ))
                : [
                    <MenubarItem
                        key={block.label}
                        className="text-xs flex items-center group"
                        onClick={() => insertAt(block.snippet, 'cursor')}
                    >
                        {block.label}
                        {insertShortcut}
                    </MenubarItem>,
                ]
        )
        const templatingItems =
            templatingGroup?.items.map(({ label, snippet }: InsertBlock) => (
                <MenubarItem
                    key={label}
                    className="text-xs flex items-center group"
                    onClick={() => insertAt(snippet, 'cursor')}
                >
                    {label}
                    <MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
                </MenubarItem>
            )) ?? []
        return (
            <>
                {typeItems}
                {templatingItems.length > 0 && (
                    <>
                        <MenubarSeparator />
                        {templatingItems}
                    </>
                )}
            </>
        )
    }, [configType.insertBlocks, insertAt])

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

    return (
        <BaseNode className="min-w-80 min-h-[280px]" dimensions={dimensions} resizable nodeId={id} selected={selected} handles={<><InputHandle id="ain" nodeId={id} /><OutputHandle id="out" /></>}>
            <BaseNodeHeaderRow
                icon={<ScrollText className="size-4" />}
                title={<NodeHeaderTitle nodeId={id} displayTitle={`${id}`} />}
                right={
                    <Select value={configTypeId} onValueChange={(v) => setConfigType(v as ConfigTypeId)}>
                        <SelectTrigger className="h-7 w-[7rem] text-xs font-normal">
                            <SelectValue />
                        </SelectTrigger>
                        <SelectContent>
                            {getConfigTypes().map((t) => (
                                <SelectItem key={t.id} value={t.id} className="text-xs">
                                    {t.label}
                                </SelectItem>
                            ))}
                        </SelectContent>
                    </Select>
                }
                onHeaderDoubleClick={supportsFullscreen && setFullscreenNodeId ? () => setFullscreenNodeId(id) : undefined}
            />

            <BaseNodeContent>
                <div className="shrink-0 w-full">
                    <NodeMenubar
                        nodeId={id}
                        nodeType="config"
                        inputsMenuContent={
                            hasDependencies ? (
                                <>
                                    {connectedConfigNodes.map((n: any) => (
                                        <MenubarSub key={`${n.id}`}>
                                            <MenubarSubTrigger className="text-xs flex items-center gap-2">
                                                <ScrollText className="size-3.5 shrink-0" />
                                                {n.data?.title ?? n.id}
                                            </MenubarSubTrigger>
                                            <MenubarSubContent>
                                                <MenubarItem
                                                    className="text-xs flex items-center group"
                                                    onClick={() => insertExtendsFromNode(n, 'cursor')}
                                                >
                                                    Extend
                                                    <MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
                                                </MenubarItem>
                                                <MenubarItem
                                                    className="text-xs flex items-center group"
                                                    onClick={() => insertIncludeFromNode(n, 'cursor')}
                                                >
                                                    Include
                                                    <MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
                                                </MenubarItem>
                                                <MenubarItem
                                                    className="text-xs flex items-center group"
                                                    onClick={() => insertImportFromNode(n, 'cursor')}
                                                >
                                                    Import
                                                    <MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
                                                </MenubarItem>
                                            </MenubarSubContent>
                                        </MenubarSub>
                                    ))}
                                    {connectedVariableNodes.map((n: any) => (
                                        <MenubarItem
                                            key={n.id}
                                            className="text-xs flex items-center gap-2 group"
                                            onClick={() => insertVariableReference(n, 'cursor')}
                                        >
                                            <Variable className="size-3.5 shrink-0" />
                                            {n.id}
                                            <MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
                                        </MenubarItem>
                                    ))}
                                    {connectedFunctionNodes.map((n: any) => (
                                        <MenubarItem
                                            key={n.id}
                                            className="text-xs flex items-center gap-2 group"
                                            onClick={() => insertFunctionCall(n, 'cursor')}
                                        >
                                            <Code2 className="size-3.5 shrink-0" />
                                            {n.id}
                                            <MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
                                        </MenubarItem>
                                    ))}
                                    {connectedDataNodes.map((n: any) => (
                                        <MenubarItem
                                            key={n.id}
                                            className="text-xs flex items-center gap-2 group"
                                            onClick={() => insertDataReference(n, 'cursor')}
                                        >
                                            <Database className="size-3.5 shrink-0" />
                                            {n.id}
                                            <MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
                                        </MenubarItem>
                                    ))}
                                </>
                            ) : (
                                <span className="text-xs text-muted-foreground px-2 py-1">Connect nodes to insert references</span>
                            )
                        }
                        insertMenuLabel="Blocks"
                        insertContentDirect
                        insertTagsContent={insertBlocksContent}
                    />
                </div>

                <div ref={editorContainerRef as React.RefObject<HTMLDivElement>} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input">
                    <CodeMirror
                        // @ts-expect-error ref is { view, state, editor }; package ref type not in our node_modules
                        ref={editorRef}
                        value={content}
                        height={`${editorHeight}px`}
                        theme={theme}
                        extensions={extensions}
                        onChange={onChange}
                        basicSetup={{ lineNumbers: true, foldGutter: false }}
                        className="text-sm [&_.cm-editor]:outline-none [&_.cm-editor]:cursor-text [&_.cm-gutters]:border-0"
                    />
                </div>
            </BaseNodeContent>

            <BaseNodeFooter>
                <NodeFooterEdgeIndicators nodeId={id} nodeType="config">
                    {configType.label} · {content ? `${content.length} chars` : 'none'}
                </NodeFooterEdgeIndicators>
            </BaseNodeFooter>
        </BaseNode>
    )
}

export const ConfigNode = createAbstractNodeComponent<ConfigNodeData>(
    'ConfigNode',
    ConfigNodeComponent
)

export default ConfigNode
