import React, { useCallback, useEffect, useMemo, useState } from 'react';
import GreyRowBox from '@/components/elements/GreyRowBox';
import Input, { Textarea } from '@/components/elements/Input';
import Select from '@/components/elements/Select';
import Spinner from '@/components/elements/Spinner';
import { Button } from '@/components/elements/button/index';
import { NbtDocument, NbtNode, NbtTagType, readNbtFile, writeNbtFile } from './api/server/files/betterFilesApi';
import { themeColors } from './colorExtractor';
import { FaChevronDown, FaChevronRight, FaCube, FaSearch } from 'react-icons/fa';

interface NbtEditorProps {
    uuid: string;
    filePath: string;
    readOnly?: boolean;
}

type NbtPath = Array<string | number>;

interface NbtTreeEntry {
    name: string;
    node: NbtNode;
    path: NbtPath;
    depth: number;
}

const containerTypes = new Set<NbtTagType>(['compound', 'list']);
const integerTypes = new Set<NbtTagType>(['byte', 'short', 'int', 'long']);
const decimalTypes = new Set<NbtTagType>(['float', 'double']);
const arrayTypes = new Set<NbtTagType>(['byte_array', 'int_array', 'long_array']);

const cx = (...classes: Array<string | false | null | undefined>): string => classes.filter(Boolean).join(' ');

const viewerPanelStyle: React.CSSProperties = {
    backgroundColor: themeColors.page.background,
    borderColor: themeColors.page.secondaryHover,
};

const viewerHeaderStyle: React.CSSProperties = {
    backgroundColor: themeColors.page.secondary,
    borderColor: themeColors.page.secondaryHover,
};

const viewerBodyStyle: React.CSSProperties = {
    backgroundColor: themeColors.page.background,
};

const cloneDocument = (document: NbtDocument): NbtDocument => JSON.parse(JSON.stringify(document)) as NbtDocument;

const pathKey = (path: NbtPath): string => path.map((part) => String(part)).join('\u001f');

const countChildren = (node: NbtNode): number => {
    if (node.type === 'compound') return Object.keys(node.children || {}).length;
    if (node.type === 'list') return node.items?.length || 0;
    return 0;
};

const tagTypeLabel = (type: NbtTagType): string => type.replace('_', ' ');

const summaryForNode = (node: NbtNode): string => {
    if (node.type === 'compound') return `${countChildren(node)} tag${countChildren(node) === 1 ? '' : 's'}`;
    if (node.type === 'list') return `${countChildren(node)} ${node.element_type || 'tag'} item${countChildren(node) === 1 ? '' : 's'}`;
    if (Array.isArray(node.value)) return `${node.value.length} value${node.value.length === 1 ? '' : 's'}`;
    if (typeof node.value === 'undefined' || node.value === null) return '';
    return String(node.value);
};

const valueToInput = (node: NbtNode): string => {
    if (Array.isArray(node.value)) return node.value.map((value) => String(value)).join(', ');
    if (typeof node.value === 'undefined' || node.value === null) return '';
    return String(node.value);
};

const inputToValue = (node: NbtNode, value: string): unknown => {
    if (arrayTypes.has(node.type)) {
        return value
            .split(/[,\n]/)
            .map((entry) => entry.trim())
            .filter(Boolean);
    }

    if (integerTypes.has(node.type) || decimalTypes.has(node.type)) {
        return value.trim();
    }

    return value;
};

const getNodeChildren = (node: NbtNode): Array<[string, NbtNode, string | number]> => {
    if (node.type === 'compound') {
        return Object.entries(node.children || {})
            .sort(([a], [b]) => a.localeCompare(b))
            .map(([key, child]) => [key, child, key]);
    }

    if (node.type === 'list') {
        return (node.items || []).map((child, index) => [`[${index}]`, child, index]);
    }

    return [];
};

const getNodeAtPath = (node: NbtNode, path: NbtPath): NbtNode | null => {
    if (path.length === 0) return node;

    const [head, ...rest] = path;

    if (node.type === 'compound' && typeof head === 'string') {
        const child = node.children?.[head];
        return child ? getNodeAtPath(child, rest) : null;
    }

    if (node.type === 'list' && typeof head === 'number') {
        const child = node.items?.[head];
        return child ? getNodeAtPath(child, rest) : null;
    }

    return null;
};

const flattenNodes = (name: string, node: NbtNode, path: NbtPath, depth: number, collapsed: Set<string>): NbtTreeEntry[] => {
    const entries: NbtTreeEntry[] = [{ name, node, path, depth }];

    if (containerTypes.has(node.type) && !collapsed.has(pathKey(path))) {
        getNodeChildren(node).forEach(([childName, child, key]) => {
            entries.push(...flattenNodes(childName, child, [...path, key], depth + 1, collapsed));
        });
    }

    return entries;
};

const displayPath = (rootName: string, path: NbtPath): string => {
    if (path.length === 0) return rootName || 'root';

    return [rootName || 'root', ...path.map((part) => (typeof part === 'number' ? `[${part}]` : part))].join(' / ');
};

const updateNodeValue = (node: NbtNode, path: NbtPath, value: unknown): NbtNode => {
    if (path.length === 0) {
        return { ...node, value };
    }

    const [head, ...rest] = path;

    if (node.type === 'compound' && typeof head === 'string') {
        const children = { ...(node.children || {}) };
        const child = children[head];
        if (!child) return node;

        children[head] = updateNodeValue(child, rest, value);
        return { ...node, children };
    }

    if (node.type === 'list' && typeof head === 'number') {
        const items = [...(node.items || [])];
        const child = items[head];
        if (!child) return node;

        items[head] = updateNodeValue(child, rest, value);
        return { ...node, items };
    }

    return node;
};

const NbtTypePill: React.FC<{ type: NbtTagType }> = ({ type }) => (
    <span className={'rounded border border-neutral-700 px-1.5 py-0.5 font-mono text-[10px] uppercase text-neutral-500'}>
        {tagTypeLabel(type)}
    </span>
);

const NbtTreeList: React.FC<{
    entries: NbtTreeEntry[];
    selectedKey: string;
    collapsed: Set<string>;
    searching: boolean;
    onSelect: (path: NbtPath) => void;
    onToggle: (path: NbtPath) => void;
}> = ({ entries, selectedKey, collapsed, searching, onSelect, onToggle }) => (
    <div className={'min-h-0 flex-1 overflow-auto py-2'}>
        {entries.map((entry) => {
            const key = pathKey(entry.path);
            const collapsible = containerTypes.has(entry.node.type);
            const selected = key === selectedKey;
            const paddingLeft = Math.min(entry.depth, 10) * 14 + 10;

            return (
                <div
                    key={key || 'root'}
                    className={cx(
                        'group grid min-h-[34px] grid-cols-[24px_minmax(0,1fr)_auto] items-center gap-1 border-b border-neutral-700/50 pr-3 text-xs transition-colors',
                        selected ? 'bg-neutral-700 text-neutral-100' : 'text-neutral-300 hover:bg-neutral-700/60 hover:text-neutral-100'
                    )}
                    style={{
                        backgroundColor: selected ? themeColors.page.secondaryHover : undefined,
                        borderColor: themeColors.page.secondaryHover,
                        paddingLeft,
                    }}
                >
                    <button
                        type={'button'}
                        disabled={!collapsible || searching}
                        className={'flex h-6 w-6 items-center justify-center rounded text-neutral-500 hover:bg-neutral-600 hover:text-neutral-100 disabled:cursor-default disabled:hover:bg-transparent disabled:hover:text-neutral-500'}
                        onClick={() => collapsible && onToggle(entry.path)}
                    >
                        {collapsible ? (
                            searching || !collapsed.has(key) ? (
                                <FaChevronDown className={'h-2.5 w-2.5'} />
                            ) : (
                                <FaChevronRight className={'h-2.5 w-2.5'} />
                            )
                        ) : null}
                    </button>
                    <button
                        type={'button'}
                        className={'flex min-w-0 flex-col py-1 text-left'}
                        onClick={() => onSelect(entry.path)}
                    >
                        <span className={'truncate font-medium'} title={entry.name}>
                            {entry.name}
                        </span>
                        <span className={'truncate font-mono text-[10px] text-neutral-500'} title={summaryForNode(entry.node)}>
                            {summaryForNode(entry.node) || tagTypeLabel(entry.node.type)}
                        </span>
                    </button>
                    <NbtTypePill type={entry.node.type} />
                </div>
            );
        })}
    </div>
);

const NbtInspector: React.FC<{
    rootName: string;
    selectedName: string;
    selectedPath: NbtPath;
    node: NbtNode;
    readOnly?: boolean;
    onChange: (path: NbtPath, node: NbtNode, value: string) => void;
    onSelect: (path: NbtPath) => void;
}> = ({ rootName, selectedName, selectedPath, node, readOnly = false, onChange, onSelect }) => {
    const editable = !containerTypes.has(node.type);
    const childEntries = getNodeChildren(node);
    const scalarValue = valueToInput(node);
    const useTextarea = arrayTypes.has(node.type) || scalarValue.length > 100 || scalarValue.includes('\n');

    return (
        <section className={'flex min-h-0 flex-1 flex-col'} style={viewerBodyStyle}>
            <div className={'border-b px-5 py-4'} style={viewerHeaderStyle}>
                <div className={'flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between'}>
                    <div className={'min-w-0'}>
                        <div className={'flex flex-wrap items-center gap-2'}>
                            <p className={'break-all text-sm font-medium text-neutral-100'}>{selectedName}</p>
                            <NbtTypePill type={node.type} />
                        </div>
                        <p className={'mt-1 break-all font-mono text-xs text-neutral-500'}>{displayPath(rootName, selectedPath)}</p>
                    </div>
                    <div className={'flex flex-wrap gap-2 text-xs'}>
                        <span className={'rounded border border-neutral-700 px-3 py-1.5 font-mono text-neutral-300'}>
                            {summaryForNode(node) || 'empty'}
                        </span>
                        {node.type === 'list' && node.element_type && (
                            <span className={'rounded border border-neutral-700 px-3 py-1.5 font-mono uppercase text-neutral-500'}>
                                {tagTypeLabel(node.element_type)}
                            </span>
                        )}
                    </div>
                </div>
            </div>

            {editable ? (
                <div className={'min-h-0 flex-1 overflow-auto p-5'}>
                    <div className={'max-w-4xl'}>
                        <label className={'block'}>
                            <span className={'mb-2 block text-[10px] uppercase tracking-wider text-neutral-500'}>Value</span>
                            {useTextarea ? (
                                <Textarea
                                    rows={arrayTypes.has(node.type) ? 10 : 7}
                                    spellCheck={node.type === 'string'}
                                    value={scalarValue}
                                    disabled={readOnly}
                                    className={'font-mono'}
                                    onChange={(event) => onChange(selectedPath, node, event.currentTarget.value)}
                                />
                            ) : (
                                <Input
                                    spellCheck={node.type === 'string'}
                                    value={scalarValue}
                                    disabled={readOnly}
                                    className={'font-mono'}
                                    onChange={(event) => onChange(selectedPath, node, event.currentTarget.value)}
                                />
                            )}
                        </label>
                    </div>
                </div>
            ) : (
                <div className={'min-h-0 flex-1 overflow-auto p-5'}>
                    {childEntries.length === 0 ? (
                        <div className={'text-sm text-neutral-500'}>No child tags.</div>
                    ) : (
                        <div className={'grid gap-2 sm:grid-cols-2 xl:grid-cols-3'}>
                            {childEntries.slice(0, 96).map(([childName, child, key]) => {
                                const childPath = [...selectedPath, key];

                                return (
                                    <button
                                        key={`${pathKey(childPath)}:${childName}`}
                                        type={'button'}
                                        className={'min-w-0 rounded border px-3 py-2 text-left text-xs text-neutral-300 transition-colors hover:text-neutral-100'}
                                        style={viewerPanelStyle}
                                        onClick={() => onSelect(childPath)}
                                    >
                                        <div className={'flex min-w-0 items-center justify-between gap-2'}>
                                            <span className={'truncate font-medium'} title={childName}>
                                                {childName}
                                            </span>
                                            <NbtTypePill type={child.type} />
                                        </div>
                                        <p className={'mt-1 truncate font-mono text-[10px] text-neutral-500'} title={summaryForNode(child)}>
                                            {summaryForNode(child) || tagTypeLabel(child.type)}
                                        </p>
                                    </button>
                                );
                            })}
                        </div>
                    )}
                </div>
            )}
        </section>
    );
};

const NbtEditor: React.FC<NbtEditorProps> = ({ uuid, filePath, readOnly = false }) => {
    const [document, setDocument] = useState<NbtDocument | null>(null);
    const [originalDocument, setOriginalDocument] = useState<NbtDocument | null>(null);
    const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
    const [loading, setLoading] = useState(false);
    const [saving, setSaving] = useState(false);
    const [dirty, setDirty] = useState(false);
    const [error, setError] = useState<string | null>(null);
    const [saved, setSaved] = useState(false);
    const [selectedPath, setSelectedPath] = useState<NbtPath>([]);
    const [query, setQuery] = useState('');

    useEffect(() => {
        let mounted = true;

        setLoading(true);
        setError(null);
        setSaved(false);
        setDirty(false);
        setDocument(null);
        setOriginalDocument(null);
        setCollapsed(new Set());
        setSelectedPath([]);
        setQuery('');

        readNbtFile(uuid, filePath)
            .then((response) => {
                if (!mounted) return;
                setDocument(cloneDocument(response.document));
                setOriginalDocument(cloneDocument(response.document));
            })
            .catch((error) => {
                if (!mounted) return;
                setError(error?.response?.data?.error || error?.message || 'Could not read NBT file.');
            })
            .finally(() => {
                if (mounted) setLoading(false);
            });

        return () => {
            mounted = false;
        };
    }, [uuid, filePath]);

    const handleToggle = useCallback((nodePath: NbtPath) => {
        const key = pathKey(nodePath);
        setCollapsed((previous) => {
            const next = new Set(previous);
            if (next.has(key)) {
                next.delete(key);
            } else {
                next.add(key);
            }
            return next;
        });
    }, []);

    const handleValueChange = useCallback((nodePath: NbtPath, node: NbtNode, value: string) => {
        if (readOnly) return;
        setDocument((current) => {
            if (!current) return current;
            return {
                ...current,
                root: {
                    ...current.root,
                    value: updateNodeValue(current.root.value, nodePath, inputToValue(node, value)),
                },
            };
        });
        setSaved(false);
        setDirty(true);
    }, [readOnly]);

    const handleRootNameChange = (value: string) => {
        if (readOnly) return;
        setDocument((current) => (current ? { ...current, root: { ...current.root, name: value } } : current));
        setSaved(false);
        setDirty(true);
    };

    const handleCompressionChange = (value: NbtDocument['compression']) => {
        if (readOnly) return;
        setDocument((current) => (current ? { ...current, compression: value } : current));
        setSaved(false);
        setDirty(true);
    };

    const handleReset = () => {
        if (!originalDocument) return;
        setDocument(cloneDocument(originalDocument));
        setCollapsed(new Set());
        setSelectedPath([]);
        setDirty(false);
        setSaved(false);
        setError(null);
    };

    const handleSave = useCallback(() => {
        if (readOnly) return;
        if (!document) return;

        setSaving(true);
        setError(null);
        setSaved(false);
        writeNbtFile(uuid, filePath, document)
            .then(() => {
                setOriginalDocument(cloneDocument(document));
                setDirty(false);
                setSaved(true);
            })
            .catch((error) => setError(error?.response?.data?.error || error?.message || 'Could not save NBT file.'))
            .finally(() => setSaving(false));
    }, [document, filePath, readOnly, uuid]);

    const rootName = document?.root?.name || 'root';
    const fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
    const searching = query.trim().length > 0;
    const treeEntries = useMemo(
        () => (document ? flattenNodes(document.root.name || 'root', document.root.value, [], 0, collapsed) : []),
        [collapsed, document]
    );
    const visibleTreeEntries = useMemo(() => {
        if (!document) return [];
        if (!searching) return treeEntries;

        const needle = query.trim().toLowerCase();
        return flattenNodes(document.root.name || 'root', document.root.value, [], 0, new Set())
            .filter((entry) => {
                const label = `${entry.name} ${entry.node.type} ${displayPath(rootName, entry.path)} ${summaryForNode(entry.node)}`;
                return label.toLowerCase().includes(needle);
            });
    }, [document, query, rootName, searching, treeEntries]);
    const selectedNode = useMemo(
        () => (document ? getNodeAtPath(document.root.value, selectedPath) || document.root.value : null),
        [document, selectedPath]
    );
    const selectedName = useMemo(() => {
        if (selectedPath.length === 0) return rootName;

        const last = selectedPath[selectedPath.length - 1];
        return typeof last === 'number' ? `[${last}]` : last;
    }, [rootName, selectedPath]);

    return (
        <GreyRowBox
            className={'flex-1 flex flex-col !items-stretch !p-0 min-h-0 overflow-hidden w-full max-w-full'}
            $hoverable={false}
            style={{
                alignItems: 'stretch',
                alignSelf: 'stretch',
                backgroundColor: themeColors.page.background,
                borderColor: themeColors.page.secondaryHover,
                display: 'flex',
                flex: '1 1 0%',
                flexDirection: 'column',
                height: '100%',
                maxWidth: '100%',
                minHeight: 0,
                minWidth: 0,
                width: '100%',
            }}
        >
            <div className={'flex flex-col gap-3 border-b px-4 py-3 lg:flex-row lg:items-center lg:justify-between'} style={viewerHeaderStyle}>
                <div className={'flex min-w-0 items-center'}>
                    <FaCube className={'mr-3 h-4 w-4 flex-shrink-0 text-blue-400'} />
                    <div className={'min-w-0'}>
                        <p className={'truncate text-sm font-medium text-neutral-100'}>{fileName}</p>
                        <p className={'text-xs text-neutral-500'}>
                            {document ? `${treeEntries.length} tag${treeEntries.length === 1 ? '' : 's'} / ${document.compression} NBT` : 'NBT data'}
                        </p>
                    </div>
                </div>
                <div className={'grid w-full grid-cols-2 gap-2 sm:flex sm:w-auto sm:flex-wrap sm:items-center lg:ml-4 lg:flex-shrink-0'}>
                    {saved && <span className={'text-xs text-green-400'}>Saved</span>}
                    {dirty && !saved && <span className={'text-xs text-yellow-400'}>Modified</span>}
                    <Button.Text className={'w-full justify-center sm:w-auto'} size={Button.Sizes.Small} disabled={loading || saving || !dirty} onClick={handleReset}>
                        Reset
                    </Button.Text>
                    <Button className={'w-full justify-center sm:w-auto'} size={Button.Sizes.Small} onClick={handleSave} disabled={readOnly || loading || saving || !document || !dirty}>
                        {saving ? 'Saving...' : 'Save NBT'}
                    </Button>
                </div>
            </div>

            {document && (
                <div className={'flex flex-col gap-3 border-b px-4 py-3 lg:flex-row lg:items-center lg:justify-between'} style={viewerPanelStyle}>
                    <div className={'grid w-full min-w-0 gap-3 sm:grid-cols-[minmax(180px,280px)_140px] lg:w-auto'}>
                        <label className={'block'}>
                            <span className={'mb-1 block text-[10px] uppercase tracking-wider text-neutral-500'}>Root</span>
                            <Input
                                className={'!w-full !px-3 !py-2 !text-xs'}
                                value={document.root.name}
                                disabled={readOnly}
                                onChange={(event) => handleRootNameChange(event.currentTarget.value)}
                                placeholder={'root'}
                            />
                        </label>
                        <label className={'block'}>
                            <span className={'mb-1 block text-[10px] uppercase tracking-wider text-neutral-500'}>Compression</span>
                            <Select
                                className={'!w-full !px-3 !py-2 !text-xs'}
                                value={document.compression}
                                disabled={readOnly}
                                onChange={(event) => handleCompressionChange(event.currentTarget.value as NbtDocument['compression'])}
                            >
                                <option value={'gzip'}>gzip</option>
                                <option value={'zlib'}>zlib</option>
                                <option value={'none'}>none</option>
                            </Select>
                        </label>
                    </div>
                    <div className={'relative w-full min-w-0 lg:w-72'}>
                        <FaSearch className={'pointer-events-none absolute left-3 top-1/2 h-3 w-3 -translate-y-1/2 text-neutral-500'} />
                        <Input
                            className={'!w-full !pl-8 !pr-3 !py-2 !text-xs'}
                            value={query}
                            onChange={(event) => setQuery(event.currentTarget.value)}
                            placeholder={'Filter tags'}
                        />
                    </div>
                </div>
            )}

            {error && <div className={'border-b border-red-500/30 bg-red-500/10 px-4 py-2 text-xs text-red-300'}>{error}</div>}
            <div className={'flex-1 min-h-0'} style={viewerBodyStyle}>
                {loading ? (
                    <div className={'flex h-full items-center justify-center'}>
                        <Spinner size={'large'} />
                    </div>
                ) : error && !document ? (
                    <div className={'p-4 text-sm text-red-300'}>{error}</div>
                ) : document && selectedNode ? (
                    <div className={'grid h-full min-h-0 grid-cols-1 lg:grid-cols-[minmax(320px,400px)_1fr]'}>
                        <aside className={'flex max-h-80 min-h-0 flex-col border-b lg:max-h-none lg:border-b-0 lg:border-r'} style={{ borderColor: themeColors.page.secondaryHover }}>
                            <div className={'flex items-center justify-between border-b px-4 py-3'} style={viewerHeaderStyle}>
                                <div className={'min-w-0'}>
                                    <p className={'text-[10px] uppercase tracking-wider text-neutral-500'}>Tags</p>
                                    <p className={'mt-1 text-xs text-neutral-500'}>
                                        {visibleTreeEntries.length} shown / {treeEntries.length} total
                                    </p>
                                </div>
                            </div>
                            {visibleTreeEntries.length === 0 ? (
                                <div className={'p-4 text-sm text-neutral-500'}>No matching tags.</div>
                            ) : (
                                <NbtTreeList
                                    entries={visibleTreeEntries}
                                    selectedKey={pathKey(selectedPath)}
                                    collapsed={searching ? new Set() : collapsed}
                                    searching={searching}
                                    onSelect={setSelectedPath}
                                    onToggle={handleToggle}
                                />
                            )}
                        </aside>
                        <NbtInspector
                            rootName={rootName}
                            selectedName={selectedName}
                            selectedPath={selectedPath}
                            node={selectedNode}
                            readOnly={readOnly}
                            onChange={handleValueChange}
                            onSelect={setSelectedPath}
                        />
                    </div>
                ) : (
                    <div className={'flex h-full items-center justify-center text-sm text-neutral-500'}>No NBT document loaded.</div>
                )}
            </div>
        </GreyRowBox>
    );
};

export default NbtEditor;
