import { memo, useEffect, useMemo, useRef, useState } from "react";
import { Graph, type GraphConfig } from "@cosmos.gl/graph";
import { cosmosBackgroundCss, cosmosRuntimeConfig } from "./cosmos_config";
import {
  computeLayoutMetrics,
  type GraphLayoutMetrics,
  type TripleGraphLink,
  type TripleGraphModel,
  type TripleGraphNode,
} from "./triple_graph";

type TripleGraphViewProps = {
  model: TripleGraphModel;
};

type InspectState =
  | { kind: "node"; node: TripleGraphNode }
  | { kind: "link"; link: TripleGraphLink }
  | null;

type LayoutDebugState = {
  phase: "idle" | "running" | "ended";
  alpha: number | null;
  progress: number;
  currentMetrics: GraphLayoutMetrics;
  lastEvent: string;
  zoomLevel: number;
  screenCenter: { x: number; y: number };
  screenOrigin: { x: number; y: number };
  screenCentroid: { x: number; y: number };
  originDelta: { x: number; y: number };
  centroidDelta: { x: number; y: number };
  nearSpaceBoundary: boolean;
};

export const TripleGraphView = memo(function TripleGraphView({ model }: TripleGraphViewProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const graphRef = useRef<Graph | null>(null);
  const modelRef = useRef(model);
  const debugLogTimeRef = useRef(0);
  const [hovered, setHovered] = useState<InspectState>(null);
  const [pinned, setPinned] = useState<InspectState>(null);
  const [layoutDebug, setLayoutDebug] = useState<LayoutDebugState>({
    phase: "idle",
    alpha: null,
    progress: 0,
    currentMetrics: model.seedMetrics,
    lastEvent: "seed",
    zoomLevel: 0,
    screenCenter: { x: 0, y: 0 },
    screenOrigin: { x: 0, y: 0 },
    screenCentroid: { x: 0, y: 0 },
    originDelta: { x: 0, y: 0 },
    centroidDelta: { x: 0, y: 0 },
    nearSpaceBoundary: false,
  });

  const activeDetail = useMemo(() => pinned ?? hovered, [pinned, hovered]);

  useEffect(() => {
    modelRef.current = model;
  }, [model]);

  useEffect(() => {
    setLayoutDebug({
      phase: "idle",
      alpha: null,
      progress: 0,
      currentMetrics: model.seedMetrics,
      lastEvent: "seed",
      zoomLevel: 0,
      screenCenter: { x: 0, y: 0 },
      screenOrigin: { x: 0, y: 0 },
      screenCentroid: { x: 0, y: 0 },
      originDelta: { x: 0, y: 0 },
      centroidDelta: { x: 0, y: 0 },
      nearSpaceBoundary: false,
    });
    if (cosmosRuntimeConfig.debugLayout) {
      console.debug("[cosmos-layout]", {
        event: "seed-applied",
        seedCentroid: {
          x: Number(model.seedMetrics.centroidX.toFixed(3)),
          y: Number(model.seedMetrics.centroidY.toFixed(3)),
        },
        bounds: {
          width: Number(model.seedMetrics.width.toFixed(3)),
          height: Number(model.seedMetrics.height.toFixed(3)),
          maxRadius: Number(model.seedMetrics.maxRadius.toFixed(3)),
        },
      });
    }
  }, [model]);

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

    const reheatSimulation = () => {
      if (!cosmosRuntimeConfig.enableSimulation) return;
      graphRef.current?.start(0.25);
    };

    const reportLayout = (event: string, phase: LayoutDebugState["phase"], alpha?: number) => {
      const graph = graphRef.current;
      if (!graph || !cosmosRuntimeConfig.debugLayout) return;
      const currentMetrics = computeLayoutMetrics(graph.getPointPositions());
      const containerRect = container.getBoundingClientRect();
      const screenCenter = {
        x: containerRect.width / 2,
        y: containerRect.height / 2,
      };
      const screenOriginTuple = graph.spaceToScreenPosition([0, 0]);
      const screenCentroidTuple = graph.spaceToScreenPosition([
        currentMetrics.centroidX,
        currentMetrics.centroidY,
      ]);
      const screenOrigin = { x: screenOriginTuple[0], y: screenOriginTuple[1] };
      const screenCentroid = { x: screenCentroidTuple[0], y: screenCentroidTuple[1] };
      const originDelta = {
        x: screenOrigin.x - screenCenter.x,
        y: screenOrigin.y - screenCenter.y,
      };
      const centroidDelta = {
        x: screenCentroid.x - screenCenter.x,
        y: screenCentroid.y - screenCenter.y,
      };
      const boundaryMargin = cosmosRuntimeConfig.spaceSize * 0.02;
      const nearSpaceBoundary =
        currentMetrics.minX <= boundaryMargin ||
        currentMetrics.maxX >= cosmosRuntimeConfig.spaceSize - boundaryMargin ||
        currentMetrics.minY <= boundaryMargin ||
        currentMetrics.maxY >= cosmosRuntimeConfig.spaceSize - boundaryMargin;
      const now = performance.now();
      const shouldPublish = event !== "tick" || now - debugLogTimeRef.current >= 250;
      const next: LayoutDebugState = {
        phase,
        alpha: typeof alpha === "number" ? alpha : null,
        progress: graph.progress,
        currentMetrics,
        lastEvent: event,
        zoomLevel: graph.getZoomLevel(),
        screenCenter,
        screenOrigin,
        screenCentroid,
        originDelta,
        centroidDelta,
        nearSpaceBoundary,
      };
      if (!shouldPublish) return;
      debugLogTimeRef.current = now;
      setLayoutDebug(next);
      console.debug("[cosmos-layout]", {
        event,
        phase,
        alpha: next.alpha,
        progress: Number(next.progress.toFixed(4)),
        seedCentroid: {
          x: Number(modelRef.current.seedMetrics.centroidX.toFixed(3)),
          y: Number(modelRef.current.seedMetrics.centroidY.toFixed(3)),
        },
        currentCentroid: {
          x: Number(currentMetrics.centroidX.toFixed(3)),
          y: Number(currentMetrics.centroidY.toFixed(3)),
        },
        screenCenter: {
          x: Number(screenCenter.x.toFixed(2)),
          y: Number(screenCenter.y.toFixed(2)),
        },
        screenOrigin: {
          x: Number(screenOrigin.x.toFixed(2)),
          y: Number(screenOrigin.y.toFixed(2)),
        },
        screenCentroid: {
          x: Number(screenCentroid.x.toFixed(2)),
          y: Number(screenCentroid.y.toFixed(2)),
        },
        originDelta: {
          x: Number(originDelta.x.toFixed(2)),
          y: Number(originDelta.y.toFixed(2)),
        },
        centroidDelta: {
          x: Number(centroidDelta.x.toFixed(2)),
          y: Number(centroidDelta.y.toFixed(2)),
        },
        zoomLevel: Number(next.zoomLevel.toFixed(4)),
        nearSpaceBoundary,
        bounds: {
          width: Number(currentMetrics.width.toFixed(3)),
          height: Number(currentMetrics.height.toFixed(3)),
          maxRadius: Number(currentMetrics.maxRadius.toFixed(3)),
        },
      });
    };

    const config: GraphConfig = {
      backgroundColor: cosmosRuntimeConfig.backgroundColor,
      spaceSize: cosmosRuntimeConfig.spaceSize,
      enableSimulation: cosmosRuntimeConfig.enableSimulation,
      pointDefaultColor: cosmosRuntimeConfig.pointDefaultColor,
      pointGreyoutColor: cosmosRuntimeConfig.pointGreyoutColor,
      pointGreyoutOpacity: cosmosRuntimeConfig.pointGreyoutOpacity,
      pointDefaultSize: cosmosRuntimeConfig.pointDefaultSize,
      pointOpacity: cosmosRuntimeConfig.pointOpacity,
      pointSizeScale: cosmosRuntimeConfig.pointSizeScale,
      hoveredPointCursor: cosmosRuntimeConfig.hoveredPointCursor,
      hoveredLinkCursor: cosmosRuntimeConfig.hoveredLinkCursor,
      renderHoveredPointRing: cosmosRuntimeConfig.renderHoveredPointRing,
      hoveredPointRingColor: cosmosRuntimeConfig.hoveredPointRingColor,
      focusedPointRingColor: cosmosRuntimeConfig.focusedPointRingColor,
      renderLinks: cosmosRuntimeConfig.renderLinks,
      linkDefaultColor: cosmosRuntimeConfig.linkDefaultColor,
      linkOpacity: cosmosRuntimeConfig.linkOpacity,
      linkGreyoutOpacity: cosmosRuntimeConfig.linkGreyoutOpacity,
      linkDefaultWidth: cosmosRuntimeConfig.linkDefaultWidth,
      hoveredLinkColor: cosmosRuntimeConfig.hoveredLinkColor,
      hoveredLinkWidthIncrease: cosmosRuntimeConfig.hoveredLinkWidthIncrease,
      linkWidthScale: cosmosRuntimeConfig.linkWidthScale,
      scaleLinksOnZoom: cosmosRuntimeConfig.scaleLinksOnZoom,
      enableDrag: cosmosRuntimeConfig.enableDrag,
      enableZoom: cosmosRuntimeConfig.enableZoom,
      enableSimulationDuringZoom: cosmosRuntimeConfig.enableSimulationDuringZoom,
      fitViewOnInit: cosmosRuntimeConfig.fitViewOnInit,
      fitViewDelay: cosmosRuntimeConfig.fitViewDelay,
      fitViewPadding: cosmosRuntimeConfig.fitViewPadding,
      fitViewDuration: cosmosRuntimeConfig.fitViewDuration,
      initialZoomLevel: cosmosRuntimeConfig.initialZoomLevel,
      pointSamplingDistance: cosmosRuntimeConfig.pointSamplingDistance,
      rescalePositions: cosmosRuntimeConfig.rescalePositions,
      curvedLinks: cosmosRuntimeConfig.curvedLinks,
      curvedLinkSegments: cosmosRuntimeConfig.curvedLinkSegments,
      curvedLinkWeight: cosmosRuntimeConfig.curvedLinkWeight,
      curvedLinkControlPointDistance: cosmosRuntimeConfig.curvedLinkControlPointDistance,
      linkDefaultArrows: cosmosRuntimeConfig.linkDefaultArrows,
      linkArrowsSizeScale: cosmosRuntimeConfig.linkArrowsSizeScale,
      linkVisibilityDistanceRange: cosmosRuntimeConfig.linkVisibilityDistanceRange,
      linkVisibilityMinTransparency: cosmosRuntimeConfig.linkVisibilityMinTransparency,
      useClassicQuadtree: cosmosRuntimeConfig.useClassicQuadtree,
      simulationDecay: cosmosRuntimeConfig.simulationDecay,
      simulationGravity: cosmosRuntimeConfig.simulationGravity,
      simulationCenter: cosmosRuntimeConfig.simulationCenter,
      simulationRepulsion: cosmosRuntimeConfig.simulationRepulsion,
      simulationRepulsionTheta: cosmosRuntimeConfig.simulationRepulsionTheta,
      simulationRepulsionQuadtreeLevels:
        cosmosRuntimeConfig.simulationRepulsionQuadtreeLevels,
      simulationLinkSpring: cosmosRuntimeConfig.simulationLinkSpring,
      simulationLinkDistance: cosmosRuntimeConfig.simulationLinkDistance,
      simulationLinkDistRandomVariationRange:
        cosmosRuntimeConfig.simulationLinkDistRandomVariationRange,
      simulationRepulsionFromMouse: cosmosRuntimeConfig.simulationRepulsionFromMouse,
      enableRightClickRepulsion: cosmosRuntimeConfig.enableRightClickRepulsion,
      simulationFriction: cosmosRuntimeConfig.simulationFriction,
      simulationCluster: cosmosRuntimeConfig.simulationCluster,
      randomSeed: cosmosRuntimeConfig.randomSeed,
      showFPSMonitor: cosmosRuntimeConfig.showFPSMonitor,
      pixelRatio: cosmosRuntimeConfig.pixelRatio,
      scalePointsOnZoom: cosmosRuntimeConfig.scalePointsOnZoom,
      attribution: cosmosRuntimeConfig.attribution,
      onSimulationStart: () => {
        reportLayout("simulation-start", "running", 1);
      },
      onSimulationTick: (alpha) => {
        reportLayout("tick", "running", alpha);
      },
      onSimulationEnd: () => {
        reportLayout("simulation-end", "ended", 0);
      },
      onPointMouseOver: (index) => {
        const node = modelRef.current.nodes[index];
        if (!node) return;
        setHovered({ kind: "node", node });
      },
      onPointMouseOut: () => {
        setHovered((prev) => (prev?.kind === "node" ? null : prev));
      },
      onLinkMouseOver: (linkIndex) => {
        const link = modelRef.current.linksMeta[linkIndex];
        if (!link) return;
        setHovered({ kind: "link", link });
      },
      onLinkMouseOut: () => {
        setHovered((prev) => (prev?.kind === "link" ? null : prev));
      },
      onPointClick: (index) => {
        const node = modelRef.current.nodes[index];
        if (!node) return;
        setPinned({ kind: "node", node });
      },
      onLinkClick: (linkIndex) => {
        const link = modelRef.current.linksMeta[linkIndex];
        if (!link) return;
        setPinned({ kind: "link", link });
      },
      onClick: (index) => {
        if (typeof index === "number") return;
        setPinned(null);
      },
      onDragStart: () => {
        reportLayout("drag-start", "running");
        reheatSimulation();
      },
      onDragEnd: () => {
        reportLayout("drag-end", "running");
        reheatSimulation();
      },
    };

    const graph = new Graph(container, config);
    graphRef.current = graph;
    if (cosmosRuntimeConfig.debugLayout) {
      console.debug("[cosmos-layout]", {
        event: "graph-created",
        seedCentroid: {
          x: Number(modelRef.current.seedMetrics.centroidX.toFixed(3)),
          y: Number(modelRef.current.seedMetrics.centroidY.toFixed(3)),
        },
        seedRadius: Number(modelRef.current.seedMetrics.maxRadius.toFixed(3)),
      });
    }

    return () => {
      setHovered(null);
      setPinned(null);
      graphRef.current = null;
      graph.destroy();
    };
  }, []);

  useEffect(() => {
    const graph = graphRef.current;
    if (!graph) return;
    setHovered(null);
    setPinned(null);
    applyGraphModel(graph, model);
    if (cosmosRuntimeConfig.debugLayout) {
      requestAnimationFrame(() => {
        const positionedGraph = graphRef.current;
        if (!positionedGraph) return;
        const currentMetrics = computeLayoutMetrics(positionedGraph.getPointPositions());
        const origin = positionedGraph.spaceToScreenPosition([0, 0]);
        const centroid = positionedGraph.spaceToScreenPosition([
          currentMetrics.centroidX,
          currentMetrics.centroidY,
        ]);
        console.debug("[cosmos-layout]", {
          event: "after-fit-requested",
          screenOrigin: { x: Number(origin[0].toFixed(2)), y: Number(origin[1].toFixed(2)) },
          screenCentroid: { x: Number(centroid[0].toFixed(2)), y: Number(centroid[1].toFixed(2)) },
        });
      });
    }
  }, [model]);

  useEffect(() => {
    const graph = graphRef.current;
    if (!graph) return;
    graph.setConfig({
      focusedPointIndex: activeDetail?.kind === "node" ? activeDetail.node.index : undefined,
    });
  }, [activeDetail]);

  return (
    <div
      style={{
        position: "relative",
        flex: 1,
        minHeight: 0,
        background: cosmosBackgroundCss,
      }}
    >
      <div
        ref={containerRef}
        style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}
      />

      {cosmosRuntimeConfig.debugLayout && (
        <div
          style={{
            position: "absolute",
            top: 12,
            left: 12,
            maxWidth: "min(340px, calc(100% - 24px))",
            background: "rgba(4, 7, 11, 0.94)",
            border: "1px solid rgba(255,255,255,0.1)",
            borderRadius: "10px",
            padding: "10px 12px",
            color: "#d7e2ea",
            fontFamily: "monospace",
            fontSize: "11px",
            lineHeight: 1.5,
            boxShadow: "0 10px 28px rgba(0,0,0,0.35)",
            pointerEvents: "none",
          }}
        >
          <div style={{ color: "#88a9b9", textTransform: "uppercase", letterSpacing: "0.08em" }}>
            Layout Debug
          </div>
          <div style={{ marginTop: "6px" }}>
            phase: {layoutDebug.phase} · event: {layoutDebug.lastEvent}
          </div>
          <div>
            alpha: {formatMaybeNumber(layoutDebug.alpha)} · progress: {formatNumber(layoutDebug.progress)}
          </div>
          <div>zoom: {formatNumber(layoutDebug.zoomLevel)}</div>
          <div style={{ marginTop: "8px", color: "#9ac7d8" }}>seed centroid</div>
          <div>
            ({formatNumber(model.seedMetrics.centroidX)}, {formatNumber(model.seedMetrics.centroidY)})
          </div>
          <div>
            bounds: {formatNumber(model.seedMetrics.width)} × {formatNumber(model.seedMetrics.height)} · r={formatNumber(model.seedMetrics.maxRadius)}
          </div>
          <div style={{ marginTop: "8px", color: "#f0c674" }}>current centroid</div>
          <div>
            ({formatNumber(layoutDebug.currentMetrics.centroidX)}, {formatNumber(layoutDebug.currentMetrics.centroidY)})
          </div>
          <div>
            bounds: {formatNumber(layoutDebug.currentMetrics.width)} × {formatNumber(layoutDebug.currentMetrics.height)} · r={formatNumber(layoutDebug.currentMetrics.maxRadius)}
          </div>
          <div style={{ marginTop: "8px", color: "#9ac7d8" }}>screen center</div>
          <div>
            ({formatNumber(layoutDebug.screenCenter.x)}, {formatNumber(layoutDebug.screenCenter.y)})
          </div>
          <div style={{ marginTop: "8px", color: "#f0c674" }}>screen origin</div>
          <div>
            ({formatNumber(layoutDebug.screenOrigin.x)}, {formatNumber(layoutDebug.screenOrigin.y)}) d=({formatNumber(layoutDebug.originDelta.x)}, {formatNumber(layoutDebug.originDelta.y)})
          </div>
          <div style={{ marginTop: "8px", color: "#f0c674" }}>screen centroid</div>
          <div>
            ({formatNumber(layoutDebug.screenCentroid.x)}, {formatNumber(layoutDebug.screenCentroid.y)}) d=({formatNumber(layoutDebug.centroidDelta.x)}, {formatNumber(layoutDebug.centroidDelta.y)})
          </div>
          <div style={{ marginTop: "8px", color: layoutDebug.nearSpaceBoundary ? "#ff8b8b" : "#8fd2a8" }}>
            near space boundary: {layoutDebug.nearSpaceBoundary ? "yes" : "no"}
          </div>
        </div>
      )}

      <div
        style={{
          position: "absolute",
          right: 12,
          bottom: 12,
          width: "min(420px, calc(100% - 24px))",
          maxHeight: "calc(100% - 24px)",
          overflowY: "auto",
          background: "rgba(4, 7, 11, 0.94)",
          border: "1px solid rgba(255,255,255,0.1)",
          borderRadius: "10px",
          padding: "12px 14px",
          color: "#d7e2ea",
          fontFamily: "monospace",
          fontSize: "12px",
          lineHeight: 1.5,
          boxShadow: "0 10px 28px rgba(0,0,0,0.35)",
          wordBreak: "break-all",
          pointerEvents: "none",
        }}
      >
        <div style={{ color: "#88a9b9", fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.08em" }}>
          {pinned ? "Pinned details" : activeDetail ? "Hovered details" : "Inspector"}
        </div>

        {!activeDetail && (
          <div style={{ marginTop: "8px", color: "#a7b7c2" }}>
            Hover a node or edge to inspect it. Click a node or edge to pin its details.
          </div>
        )}

        {activeDetail?.kind === "node" && (
          <>
            <div style={{ marginTop: "8px", color: activeDetail.node.isSelectedSource ? "#35d6ff" : "#9ac7d8", fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.08em" }}>
              Node
            </div>
            <div style={{ marginTop: "4px" }}>{activeDetail.node.text}</div>
            {typeof activeDetail.node.backendId === "number" && (
              <div style={{ marginTop: "6px", color: "#88a9b9" }}>
                backend id: {activeDetail.node.backendId}
              </div>
            )}
            {activeDetail.node.isSelectedSource && (
              <div style={{ marginTop: "4px", color: "#35d6ff" }}>
                selected source node
              </div>
            )}
          </>
        )}

        {activeDetail?.kind === "link" && (
          <>
            <div style={{ marginTop: "8px", color: "#f0c674", fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.08em" }}>
              Edge
            </div>
            <div style={{ marginTop: "4px", color: "#f0c674" }}>{activeDetail.link.predicateText}</div>
            <div style={{ marginTop: "8px", color: "#88a9b9" }}>from</div>
            <div>{activeDetail.link.sourceText}</div>
            <div style={{ marginTop: "8px", color: "#88a9b9" }}>to</div>
            <div>{activeDetail.link.targetText}</div>
            {typeof activeDetail.link.predicateId === "number" && (
              <div style={{ marginTop: "6px", color: "#88a9b9" }}>
                predicate id: {activeDetail.link.predicateId}
              </div>
            )}
          </>
        )}
      </div>
    </div>
  );
});

function applyGraphModel(graph: Graph, model: TripleGraphModel): void {
  graph.setPointPositions(model.pointPositions);
  graph.setLinks(model.links);
  graph.setPointColors(model.pointColors);
  graph.setPointSizes(model.pointSizes);
  graph.setLinkColors(model.linkColors);
  graph.setLinkWidths(model.linkWidths);
  graph.render(0);
  requestAnimationFrame(() => {
    if (typeof cosmosRuntimeConfig.initialZoomLevel === "number") {
      graph.setZoomLevel(
        cosmosRuntimeConfig.initialZoomLevel,
        cosmosRuntimeConfig.fitViewDuration,
      );
    } else {
      graph.fitViewByPointPositions(
        Array.from(model.pointPositions),
        cosmosRuntimeConfig.fitViewDuration,
        cosmosRuntimeConfig.fitViewPadding,
      );
    }
    if (cosmosRuntimeConfig.enableSimulation) {
      graph.start(1);
    }
  });
}

function formatNumber(value: number): string {
  return value.toFixed(2);
}

function formatMaybeNumber(value: number | null): string {
  return value === null ? "-" : value.toFixed(3);
}
