import dagre from '@dagrejs/dagre';
import {
    Background,
    Controls,
    MiniMap,
    ReactFlow,
    ReactFlowProvider,
    useEdgesState,
    useNodesState,
    useReactFlow,
} from '@xyflow/react';
import type { Edge } from '@xyflow/react';
import { toPng } from 'html-to-image';
import {
    forwardRef,
    useCallback,
    useEffect,
    useImperativeHandle,
    useMemo,
    useState,
    useSyncExternalStore,
} from 'react';
import type { Ref } from 'react';
import '@xyflow/react/dist/style.css';
import { OrgChartCollapseContext } from './org-chart-collapse-context';
import { OrganizationChartNode } from './organization-chart-node';
import type { OrgFlowNode, OrgNodeData } from './organization-chart-node';

const NODE_WIDTH = 224;
const NODE_HEIGHT = 112;

function subscribe() {
    return () => {};
}

function useIsClient(): boolean {
    return useSyncExternalStore(
        subscribe,
        () => true,
        () => false,
    );
}

export type ChartNode = Omit<
    OrgNodeData,
    'hasChildren' | 'collapsed'
>;

export type OrganizationChartCanvasHandle = {
    fitView: () => void;
    zoomIn: () => void;
    zoomOut: () => void;
    expandAll: () => void;
    collapseAll: () => void;
    exportPng: () => Promise<void>;
};

type Props = {
    nodes: ChartNode[];
};

const nodeTypes = { org: OrganizationChartNode };

function layoutElements(
    nodes: OrgFlowNode[],
    edges: Edge[],
): { nodes: OrgFlowNode[]; edges: Edge[] } {
    const graph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
    graph.setGraph({
        rankdir: 'TB',
        nodesep: 48,
        ranksep: 72,
        marginx: 24,
        marginy: 24,
    });

    for (const node of nodes) {
        graph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
    }

    for (const edge of edges) {
        graph.setEdge(edge.source, edge.target);
    }

    dagre.layout(graph);

    return {
        nodes: nodes.map((node) => {
            const position = graph.node(node.id);

            return {
                ...node,
                position: {
                    x: position.x - NODE_WIDTH / 2,
                    y: position.y - NODE_HEIGHT / 2,
                },
            };
        }),
        edges,
    };
}

function childrenMap(nodes: ChartNode[]): Map<string, string[]> {
    const map = new Map<string, string[]>();

    for (const node of nodes) {
        if (!node.parent_id) {
            continue;
        }

        const list = map.get(node.parent_id) ?? [];
        list.push(node.id);
        map.set(node.parent_id, list);
    }

    return map;
}

function visibleNodeIds(
    nodes: ChartNode[],
    collapsedIds: Set<string>,
): Set<string> {
    const byParent = childrenMap(nodes);
    const roots = nodes.filter((node) => node.parent_id === null);
    const visible = new Set<string>();

    const visit = (id: string) => {
        visible.add(id);

        if (collapsedIds.has(id)) {
            return;
        }

        for (const childId of byParent.get(id) ?? []) {
            visit(childId);
        }
    };

    for (const root of roots) {
        visit(root.id);
    }

    return visible;
}

function OrganizationChartCanvasInner(
    { nodes: sourceNodes }: Props,
    ref: Ref<OrganizationChartCanvasHandle>,
) {
    const { fitView, zoomIn, zoomOut, getViewport } = useReactFlow();
    const byParent = useMemo(() => childrenMap(sourceNodes), [sourceNodes]);
    const parentIdsWithChildren = useMemo(
        () => new Set(byParent.keys()),
        [byParent],
    );

    const [collapsedIds, setCollapsedIds] = useState<Set<string>>(
        () => new Set(),
    );

    const toggleCollapse = useCallback((id: string) => {
        setCollapsedIds((prev) => {
            const next = new Set(prev);

            if (next.has(id)) {
                next.delete(id);
            } else {
                next.add(id);
            }

            return next;
        });
    }, []);

    const expandAll = useCallback(() => {
        setCollapsedIds(new Set());
    }, []);

    const collapseAll = useCallback(() => {
        setCollapsedIds(new Set(parentIdsWithChildren));
    }, [parentIdsWithChildren]);

    const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo(() => {
        const visible = visibleNodeIds(sourceNodes, collapsedIds);
        const nextNodes: OrgFlowNode[] = sourceNodes
            .filter((node) => visible.has(node.id))
            .map((node) => ({
                id: node.id,
                type: 'org' as const,
                position: { x: 0, y: 0 },
                data: {
                    ...node,
                    hasChildren: parentIdsWithChildren.has(node.id),
                    collapsed: collapsedIds.has(node.id),
                },
                draggable: false,
                connectable: false,
                // React Flow sets `pointer-events: none` on nodes that are
                // neither draggable nor selectable and have no onClick/hover
                // handler wired at the <ReactFlow> level — which is our case
                // here (interactions live inside each custom node instead).
                // Without this override, buttons/links inside the card are
                // visually present but never receive clicks.
                style: { pointerEvents: 'all' as const },
            }));

        const nextEdges: Edge[] = sourceNodes
            .filter(
                (node) =>
                    node.parent_id !== null &&
                    visible.has(node.id) &&
                    visible.has(node.parent_id),
            )
            .map((node) => ({
                id: `${node.parent_id}->${node.id}`,
                source: node.parent_id as string,
                target: node.id,
                type: 'smoothstep',
                style: { stroke: 'var(--border)', strokeWidth: 1.5 },
            }));

        return layoutElements(nextNodes, nextEdges);
    }, [sourceNodes, collapsedIds, parentIdsWithChildren]);

    const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
    const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges);

    useEffect(() => {
        setNodes(layoutedNodes);
        setEdges(layoutedEdges);
        requestAnimationFrame(() => {
            fitView({ padding: 0.2, duration: 200 });
        });
    }, [layoutedNodes, layoutedEdges, setNodes, setEdges, fitView]);

    const exportPng = useCallback(async () => {
        const viewport = document.querySelector(
            '.react-flow__viewport',
        ) as HTMLElement | null;

        if (!viewport) {
            return;
        }

        const { zoom } = getViewport();
        const dataUrl = await toPng(viewport, {
            backgroundColor:
                getComputedStyle(document.documentElement)
                    .getPropertyValue('--background')
                    .trim() || '#ffffff',
            pixelRatio: Math.max(2, zoom),
            cacheBust: true,
        });

        const link = document.createElement('a');
        link.download = `organigramme-${new Date().toISOString().slice(0, 10)}.png`;
        link.href = dataUrl;
        link.click();
    }, [getViewport]);

    useImperativeHandle(
        ref,
        () => ({
            fitView: () => fitView({ padding: 0.2, duration: 200 }),
            zoomIn: () => zoomIn({ duration: 150 }),
            zoomOut: () => zoomOut({ duration: 150 }),
            expandAll,
            collapseAll,
            exportPng,
        }),
        [fitView, zoomIn, zoomOut, expandAll, collapseAll, exportPng],
    );

    return (
        <OrgChartCollapseContext.Provider value={toggleCollapse}>
            <ReactFlow
                nodes={nodes}
                edges={edges}
                onNodesChange={onNodesChange}
                onEdgesChange={onEdgesChange}
                nodeTypes={nodeTypes}
                fitView
                fitViewOptions={{ padding: 0.2 }}
                minZoom={0.2}
                maxZoom={1.75}
                nodesDraggable={false}
                nodesConnectable={false}
                elementsSelectable={false}
                panOnScroll
                proOptions={{ hideAttribution: true }}
                className="h-full bg-muted/20"
            >
                <Background gap={20} size={1} />
                <Controls showInteractive={false} />
                <MiniMap
                    pannable
                    zoomable
                    className="!bg-background"
                    maskColor="rgb(0 0 0 / 8%)"
                />
            </ReactFlow>
        </OrgChartCollapseContext.Provider>
    );
}

const OrganizationChartCanvasForward = forwardRef(OrganizationChartCanvasInner);

export function OrganizationChartCanvas({
    nodes,
    canvasRef,
}: Props & { canvasRef?: Ref<OrganizationChartCanvasHandle> }) {
    const isClient = useIsClient();

    if (!isClient) {
        return (
            <div className="flex h-full items-center justify-center text-sm text-muted-foreground">
                Chargement de l’organigramme…
            </div>
        );
    }

    return (
        <ReactFlowProvider>
            <div className="h-full w-full">
                <OrganizationChartCanvasForward
                    ref={canvasRef}
                    nodes={nodes}
                />
            </div>
        </ReactFlowProvider>
    );
}
