/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { useLocale, useTranslations } from 'next-intl';

import {
  ExternalLink,
  Info,
  Maximize2,
  Minimize2,
  Scan,
  ToggleLeft,
  ToggleRight,
  X,
} from 'lucide-react';
import ForceGraph2D from 'react-force-graph-2d';

import CreatorAvatar from '@/components/creators/CreatorAvatar';
import { getCreationCardImageSrc, getCreationHref } from '@/utils/creationHelpers';
import { getFullSizeImageByKey } from '@/utils/images';

import { CreationListItem } from '@/types/Creation';

import { Link } from '@/i18n/navigation';

type GraphSubjectWord = {
  cnt: number;
  id: number;
  nev: string;
  telepules?: boolean;
};

type GraphAward = {
  id: number;
  nev: string;
  ev?: number | null;
};

export type ConnectionPerson = {
  id: number;
  nev: string;
  targyszo: GraphSubjectWord[] | null;
  dijak?: GraphAward[] | null;
  fileName: string | null;
  fileId: number | null;
  fileKey?: string | null;
  alkoto_azonosito: string | null;
  szakma: string | null;
};

type PersonNode = {
  id: string;
  type: 'person';
  name: string;
  personId: number;
  avatar?: string;
  profession?: string;
};

type SubjectNode = {
  id: string;
  type: 'subject';
  label: string;
  subjectId: number;
  count?: number;
  isSettlement?: boolean;
};

type AwardNode = {
  id: string;
  type: 'award';
  label: string;
  awardId: number;
};

type GraphNode = PersonNode | SubjectNode | AwardNode;

type GraphLink = {
  source: string;
  target: string;
};

type InternalNode = GraphNode & {
  neighbors?: InternalNode[];
  links?: InternalLink[];
  x?: number;
  y?: number;
  fx?: number;
  fy?: number;
};

type InternalLink = {
  source: InternalNode;
  target: InternalNode;
};

type InternalGraph = {
  nodes: InternalNode[];
  links: InternalLink[];
};

type ExpansionEntry = {
  linkKeys: Set<string>;
};

const NODE_R = 10;
const imageCache = new Map<string, HTMLImageElement>();
const normalizeName = (value: string) =>
  value
    .toLowerCase()
    .normalize('NFKD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/\s+/g, ' ')
    .trim();

const linkKey = (sourceId: string, targetId: string) => `${sourceId}|${targetId}`;

function cloneNode(node: InternalNode): InternalNode {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const { neighbors, links, ...rest } = node;
  return { ...rest };
}

function rebuildGraph(nodes: InternalNode[], links: InternalLink[]) {
  const nodeMap = new Map(nodes.map((node) => [node.id, cloneNode(node)]));
  const internalLinks: InternalLink[] = links
    .map((link) => {
      const source = nodeMap.get(link.source.id);
      const target = nodeMap.get(link.target.id);
      if (!source || !target) return null;
      return { source, target };
    })
    .filter((link): link is InternalLink => Boolean(link));

  internalLinks.forEach((link) => {
    const { source, target } = link;

    source.neighbors ??= [];
    target.neighbors ??= [];
    source.links ??= [];
    target.links ??= [];

    source.neighbors.push(target);
    target.neighbors.push(source);
    source.links.push(link);
    target.links.push(link);
  });

  return {
    nodes: Array.from(nodeMap.values()),
    links: internalLinks,
  };
}

function addGraphData(prev: InternalGraph, nodes: GraphNode[], links: GraphLink[]): InternalGraph {
  const nodeMap = new Map(prev.nodes.map((n) => [n.id, n]));

  nodes.forEach((n) => {
    if (!nodeMap.has(n.id)) {
      nodeMap.set(n.id, { ...n });
    }
  });

  const existingLinkKeys = new Set(prev.links.map((link) => `${link.source.id}|${link.target.id}`));

  const allLinks: InternalLink[] = [...prev.links];

  links.forEach((link) => {
    const source = nodeMap.get(link.source);
    const target = nodeMap.get(link.target);

    if (!source || !target) return;

    const key = `${source.id}|${target.id}`;
    if (existingLinkKeys.has(key)) return;

    existingLinkKeys.add(key);

    const internalLink: InternalLink = { source, target };
    allLinks.push(internalLink);

    source.neighbors ??= [];
    target.neighbors ??= [];
    source.links ??= [];
    target.links ??= [];

    if (!source.neighbors.includes(target)) source.neighbors.push(target);
    if (!target.neighbors.includes(source)) target.neighbors.push(source);

    source.links.push(internalLink);
    target.links.push(internalLink);
  });

  return {
    nodes: Array.from(nodeMap.values()),
    links: allLinks,
  };
}

function createPersonNode(
  personId: number,
  name: string,
  avatar?: string,
  profession?: string
): PersonNode {
  return {
    id: `person:${personId}`,
    type: 'person',
    name,
    personId,
    avatar,
    profession,
  };
}

function getAvatarUrl(person: ConnectionPerson | null | undefined) {
  if (!person) return undefined;

  const imageKey = person.fileKey || person.fileName;

  if (!imageKey) return undefined;

  return getFullSizeImageByKey(imageKey);
}

function buildSubjectNodes(subjects: GraphSubjectWord[]): SubjectNode[] {
  return subjects.map((subject) => ({
    id: `subject:${subject.id}`,
    type: 'subject',
    label: subject.nev,
    subjectId: subject.id,
    count: subject.cnt,
    isSettlement: subject.telepules ?? false,
  }));
}

function buildAwardNodes(awards: GraphAward[]): AwardNode[] {
  const unique = new Map<number, GraphAward>();
  awards.forEach((award) => {
    if (!award || award.id === undefined || award.id === null) return;
    if (!unique.has(award.id)) unique.set(award.id, award);
  });

  return Array.from(unique.values()).map((award) => ({
    id: `award:${award.id}`,
    type: 'award',
    label: award.nev,
    awardId: award.id,
  }));
}

function extractPersonsFromConnections(
  items: ConnectionPerson[],
  fallbackName: string
): PersonNode[] {
  const unique = new Map<string, ConnectionPerson>();

  if (!Array.isArray(items)) return [];

  items.forEach((item) => {
    if (!item || item.id === undefined || item.id === null) {
      return;
    }

    const id = String(item.id);
    if (!unique.has(id)) {
      unique.set(id, item);
    }
  });

  return Array.from(unique.entries()).map(([id, person]) =>
    createPersonNode(
      Number(id),
      person.nev || fallbackName,
      getAvatarUrl(person),
      person.szakma ?? undefined
    )
  );
}

function escapeHtml(value: string) {
  return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

const SUBTITLE_MAX = 5;

function formatNameList(names: string[], max = SUBTITLE_MAX) {
  if (names.length <= max) return names.join(', ');
  return names.slice(0, max).join(', ');
}

interface Interactive2DGraphProps {
  creatorId: string;
  creatorName: string;
  creatorAvatar?: string;
  connections: ConnectionPerson[];
  className?: string;
  showControls?: boolean;
  onRequestFullscreen?: () => void;
  fullscreenMode?: boolean;
  controlsSlot?: React.ReactNode;
}

const Interactive2DGraph: React.FC<Interactive2DGraphProps> = ({
  creatorId,
  creatorName,
  creatorAvatar,
  connections,
  className = '',
  showControls = false,
  onRequestFullscreen,
  fullscreenMode = false,
  controlsSlot,
}) => {
  const t = useTranslations('connectionsGraph');
  const locale = useLocale();
  const graphRef = useRef<any>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const [highlightNodes, setHighlightNodes] = useState<Set<InternalNode>>(new Set());
  const [highlightLinks, setHighlightLinks] = useState<Set<InternalLink>>(new Set());
  const [hoverNode, setHoverNode] = useState<InternalNode | null>(null);
  const [expansions, setExpansions] = useState<Map<string, ExpansionEntry>>(new Map());
  const [rootNodeId, setRootNodeId] = useState<string | null>(null);
  const [data, setData] = useState<InternalGraph>({ nodes: [], links: [] });
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  const [hasFitInitial, setHasFitInitial] = useState(false);
  const [showTags, setShowTags] = useState(true);
  const [showSettlements, setShowSettlements] = useState(false);
  const [showAwards, setShowAwards] = useState(false);
  const [showInfo, setShowInfo] = useState(false);
  const [creationPanel, setCreationPanel] = useState<{
    open: boolean;
    title: string;
    subtitle?: string;
    subtitleFull?: string;
    subtitleOverflowCount?: number;
    items: CreationListItem[];
    creators: ConnectionPerson[];
    mode: 'creations' | 'creators';
    loading: boolean;
    error?: string;
  }>({
    open: false,
    title: '',
    subtitle: undefined,
    subtitleFull: undefined,
    subtitleOverflowCount: undefined,
    items: [],
    creators: [],
    mode: 'creations',
    loading: false,
  });
  const lastGlobalScaleRef = useRef(1);

  const { connectionsById, connectionsByName, peopleBySubject, peopleByAward } = useMemo(() => {
    const byId = new Map<number, ConnectionPerson>();
    const byName = new Map<string, ConnectionPerson>();
    const bySubject = new Map<number, ConnectionPerson[]>();
    const byAward = new Map<number, ConnectionPerson[]>();

    connections.forEach((person) => {
      byId.set(person.id, person);
      byName.set(normalizeName(person.nev), person);

      const subjects = person.targyszo ?? [];
      subjects.forEach((subject) => {
        const list = bySubject.get(subject.id) ?? [];
        list.push(person);
        bySubject.set(subject.id, list);
      });

      const awards = person.dijak ?? [];
      awards.forEach((award) => {
        const list = byAward.get(award.id) ?? [];
        list.push(person);
        byAward.set(award.id, list);
      });
    });

    return {
      connectionsById: byId,
      connectionsByName: byName,
      peopleBySubject: bySubject,
      peopleByAward: byAward,
    };
  }, [connections]);

  const visibleData = useMemo(() => {
    if (showSettlements && showTags && showAwards) return data;
    const visibleNodes = data.nodes.filter((node) => {
      if (node.type === 'subject' && node.isSettlement && !showSettlements) {
        return false;
      }
      if (node.type === 'subject' && !node.isSettlement && !showTags) {
        return false;
      }
      if (node.type === 'award' && !showAwards) return false;
      return true;
    });
    const visibleIds = new Set(visibleNodes.map((node) => node.id));
    const visibleLinks = data.links.filter(
      (link) => visibleIds.has(link.source.id) && visibleIds.has(link.target.id)
    );
    const linkedIds = new Set<string>();
    visibleLinks.forEach((link) => {
      linkedIds.add(link.source.id);
      linkedIds.add(link.target.id);
    });
    if (rootNodeId) linkedIds.add(rootNodeId);
    const prunedNodes = visibleNodes.filter((node) => linkedIds.has(node.id));
    return { nodes: prunedNodes, links: visibleLinks };
  }, [data, showSettlements, showTags, showAwards, rootNodeId]);

  const hasSettlementNodes = useMemo(
    () => data.nodes.some((node) => node.type === 'subject' && node.isSettlement),
    [data.nodes]
  );

  const fitGraph = useCallback(() => {
    if (!graphRef.current) return;
    graphRef.current.centerAt(0, 0, 0);
    graphRef.current.zoomToFit(500, 80);
    if (dimensions.width > 0 && dimensions.width < 900) {
      const current = graphRef.current.zoom();
      const capped = Math.min(current, 1.1);
      if (capped !== current) {
        graphRef.current.zoom(capped, 200);
      }
    }
  }, [dimensions.width]);

  const zoomBy = useCallback((delta: number) => {
    if (!graphRef.current?.zoom) return;
    const current = graphRef.current.zoom();
    const next = Math.max(0.2, Math.min(8, current + delta));
    graphRef.current.zoom(next, 200);
  }, []);

  const restoreZoomPosition = useCallback(() => {
    if (!graphRef.current) return;
    setData((prev) => {
      prev.nodes.forEach((node) => {
        node.fx = undefined;
        node.fy = undefined;
      });
      return { ...prev };
    });
    graphRef.current.centerAt(0, 0, 0);
    graphRef.current.zoom(1, 200);
    if (graphRef.current.d3ReheatSimulation) {
      graphRef.current.d3ReheatSimulation();
    }
  }, []);

  const fitToScreen = useCallback(() => {
    if (!graphRef.current) return;
    graphRef.current.zoomToFit(600, 80);
  }, []);

  const updateHighlight = () => {
    setHighlightNodes(highlightNodes);
    setHighlightLinks(highlightLinks);
  };

  const handleNodeHover = (node: InternalNode | null) => {
    highlightNodes.clear();
    highlightLinks.clear();

    if (node) {
      highlightNodes.add(node);
      node.neighbors?.forEach((neighbor) => highlightNodes.add(neighbor));
      node.links?.forEach((link) => highlightLinks.add(link));
    }

    setHoverNode(node || null);
    updateHighlight();
  };

  const handleLinkHover = (link: InternalLink | null) => {
    highlightNodes.clear();
    highlightLinks.clear();

    if (link) {
      highlightLinks.add(link);
      highlightNodes.add(link.source);
      highlightNodes.add(link.target);
    }

    updateHighlight();
  };

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

    const loadGraph = async () => {
      if (!creatorId) {
        setData({ nodes: [], links: [] });
        // setExpanded(new Set());
        return;
      }

      try {
        const fallbackName = t('unknownCreator');
        const safeName = creatorName?.trim() || fallbackName;
        const person = connectionsByName.get(normalizeName(safeName)) ?? null;
        if (!active) return;

        const subjects = person?.targyszo ?? [];
        const fallbackId = connectionsById.size + 1;
        const centerNode = createPersonNode(
          person?.id ?? fallbackId,
          person?.nev || safeName,
          getAvatarUrl(person) ?? creatorAvatar,
          person?.szakma ?? undefined
        );
        const subjectNodes = buildSubjectNodes(subjects);
        const awardNodes = buildAwardNodes(person?.dijak ?? []);
        const links = [...subjectNodes, ...awardNodes].map((node) => ({
          source: centerNode.id,
          target: node.id,
        }));

        const graph = addGraphData(
          { nodes: [], links: [] },
          [centerNode, ...subjectNodes, ...awardNodes],
          links
        );

        setHasFitInitial(false);
        setData(graph);
        setRootNodeId(centerNode.id);
        setExpansions(
          new Map([
            [
              centerNode.id,
              {
                linkKeys: new Set(links.map((link) => linkKey(link.source, link.target))),
              },
            ],
          ])
        );
      } catch (err) {
        console.error('Failed to load graph data from connections.json:', err);
        if (!active) return;
        setData({ nodes: [], links: [] });
        setRootNodeId(null);
        setExpansions(new Map());
      }
    };

    loadGraph();

    return () => {
      active = false;
    };
  }, [creatorId, creatorName, creatorAvatar, t, connections, connectionsById, connectionsByName]);

  useEffect(() => {
    if (
      !graphRef.current ||
      data.nodes.length === 0 ||
      dimensions.width === 0 ||
      dimensions.height === 0
    ) {
      return;
    }

    const forceGraph = graphRef.current;

    if (forceGraph.d3Force) {
      forceGraph.d3Force('charge').strength(-220);
      forceGraph
        .d3Force('link')
        .distance((link: InternalLink) =>
          link.source.type === 'person' && link.target.type === 'person' ? 120 : 90
        )
        .strength(0.9);
    }

    if (forceGraph.d3ReheatSimulation) {
      forceGraph.d3ReheatSimulation();
    }

    if (!hasFitInitial) {
      const fit = setTimeout(() => {
        fitGraph();
        setHasFitInitial(true);
      }, 200);

      return () => clearTimeout(fit);
    }
  }, [data, fitGraph, hasFitInitial, dimensions.width, dimensions.height]);

  useEffect(() => {
    if (!fullscreenMode || data.nodes.length === 0) return;
    const fit = setTimeout(() => {
      fitGraph();
    }, 200);

    return () => clearTimeout(fit);
  }, [fullscreenMode, data.nodes.length, fitGraph]);

  useEffect(() => {
    if (!containerRef.current) return;

    const observer = new ResizeObserver((entries) => {
      entries.forEach((entry) => {
        const { width, height } = entry.contentRect;
        setDimensions({ width, height });
      });
    });

    observer.observe(containerRef.current);

    return () => observer.disconnect();
  }, []);

  const paintRing = useCallback(
    (node: InternalNode, ctx: CanvasRenderingContext2D, globalScale: number) => {
      lastGlobalScaleRef.current = globalScale;
      const r = NODE_R;
      const fontSize = 12 / globalScale;

      if (highlightNodes.has(node)) {
        ctx.beginPath();
        ctx.arc(node.x!, node.y!, r * 1.2, 0, 2 * Math.PI);
        ctx.fillStyle = node === hoverNode ? 'red' : 'orange';
        ctx.fill();
      }

      const isExpanded = expansions.has(node.id);
      if (isExpanded) {
        const ringR = node.type === 'subject' ? r * 1.1 : r * 1.35;
        ctx.beginPath();
        ctx.arc(node.x!, node.y!, ringR, 0, 2 * Math.PI);
        ctx.strokeStyle = '#22c55e';
        ctx.lineWidth = 1.5;
        ctx.stroke();
      }

      if (node.type === 'person') {
        const avatar = node.avatar;

        if (avatar) {
          let img = imageCache.get(avatar);
          if (!img) {
            img = new Image();
            img.src = avatar;
            imageCache.set(avatar, img);
          }

          ctx.save();
          ctx.beginPath();
          ctx.arc(node.x!, node.y!, r, 0, 2 * Math.PI);
          ctx.closePath();
          ctx.clip();

          if (img.complete && img.naturalWidth > 0) {
            ctx.drawImage(img, node.x! - r, node.y! - r, r * 2, r * 2);
          }

          ctx.restore();

          ctx.beginPath();
          ctx.arc(node.x!, node.y!, r, 0, 2 * Math.PI);
          ctx.strokeStyle = '#cbd5f5';
          ctx.lineWidth = 1;
          ctx.stroke();
        } else {
          ctx.beginPath();
          ctx.arc(node.x!, node.y!, r, 0, 2 * Math.PI);
          ctx.fillStyle = '#f1f5f9';
          ctx.fill();
          ctx.strokeStyle = '#94a3b8';
          ctx.lineWidth = 1;
          ctx.stroke();
        }

        ctx.font = `${fontSize}px sans-serif`;
        ctx.fillStyle = '#111';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'top';
        ctx.fillText(node.name, node.x!, node.y! + r + 4);
      }

      if (node.type === 'subject') {
        const isSettlement = Boolean(node.isSettlement);
        const fillColor = isSettlement ? '#6ba9a9' : '#9a7fae';
        const strokeColor = isSettlement ? '#2d5a5a' : '#4f3c63';
        const textColor = isSettlement ? '#2d5a5a' : '#4f3c63';

        ctx.beginPath();
        ctx.arc(node.x!, node.y!, r * 0.7, 0, 2 * Math.PI);
        ctx.fillStyle = fillColor;
        ctx.fill();
        ctx.strokeStyle = strokeColor;
        ctx.lineWidth = 1;
        ctx.stroke();

        // const label = node.count ? `${node.label} (${node.count})` : node.label;
        const label = node.label;

        ctx.font = `${fontSize}px sans-serif`;
        ctx.fillStyle = textColor;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'top';
        ctx.fillText(label, node.x!, node.y! + r * 0.8 + 4);
      }

      if (node.type === 'award') {
        ctx.beginPath();
        ctx.arc(node.x!, node.y!, r * 0.75, 0, 2 * Math.PI);
        ctx.fillStyle = '#d2b062';
        ctx.fill();
        ctx.strokeStyle = '#6b4e16';
        ctx.lineWidth = 1;
        ctx.stroke();

        const label = node.label;

        ctx.font = `${fontSize}px sans-serif`;
        ctx.fillStyle = '#5e4312';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'top';
        ctx.fillText(label, node.x!, node.y! + r * 0.85 + 4);
      }
    },
    [hoverNode, highlightNodes, expansions]
  );

  const openCreationPanel = useCallback(
    async (node: InternalNode) => {
      if (node.type === 'award') {
        const creators = Array.from(
          new Map(
            (peopleByAward.get(node.awardId) ?? []).map((person) => [
              person.alkoto_azonosito ?? person.id,
              person,
            ])
          ).values()
        ).sort((a, b) => (a.nev || '').localeCompare(b.nev || '', locale));
        setCreationPanel({
          open: true,
          title: node.label,
          subtitle: undefined,
          subtitleFull: undefined,
          subtitleOverflowCount: undefined,
          items: [],
          creators,
          mode: 'creators',
          loading: false,
        });
        return;
      }

      const neighborPeople =
        node.type === 'subject'
          ? Array.from(
              new Map(
                (node.neighbors ?? [])
                  .filter((neighbor) => neighbor.type === 'person')
                  .map((neighbor) => [normalizeName(neighbor.name), neighbor.name])
              ).values()
            ).sort((a, b) => a.localeCompare(b, locale))
          : [];
      const neighborTags =
        node.type === 'person'
          ? Array.from(
              new Map(
                (node.neighbors ?? [])
                  .filter((neighbor) => neighbor.type === 'subject')
                  .map((neighbor) => [
                    normalizeName((neighbor as SubjectNode).label),
                    (neighbor as SubjectNode).label,
                  ])
              ).values()
            ).sort((a, b) => a.localeCompare(b, locale))
          : [];
      const uniqueNeighborPeople = Array.from(new Set(neighborPeople));
      const uniqueNeighborTags = Array.from(new Set(neighborTags));
      const formattedNames =
        node.type === 'subject' && neighborPeople.length > 0
          ? formatNameList(uniqueNeighborPeople)
          : undefined;
      const formattedTags =
        node.type === 'person' && neighborTags.length > 0
          ? formatNameList(uniqueNeighborTags)
          : undefined;
      const subtitleOverflowCount =
        node.type === 'subject'
          ? Math.max(0, uniqueNeighborPeople.length - SUBTITLE_MAX)
          : node.type === 'person'
            ? Math.max(0, uniqueNeighborTags.length - SUBTITLE_MAX)
            : undefined;
      const subtitleFull =
        node.type === 'subject'
          ? uniqueNeighborPeople.join(', ')
          : node.type === 'person'
            ? uniqueNeighborTags.join(', ')
            : undefined;
      const title = node.type === 'person' ? node.name : node.type === 'subject' ? node.label : '';
      setCreationPanel({
        open: true,
        title,
        subtitle: formattedNames ?? formattedTags,
        subtitleFull,
        subtitleOverflowCount,
        items: [],
        creators: [],
        mode: 'creations',
        loading: true,
      });

      try {
        let url = '/api/get-creations';
        if (node.type === 'person') {
          const person = connectionsById.get(node.personId);
          const identifier = person?.alkoto_azonosito;
          if (!identifier) {
            setCreationPanel((prev) => ({
              ...prev,
              loading: false,
              error: t('panel.errors.missingIdentifier'),
            }));
            return;
          }
          const subjectIds = (node.neighbors ?? [])
            .filter((neighbor) => neighbor.type === 'subject')
            .map((neighbor) => (neighbor as SubjectNode).subjectId)
            .filter((subjectId) => Number.isFinite(subjectId));
          const params = new URLSearchParams();
          params.set('personIdentifierList', JSON.stringify([identifier]));
          if (subjectIds.length > 0) {
            params.set('conceptionIdList', JSON.stringify(subjectIds));
          }
          url += `?${params.toString()}`;
        } else if (node.type === 'subject') {
          const neighborIdentifiers = (node.neighbors ?? [])
            .filter((neighbor) => neighbor.type === 'person')
            .map((neighbor) => connectionsById.get(neighbor.personId))
            .map((person) => person?.alkoto_azonosito)
            .filter((identifier): identifier is string => Boolean(identifier));

          const params = new URLSearchParams();
          params.set('conceptionIdList', JSON.stringify([node.subjectId]));
          if (neighborIdentifiers.length > 0) {
            params.set('personIdentifierList', JSON.stringify(neighborIdentifiers));
          }
          url += `?${params.toString()}`;
        } else {
          setCreationPanel((prev) => ({
            ...prev,
            loading: false,
            error: t('panel.errors.unknownNode'),
          }));
          return;
        }

        const res = await fetch(url);
        if (!res.ok) {
          throw new Error(`Creations fetch failed: ${res.status}`);
        }
        const response = (await res.json()) as {
          success: boolean;
          data?: CreationListItem[];
        };
        const items = Array.isArray(response?.data) ? response.data : [];
        setCreationPanel((prev) => ({
          ...prev,
          items,
          loading: false,
        }));
      } catch (err) {
        console.error('Failed to load creation list:', err);
        setCreationPanel((prev) => ({
          ...prev,
          loading: false,
          error: t('panel.errors.failed'),
        }));
      }
    },
    [connectionsById, peopleByAward, locale, t]
  );

  const handleNodeClick = useCallback(
    async (node: InternalNode) => {
      if (expansions.has(node.id)) {
        const nextExpansions = new Map(expansions);
        nextExpansions.delete(node.id);

        const keepLinkKeys = new Set<string>();
        nextExpansions.forEach((entry) => {
          entry.linkKeys.forEach((key) => keepLinkKeys.add(key));
        });

        setExpansions(nextExpansions);
        setData((prev) => {
          const remainingLinks = prev.links.filter((link) =>
            keepLinkKeys.has(linkKey(link.source.id, link.target.id))
          );
          const keepNodeIds = new Set<string>();
          const expandedNodeIds = new Set<string>(nextExpansions.keys());
          if (rootNodeId) expandedNodeIds.add(rootNodeId);

          remainingLinks.forEach((link) => {
            keepNodeIds.add(link.source.id);
            keepNodeIds.add(link.target.id);
          });

          expandedNodeIds.forEach((id) => keepNodeIds.add(id));

          const remainingNodes = prev.nodes.filter((n) => {
            if (keepNodeIds.has(n.id)) return true;
            const hasLink = n.links?.some((link) =>
              keepLinkKeys.has(linkKey(link.source.id, link.target.id))
            );
            return Boolean(hasLink);
          });

          return rebuildGraph(remainingNodes, remainingLinks);
        });

        return;
      }

      const fallbackName = t('unknownCreator');

      if (node.type === 'subject') {
        try {
          const items = peopleBySubject.get(node.subjectId) ?? [];
          const personNodes = extractPersonsFromConnections(items, fallbackName);
          const links = personNodes.map((person) => ({
            source: node.id,
            target: person.id,
          }));

          setData((prev) => addGraphData(prev, personNodes, links));
          setExpansions((prev) => {
            const next = new Map(prev);
            next.set(node.id, {
              linkKeys: new Set(links.map((link) => linkKey(link.source, link.target))),
            });
            return next;
          });
        } catch (err) {
          console.error('Failed to expand subject node:', err);
        }

        return;
      }

      if (node.type === 'award') {
        try {
          const items = peopleByAward.get(node.awardId) ?? [];
          const personNodes = extractPersonsFromConnections(items, fallbackName);
          const links = personNodes.map((person) => ({
            source: node.id,
            target: person.id,
          }));

          setData((prev) => addGraphData(prev, personNodes, links));
          setExpansions((prev) => {
            const next = new Map(prev);
            next.set(node.id, {
              linkKeys: new Set(links.map((link) => linkKey(link.source, link.target))),
            });
            return next;
          });
        } catch (err) {
          console.error('Failed to expand award node:', err);
        }
        return;
      }

      if (node.type === 'person') {
        try {
          const person = connectionsById.get(node.personId);
          const subjectNodes = buildSubjectNodes(person?.targyszo ?? []);
          const awardNodes = buildAwardNodes(person?.dijak ?? []);
          const links = [...subjectNodes, ...awardNodes].map((subject) => ({
            source: node.id,
            target: subject.id,
          }));

          setData((prev) => addGraphData(prev, [...subjectNodes, ...awardNodes], links));
          setExpansions((prev) => {
            const next = new Map(prev);
            next.set(node.id, {
              linkKeys: new Set(links.map((link) => linkKey(link.source, link.target))),
            });
            return next;
          });
        } catch (err) {
          console.error('Failed to expand person node:', err);
        }
      }
    },
    [connectionsById, expansions, peopleByAward, peopleBySubject, rootNodeId, t]
  );

  const handleGraphNodeClick = useCallback(
    (node: InternalNode) => {
      handleNodeClick(node);
    },
    [handleNodeClick]
  );

  const handleGraphNodeRightClick = useCallback(
    (node: InternalNode) => {
      openCreationPanel(node);
    },
    [openCreationPanel]
  );

  return (
    <div ref={containerRef} className={`relative h-full w-full ${className}`}>
      {showControls && (
        <>
          <div className="absolute bottom-4 left-4 z-10 flex flex-col gap-2">
            <button
              type="button"
              onClick={() => zoomBy(0.2)}
              className="h-10 w-10 border border-slate-200 bg-white text-lg font-semibold text-slate-700 shadow hover:bg-slate-100"
              aria-label={t('controls.zoomIn')}
              title={t('controls.zoomIn')}
            >
              +
            </button>
            <button
              type="button"
              onClick={restoreZoomPosition}
              className="flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 bg-white text-[13px] font-semibold text-slate-700 shadow hover:bg-slate-100"
              aria-label={t('controls.resetZoom')}
              title={t('controls.resetZoom')}
            >
              100%
            </button>
            <button
              type="button"
              onClick={() => zoomBy(-0.2)}
              className="h-10 w-10 border border-slate-200 bg-white text-lg font-semibold text-slate-700 shadow hover:bg-slate-100"
              aria-label={t('controls.zoomOut')}
              title={t('controls.zoomOut')}
            >
              -
            </button>
            <button
              type="button"
              onClick={() => setShowInfo((prev) => !prev)}
              className="flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-700 shadow hover:bg-slate-100"
              aria-label={t('info.label')}
              title={t('info.label')}
            >
              <Info size={18} />
            </button>
          </div>

          <div className="absolute left-3 top-3 z-10 flex flex-col gap-2 sm:left-4 sm:top-4">
            <div className="flex items-center gap-2">{controlsSlot}</div>
            <div className="flex flex-col gap-2">
              <button
                type="button"
                onClick={() => {
                  setShowAwards(false);
                  setShowTags((prev) => !prev);
                }}
                aria-pressed={showTags}
                className={`flex items-center gap-2 border px-3 py-2 text-sm font-semibold shadow transition ${
                  showTags
                    ? 'border-violet-900/30 bg-violet-50 text-violet-900'
                    : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-100'
                }`}
              >
                {showTags ? <ToggleRight size={20} /> : <ToggleLeft size={20} />}
                {t('controls.toggleTags')}
              </button>
              <button
                type="button"
                onClick={() => {
                  if (!hasSettlementNodes) return;
                  setShowAwards(false);
                  setShowSettlements((prev) => !prev);
                }}
                aria-pressed={showSettlements}
                aria-disabled={!hasSettlementNodes}
                disabled={!hasSettlementNodes}
                className={`flex items-center gap-2 border px-3 py-2 text-sm font-semibold shadow transition ${
                  showSettlements
                    ? 'border-teal-900/30 bg-teal-50 text-teal-900'
                    : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-100'
                } ${!hasSettlementNodes ? 'cursor-not-allowed opacity-50 hover:bg-white' : ''}`}
              >
                {showSettlements ? <ToggleRight size={20} /> : <ToggleLeft size={20} />}
                {t('controls.toggleSettlements')}
              </button>
              <button
                type="button"
                onClick={() => {
                  setShowAwards((prev) => {
                    const next = !prev;
                    if (next) {
                      setShowTags(false);
                      setShowSettlements(false);
                    }
                    return next;
                  });
                }}
                aria-pressed={showAwards}
                className={`flex items-center gap-2 border px-3 py-2 text-sm font-semibold shadow transition ${
                  showAwards
                    ? 'border-amber-900/30 bg-amber-50 text-amber-900'
                    : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-100'
                }`}
              >
                {showAwards ? <ToggleRight size={20} /> : <ToggleLeft size={20} />}
                {t('controls.toggleAwards')}
              </button>
            </div>
          </div>

          <button
            type="button"
            onClick={fitToScreen}
            className="absolute bottom-4 right-4 z-10 flex items-center gap-2 border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-700 shadow hover:bg-slate-100"
            aria-label={t('controls.fit')}
            title={t('controls.fit')}
          >
            <span className="hidden md:inline">{t('controls.fit')}</span>
            <Scan size={18} />
          </button>

          {onRequestFullscreen && (
            <button
              type="button"
              onClick={onRequestFullscreen}
              className="absolute right-4 top-4 z-10 flex items-center gap-2 border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-700 shadow hover:bg-slate-100"
              aria-label={fullscreenMode ? t('controls.exitFullscreen') : t('controls.fullscreen')}
              title={fullscreenMode ? t('controls.exitFullscreen') : t('controls.fullscreen')}
            >
              <span className="hidden md:inline">
                {fullscreenMode ? t('controls.exitFullscreen') : t('controls.fullscreen')}
              </span>
              {fullscreenMode ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
            </button>
          )}
        </>
      )}

      {showInfo && (
        <div className="absolute left-4 top-16 z-20 w-[280px] border border-slate-200 bg-white/95 p-3 text-xs text-slate-700 shadow-lg backdrop-blur">
          <button
            type="button"
            onClick={() => setShowInfo(false)}
            className="absolute right-2 top-2 text-slate-500 hover:text-slate-700"
            aria-label={t('panel.close')}
            title={t('panel.close')}
          >
            <X size={14} />
          </button>
          <div className="mb-1 text-sm font-semibold">{t('info.title')}</div>
          <div className="whitespace-pre-line">{t('info.body')}</div>
        </div>
      )}

      {creationPanel.open && (
        <>
          <div
            className="absolute inset-0 z-20 bg-black/30"
            onClick={() => setCreationPanel((prev) => ({ ...prev, open: false }))}
          />
          <div className="absolute right-0 top-0 z-30 flex h-full w-full max-w-[380px] flex-col border-l-4 border-mma-blue bg-white/95 p-4 shadow-2xl backdrop-blur">
            <div className="flex items-start justify-between gap-2 border-b border-slate-200 pb-3">
              <div className="flex flex-col">
                <div className="text-xs uppercase text-mma-blue font-semibold">
                  {creationPanel.mode === 'creators'
                    ? t('panel.kickerCreators')
                    : t('panel.kicker')}
                </div>
                <div className="text-lg font-bold uppercase text-mma-blue">
                  {creationPanel.title}
                </div>
                {creationPanel.subtitle && (
                  <div className="text-[11px] text-slate-500">
                    <span>{creationPanel.subtitle}</span>
                    {creationPanel.subtitleFull &&
                      (creationPanel.subtitleOverflowCount ?? 0) > 0 && (
                        <span className="group relative ml-1 text-slate-400">
                          +{creationPanel.subtitleOverflowCount}
                          <span className="pointer-events-none absolute left-0 top-full z-20 mt-1 w-56 rounded bg-slate-900 px-2 py-1.5 text-[10px] text-white opacity-0 transition group-hover:opacity-100">
                            {creationPanel.subtitleFull}
                          </span>
                        </span>
                      )}
                  </div>
                )}
              </div>
              <button
                type="button"
                className="flex h-9 w-9 items-center justify-center text-slate-500 hover:bg-slate-100"
                onClick={() => setCreationPanel((prev) => ({ ...prev, open: false }))}
                aria-label={t('panel.close')}
                title={t('panel.close')}
              >
                <X size={20} className="text-mma-blue font-bold" />
              </button>
            </div>

            <div className="flex-1 overflow-y-auto pt-3">
              {creationPanel.loading && (
                <div className="text-sm text-slate-500">{t('panel.loading')}</div>
              )}

              {!creationPanel.loading && creationPanel.error && (
                <div className="text-sm text-red-600">{creationPanel.error}</div>
              )}

              {!creationPanel.loading &&
                !creationPanel.error &&
                ((creationPanel.mode === 'creations' && creationPanel.items.length === 0) ||
                  (creationPanel.mode === 'creators' && creationPanel.creators.length === 0)) && (
                  <div className="text-sm text-slate-500">{t('panel.empty')}</div>
                )}

              <div className="flex flex-col gap-3">
                {creationPanel.mode === 'creators' &&
                  creationPanel.creators.map((creator) => {
                    const href = creator.alkoto_azonosito
                      ? {
                          pathname: '/alkoto/[id]',
                          params: { id: creator.alkoto_azonosito },
                        }
                      : null;
                    const imageKey = creator.fileKey || creator.fileName || null;
                    const content = (
                      <div className="relative flex items-center gap-3 border border-slate-200 border-l-4 border-l-mma-yellow bg-mma-blue/5 p-2 transition hover:bg-mma-cyan/40">
                        <div
                          className="absolute right-2 top-2 text-slate-400"
                          title={t('panel.openInNewTab')}
                        >
                          <ExternalLink size={14} />
                        </div>
                        <CreatorAvatar
                          imageKey={imageKey}
                          name={creator.nev || ''}
                          width={48}
                          height={48}
                          wrapperClassName="h-12 w-12 shrink-0 rounded-full"
                          imageClassName="rounded-full"
                        />
                        <div className="flex min-w-0 flex-col gap-1">
                          <div className="text-sm font-semibold text-mma-blue line-clamp-2">
                            {creator.nev || '-'}
                          </div>
                          <div className="text-xs text-slate-600 line-clamp-1">
                            {creator.szakma || '-'}
                          </div>
                        </div>
                      </div>
                    );

                    if (!href) {
                      return <div key={`${creator.id}-${creator.nev ?? ''}`}>{content}</div>;
                    }

                    return (
                      <Link
                        key={`${creator.id}-${creator.nev ?? ''}`}
                        href={href as never}
                        title={t('panel.openInNewTab')}
                        target="_blank"
                        rel="noreferrer"
                      >
                        {content}
                      </Link>
                    );
                  })}

                {creationPanel.mode === 'creations' &&
                  creationPanel.items.map((item) => {
                    const href = getCreationHref(item);
                    const imageSrc = getCreationCardImageSrc(item);
                    const content = (
                      <div className="relative flex gap-3 border border-slate-200 border-l-4 border-l-mma-yellow bg-mma-blue/5 p-2 transition hover:bg-mma-cyan/40">
                        <div
                          className="absolute right-2 top-2 text-slate-400"
                          title={t('panel.openInNewTab')}
                        >
                          <ExternalLink size={14} />
                        </div>
                        <div className="h-16 w-16 shrink-0 overflow-hidden bg-slate-100">
                          {imageSrc ? (
                            // eslint-disable-next-line @next/next/no-img-element
                            <img
                              src={imageSrc}
                              alt={item.nev || ''}
                              className="h-full w-full object-cover"
                            />
                          ) : (
                            <div className="flex h-full w-full items-center justify-center text-xs text-slate-400">
                              {t('panel.noImage')}
                            </div>
                          )}
                        </div>
                        <div className="flex min-w-0 flex-col gap-1">
                          {item.labelYear && (
                            <div className="text-xs font-semibold text-slate-500">
                              {item.labelYear}
                            </div>
                          )}
                          <div className="text-sm font-semibold text-mma-blue line-clamp-2">
                            {item.nev || '-'}
                          </div>
                          <div className="text-xs text-mma-blue line-clamp-1">
                            {item.megjelenitendoNev || '-'}
                          </div>
                          {item.telepules && (
                            <div className="text-xs text-slate-500 line-clamp-1">
                              {item.telepules}
                            </div>
                          )}
                        </div>
                      </div>
                    );

                    if (typeof href === 'string') {
                      return (
                        <a key={item.id} href={href} target="_blank" rel="noreferrer">
                          {content}
                        </a>
                      );
                    }

                    return (
                      <Link key={item.id} href={href as never} target="_blank" rel="noreferrer">
                        {content}
                      </Link>
                    );
                  })}
              </div>
            </div>
          </div>
        </>
      )}

      <ForceGraph2D
        ref={graphRef}
        width={dimensions.width}
        height={dimensions.height}
        backgroundColor="#e9e9e9"
        graphData={visibleData}
        nodeRelSize={NODE_R}
        autoPauseRedraw={false}
        cooldownTicks={120}
        warmupTicks={80}
        linkWidth={(link) => (highlightLinks.has(link) ? 5 : 1)}
        linkDirectionalParticles={4}
        linkDirectionalParticleWidth={(link) => (highlightLinks.has(link) ? 4 : 0)}
        nodeLabel={(node: InternalNode) => {
          if (node.type === 'person') {
            const name = escapeHtml(node.name);
            const profession = node.profession ? ` ${escapeHtml(node.profession)}` : '';
            const hint = escapeHtml(t('node.hint'));
            return `${name}${profession}<br/>${hint}`;
          }

          const labelKey = node.type === 'award' ? 'node.award' : 'node.subject';
          const label = escapeHtml(t(labelKey, { label: node.label }));
          const hint = escapeHtml(t('node.hint'));
          return `${label}<br/>${hint}`;
        }}
        nodeCanvasObject={paintRing}
        onNodeHover={handleNodeHover}
        onLinkHover={handleLinkHover}
        onNodeClick={handleGraphNodeClick}
        onNodeRightClick={handleGraphNodeRightClick}
        onNodeDragEnd={(node: InternalNode) => {
          node.fx = node.x;
          node.fy = node.y;
        }}
      />
    </div>
  );
};

export default Interactive2DGraph;
