'use client';

import * as d3 from 'd3';
import { useEffect, useRef } from 'react';
import { EDGE_STYLES, JURISDICTION_COLORS, NODE_RADIUS } from '@/lib/constants';
import type { GraphEdge, GraphFilters, GraphNode } from '@/lib/types';

interface Props {
  nodes: GraphNode[];
  edges: GraphEdge[];
  filters: GraphFilters;
  selectedNode: GraphNode | null;
  hoveredNode: GraphNode | null;
  onNodeClick: (node: GraphNode | null) => void;
  onNodeHover: (node: GraphNode | null) => void;
  width: number;
  height: number;
}

export function ForceGraph({
  nodes,
  edges,
  filters,
  selectedNode,
  hoveredNode,
  onNodeClick,
  onNodeHover,
  width,
  height,
}: Props) {
  const svgRef = useRef<SVGSVGElement>(null);
  const simRef = useRef<d3.Simulation<GraphNode, GraphEdge> | null>(null);
  const zoomRef = useRef<d3.ZoomBehavior<SVGSVGElement, unknown> | null>(null);

  const filteredNodes = nodes.filter(node => filters.jurisdictions.has(node.jurisdiction));
  const filteredNodeIds = new Set(filteredNodes.map(node => node.chunk_id));
  const filteredEdges = edges.filter(
    edge =>
      filters.edgeTypes.has(edge.edge_type) &&
      filteredNodeIds.has(typeof edge.source === 'string' ? edge.source : edge.source.chunk_id) &&
      filteredNodeIds.has(typeof edge.target === 'string' ? edge.target : edge.target.chunk_id),
  );

  useEffect(() => {
    if (!svgRef.current || width === 0 || height === 0) {
      return;
    }

    const svg = d3.select(svgRef.current);
    svg.selectAll('*').remove();

    const defs = svg.append('defs');
    const filter = defs.append('filter').attr('id', 'node-glow');
    filter.append('feGaussianBlur').attr('stdDeviation', '3').attr('result', 'blur');
    const merge = filter.append('feMerge');
    merge.append('feMergeNode').attr('in', 'blur');
    merge.append('feMergeNode').attr('in', 'SourceGraphic');

    const container = svg.append('g').attr('class', 'zoom-container');
    const edgeGroup = container.append('g').attr('class', 'edges');
    const nodeGroup = container.append('g').attr('class', 'nodes');

    const simNodes: GraphNode[] = filteredNodes.map(node => ({ ...node }));
    const chunkIdToNode = new Map(simNodes.map(node => [node.chunk_id, node]));
    const simEdges: GraphEdge[] = filteredEdges.map(edge => ({
      ...edge,
      source: typeof edge.source === 'string' ? edge.source : edge.source.chunk_id,
      target: typeof edge.target === 'string' ? edge.target : edge.target.chunk_id,
    }));

    const connectionCounts = simNodes.map(node => node.connection_count);
    const minConn = Math.min(...connectionCounts, 1);
    const maxConn = Math.max(...connectionCounts, 2);
    const radiusScale = d3.scaleSqrt().domain([minConn, maxConn]).range([NODE_RADIUS.MIN, NODE_RADIUS.MAX]);
    const getRadius = (node: GraphNode) =>
      filters.nodeSizeMode === 'uniform' ? NODE_RADIUS.DEFAULT : radiusScale(node.connection_count);

    const sim = d3
      .forceSimulation<GraphNode>(simNodes)
      .force(
        'link',
        d3
          .forceLink<GraphNode, GraphEdge>(simEdges)
          .id(node => node.chunk_id)
          .distance(82)
          .strength(0.38),
      )
      .force('charge', d3.forceManyBody<GraphNode>().strength(-64).distanceMax(310))
      .force('center', d3.forceCenter(width / 2, height / 2))
      .force('collide', d3.forceCollide<GraphNode>(node => getRadius(node) + 3))
      .alphaDecay(0.02)
      .velocityDecay(0.42);

    simRef.current = sim;

    const link = edgeGroup
      .selectAll<SVGLineElement, GraphEdge>('line')
      .data(simEdges)
      .join('line')
      .attr('stroke', edge => EDGE_STYLES[edge.edge_type]?.color ?? 'rgba(255,255,255,0.14)')
      .attr('stroke-width', edge => EDGE_STYLES[edge.edge_type]?.width ?? 1)
      .attr('stroke-dasharray', edge => EDGE_STYLES[edge.edge_type]?.dashArray ?? 'none')
      .attr('stroke-opacity', 0.82);

    const nodeEl = nodeGroup
      .selectAll<SVGGElement, GraphNode>('g.node')
      .data(simNodes, node => node.chunk_id)
      .join('g')
      .attr('class', 'node')
      .style('cursor', 'pointer')
      .call(
        d3
          .drag<SVGGElement, GraphNode>()
          .on('start', (event: d3.D3DragEvent<SVGGElement, GraphNode, GraphNode>, node: GraphNode) => {
            if (!event.active) {
              sim.alphaTarget(0.3).restart();
            }
            node.fx = node.x;
            node.fy = node.y;
          })
          .on('drag', (event: d3.D3DragEvent<SVGGElement, GraphNode, GraphNode>, node: GraphNode) => {
            node.fx = event.x;
            node.fy = event.y;
          })
          .on('end', (event: d3.D3DragEvent<SVGGElement, GraphNode, GraphNode>, node: GraphNode) => {
            if (!event.active) {
              sim.alphaTarget(0);
            }
            node.fx = null;
            node.fy = null;
          }),
      );

    nodeEl
      .append('circle')
      .attr('class', 'glow-ring')
      .attr('r', node => getRadius(node) + 4)
      .attr('fill', 'none')
      .attr('stroke', node => JURISDICTION_COLORS[node.jurisdiction] ?? '#888')
      .attr('stroke-width', 1.5)
      .attr('filter', 'url(#node-glow)')
      .attr('opacity', 0);

    nodeEl
      .append('circle')
      .attr('class', 'main-circle')
      .attr('r', node => getRadius(node))
      .attr('fill', node => JURISDICTION_COLORS[node.jurisdiction] ?? '#888')
      .attr('stroke', '#0d0d0d')
      .attr('stroke-width', 1.25)
      .attr('stroke-opacity', 0.75);

    nodeEl
      .append('text')
      .attr('class', 'node-label')
      .attr('dy', node => -(getRadius(node) + 6))
      .attr('text-anchor', 'middle')
      .attr('fill', 'rgba(229,226,225,0.86)')
      .attr('font-size', '10px')
      .attr('font-family', 'var(--font-inter), sans-serif')
      .attr('pointer-events', 'none')
      .text(node => `Sec ${node.section_id} / ${node.title.slice(0, 28)}${node.title.length > 28 ? '...' : ''}`)
      .attr('opacity', 0);

    nodeEl
      .on('mouseenter', (_event: MouseEvent, node: GraphNode) => {
        onNodeHover(chunkIdToNode.get(node.chunk_id) ?? null);
      })
      .on('mouseleave', () => {
        onNodeHover(null);
      })
      .on('click', (event: MouseEvent, node: GraphNode) => {
        event.stopPropagation();
        onNodeClick(chunkIdToNode.get(node.chunk_id) ?? node);
      });

    svg.on('click', () => onNodeClick(null));

    const zoom = d3
      .zoom<SVGSVGElement, unknown>()
      .scaleExtent([0.3, 5])
      .on('zoom', (event: d3.D3ZoomEvent<SVGSVGElement, unknown>) => {
        container.attr('transform', event.transform.toString());
        nodeEl.select<SVGTextElement>('.node-label').attr('opacity', event.transform.k > 2.5 ? 0.9 : 0);
      });

    zoomRef.current = zoom;
    svg.call(zoom);

    sim.on('tick', () => {
      link
        .attr('x1', edge => (edge.source as GraphNode).x ?? 0)
        .attr('y1', edge => (edge.source as GraphNode).y ?? 0)
        .attr('x2', edge => (edge.target as GraphNode).x ?? 0)
        .attr('y2', edge => (edge.target as GraphNode).y ?? 0);

      nodeEl.attr('transform', node => `translate(${node.x ?? 0},${node.y ?? 0})`);
    });

    return () => {
      sim.stop();
      svg.on('.zoom', null);
    };
    // Rebuilding here keeps D3 ownership simple when the filter set changes.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [filteredNodes.length, filteredEdges.length, width, height, filters.nodeSizeMode]);

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

    const svg = d3.select(svgRef.current);
    const nodeEl = svg.selectAll<SVGGElement, GraphNode>('g.node');

    if (!hoveredNode) {
      nodeEl.selectAll('.main-circle').attr('opacity', 1);
      nodeEl.selectAll<SVGTextElement, GraphNode>('.node-label').each(function() {
        const zoom = zoomRef.current;
        if (zoom && svgRef.current) {
          const transform = d3.zoomTransform(svgRef.current);
          d3.select(this).attr('opacity', transform.k > 2.5 ? 0.9 : 0);
        } else {
          d3.select(this).attr('opacity', 0);
        }
      });
      return;
    }

    const neighborIds = new Set<string>([hoveredNode.chunk_id]);
    svg.selectAll<SVGLineElement, GraphEdge>('line').each((edge: GraphEdge) => {
      const src = typeof edge.source === 'string' ? edge.source : edge.source.chunk_id;
      const tgt = typeof edge.target === 'string' ? edge.target : edge.target.chunk_id;
      if (src === hoveredNode.chunk_id) {
        neighborIds.add(tgt);
      }
      if (tgt === hoveredNode.chunk_id) {
        neighborIds.add(src);
      }
    });

    nodeEl.each(function(this: SVGGElement, node: GraphNode) {
      const isNeighbor = neighborIds.has(node.chunk_id);
      d3.select(this).select('.main-circle').attr('opacity', isNeighbor ? 1 : 0.12);
      d3.select(this)
        .select('.node-label')
        .attr('opacity', node.chunk_id === hoveredNode.chunk_id ? 1 : 0);
    });
  }, [hoveredNode]);

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

    const svg = d3.select(svgRef.current);
    svg.selectAll<SVGCircleElement, GraphNode>('.glow-ring').each(function(this: SVGCircleElement, node: GraphNode) {
      const isSelected = selectedNode?.chunk_id === node.chunk_id;
      d3.select(this)
        .attr('opacity', isSelected ? 0.8 : 0)
        .classed('node-selected', isSelected);
    });
  }, [selectedNode]);

  return (
    <svg
      suppressHydrationWarning
      ref={svgRef}
      width={width}
      height={height}
      className="h-full w-full bg-[#0d0d0d]"
      style={{ display: 'block' }}
    />
  );
}
