import { useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
  AlertCircle,
  CircleDot,
  Eye,
  EyeOff,
  GitBranch,
  Keyboard,
  Layers,
  Map as MapIcon,
  Network,
  RefreshCw,
} from "lucide-react";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { Drawer } from "@/components/ui/drawer";
import { EmptyState } from "@/components/ui/empty-state";
import { Tooltip } from "@/components/ui/tooltip";
import {
  FilterPanel,
  FilterSearch,
  FilterSelect,
  FilterRange,
  FilterToggle,
  FilterChips,
  type ActiveChip,
} from "@/components/ui/filter-panel";
import {
  ShortcutsOverlay,
  type ShortcutGroup,
} from "@/components/ui/shortcuts-overlay";
import { PageHeader } from "@/components/layout/PageHeader";
import { useGraphNeighborhood } from "@/hooks/usePlatform";
import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";
import { useProject } from "@/hooks/useProjects";
import { GraphEdge, GraphNode } from "@/lib/api/platform";
import { formatDateTime, formatPercent, humanizeValue } from "@/lib/display";

const COLORS = [
  "#2563eb",
  "#059669",
  "#d97706",
  "#7c3aed",
  "#db2777",
  "#0891b2",
  "#4b5563",
  "#dc2626",
];

const VIEW_W = 860;
const VIEW_H = 600;

interface VisualNode {
  id: string;
  name: string;
  type: string;
  x: number;
  y: number;
  radius: number;
  color: string;
  source: "entity" | "literal";
  raw?: GraphNode;
}

interface VisualEdge {
  id: string;
  source: string;
  target: string;
  predicate: string;
  confidence: number;
  raw: GraphEdge;
}

function nodeColor(type: string, types: string[]): string {
  const index = Math.max(types.indexOf(type), 0);
  return COLORS[index % COLORS.length];
}

function buildGraph(
  nodes: GraphNode[],
  edges: GraphEdge[],
  options: {
    search: string;
    predicate: string;
    entityType: string;
    minConfidence: number;
  },
) {
  const search = options.search.trim().toLowerCase();
  const types = Array.from(
    new Set(nodes.map((node) => node.type || "Entity").concat("Literal")),
  ).sort();
  const nodeMap = new Map<string, VisualNode>();
  nodes.forEach((node) => {
    const type = node.type || "Entity";
    nodeMap.set(String(node.id), {
      id: String(node.id),
      name: node.name,
      type,
      x: 0,
      y: 0,
      radius: 18,
      color: nodeColor(type, types),
      source: "entity",
      raw: node,
    });
  });

  const visualEdges: VisualEdge[] = [];
  edges.forEach((edge) => {
    const confidence = edge.confidence ?? 0;
    if (confidence < options.minConfidence) return;
    if (options.predicate && edge.predicate !== options.predicate) return;
    const source = String(edge.source);
    const target =
      edge.target !== null && edge.target !== undefined
        ? String(edge.target)
        : `literal-${edge.claim_id}`;
    if (!nodeMap.has(source)) return;
    if (!nodeMap.has(target)) {
      const label = humanizeValue(edge.target_value ?? edge.object?.name);
      nodeMap.set(target, {
        id: target,
        name: label || "(empty value)",
        type: "Literal",
        x: 0,
        y: 0,
        radius: 14,
        color: nodeColor("Literal", types),
        source: "literal",
      });
    }
    visualEdges.push({
      id: String(edge.claim_id),
      source,
      target,
      predicate: edge.predicate,
      confidence,
      raw: edge,
    });
  });

  let visualNodes = Array.from(nodeMap.values()).filter((node) => {
    if (options.entityType && node.type !== options.entityType) return false;
    if (!search) return true;
    return (
      node.name.toLowerCase().includes(search) ||
      node.type.toLowerCase().includes(search)
    );
  });
  const visibleIds = new Set(visualNodes.map((node) => node.id));
  const filteredEdges = visualEdges.filter(
    (edge) => visibleIds.has(edge.source) && visibleIds.has(edge.target),
  );
  const connectedIds = new Set<string>();
  filteredEdges.forEach((edge) => {
    connectedIds.add(edge.source);
    connectedIds.add(edge.target);
  });
  if (options.predicate || options.minConfidence > 0 || search) {
    visualNodes = visualNodes.filter((node) => connectedIds.has(node.id));
  }

  const centerX = VIEW_W / 2;
  const centerY = VIEW_H / 2;
  const radius = Math.max(160, Math.min(260, visualNodes.length * 13));
  visualNodes.forEach((node, index) => {
    const angle = (Math.PI * 2 * index) / Math.max(visualNodes.length, 1);
    node.x = centerX + Math.cos(angle) * radius;
    node.y = centerY + Math.sin(angle) * radius;
  });
  return { nodes: visualNodes, edges: filteredEdges, types };
}

function edgeEndpoint(edge: VisualEdge, nodes: VisualNode[]) {
  const source = nodes.find((node) => node.id === edge.source);
  const target = nodes.find((node) => node.id === edge.target);
  return { source, target };
}

export default function GraphViewPage() {
  const navigate = useNavigate();
  const { projectId } = useParams<{ projectId: string }>();
  const projectName = projectId ?? "";
  const { data: project } = useProject(projectName);
  const [statusScope, setStatusScope] = useState("validated_claim");
  const [includeCandidates, setIncludeCandidates] = useState(true);
  const [search, setSearch] = useState("");
  const [predicate, setPredicate] = useState("");
  const [entityType, setEntityType] = useState("");
  const [minConfidence, setMinConfidence] = useState(0);
  const [selected, setSelected] = useState<VisualNode | VisualEdge | null>(null);
  const [drawerOpen, setDrawerOpen] = useState(false);
  const [hiddenTypes, setHiddenTypes] = useState<Set<string>>(new Set());
  const [showLegend, setShowLegend] = useState(true);
  const [showMinimap, setShowMinimap] = useState(true);
  const [shortcutsOpen, setShortcutsOpen] = useState(false);

  const graph = useGraphNeighborhood(projectName, {
    includeCandidates: includeCandidates && statusScope === "validated_claim",
    limit: 300,
    status:
      includeCandidates && statusScope === "validated_claim"
        ? undefined
        : statusScope,
  });

  const predicates = useMemo(
    () =>
      Array.from(
        new Set((graph.data?.edges ?? []).map((edge) => edge.predicate)),
      ).sort(),
    [graph.data?.edges],
  );
  const visual = useMemo(
    () =>
      buildGraph(graph.data?.nodes ?? [], graph.data?.edges ?? [], {
        search,
        predicate,
        entityType,
        minConfidence,
      }),
    [
      entityType,
      graph.data?.edges,
      graph.data?.nodes,
      minConfidence,
      predicate,
      search,
    ],
  );

  // Filter out types the user toggled off in the legend
  const visibleNodes = useMemo(
    () => visual.nodes.filter((n) => !hiddenTypes.has(n.type)),
    [visual.nodes, hiddenTypes],
  );
  const visibleNodeIds = useMemo(
    () => new Set(visibleNodes.map((n) => n.id)),
    [visibleNodes],
  );
  const visibleEdges = useMemo(
    () =>
      visual.edges.filter(
        (e) => visibleNodeIds.has(e.source) && visibleNodeIds.has(e.target),
      ),
    [visual.edges, visibleNodeIds],
  );
  const typeCounts = useMemo(() => {
    const counts = new Map<string, number>();
    visual.nodes.forEach((n) =>
      counts.set(n.type, (counts.get(n.type) ?? 0) + 1),
    );
    return counts;
  }, [visual.nodes]);

  const averageConfidence = visibleEdges.length
    ? visibleEdges.reduce((sum, edge) => sum + edge.confidence, 0) /
      visibleEdges.length
    : 0;

  const selectNode = (node: VisualNode | VisualEdge) => {
    setSelected(node);
    setDrawerOpen(true);
  };

  const toggleType = (type: string) => {
    setHiddenTypes((prev) => {
      const next = new Set(prev);
      if (next.has(type)) next.delete(type);
      else next.add(type);
      return next;
    });
  };

  // Page-level shortcuts (disabled while drawer or overlay is open)
  useKeyboardShortcuts(
    {
      l: () => setShowLegend((v) => !v),
      m: () => setShowMinimap((v) => !v),
      r: () => graph.refetch(),
      "Shift+?": () => setShortcutsOpen(true),
      Escape: () => {
        if (drawerOpen) {
          setDrawerOpen(false);
          setSelected(null);
        }
      },
    },
    { disabled: shortcutsOpen },
  );

  const pageShortcutGroups: ShortcutGroup[] = [
    {
      title: "Graph View",
      items: [
        { keys: "l", label: "Toggle legend" },
        { keys: "m", label: "Toggle minimap" },
        { keys: "r", label: "Refresh graph" },
        { keys: "Escape", label: "Close detail drawer" },
      ],
    },
    {
      title: "Global",
      items: [
        { keys: "Mod+K", label: "Command palette" },
        { keys: "Shift+?", label: "Show this panel" },
        { keys: "g g", label: "Jump to Graph View" },
      ],
    },
  ];

  return (
    <>
      <PageHeader
        icon={Network}
        breadcrumbs={[
          { label: "Dashboard", to: "/" },
          { label: projectName, to: `/sources/${projectName}` },
          { label: "Graph View" },
        ]}
        title="Graph View"
        description={`${project?.name ?? projectName} — relation graph, candidates, and edge evidence.`}
        actions={
          <Tooltip content="Refresh graph data" shortcut="R">
            <Button variant="outline" onClick={() => graph.refetch()}>
              <RefreshCw className="h-4 w-4" />
              Refresh
            </Button>
          </Tooltip>
        }
      />

      <div className="mx-auto max-w-7xl px-6 py-6">
        {graph.isError && (
          <div className="mb-4 flex items-center gap-2 rounded-md border border-danger-border bg-danger-subtle px-4 py-3 text-sm text-danger">
            <AlertCircle className="h-4 w-4" />
            {(graph.error as Error).message}
          </div>
        )}

        {/* Filter bar — unified FilterPanel */}
        {(() => {
          const statusLabels: Record<string, string> = {
            validated_claim: "Validated",
            candidate_claim: "Candidate",
            rule_candidate: "Rule candidate",
            active: "Active",
            all: "All statuses",
          };
          const chips: ActiveChip[] = [];
          if (search)
            chips.push({
              id: "search",
              label: "Search",
              value: `"${search}"`,
              onClear: () => setSearch(""),
            });
          if (entityType)
            chips.push({
              id: "type",
              label: "Type",
              value: entityType,
              onClear: () => setEntityType(""),
            });
          if (predicate)
            chips.push({
              id: "predicate",
              label: "Predicate",
              value: predicate,
              onClear: () => setPredicate(""),
            });
          if (statusScope !== "validated_claim")
            chips.push({
              id: "status",
              label: "Status",
              value: statusLabels[statusScope] ?? statusScope,
              onClear: () => setStatusScope("validated_claim"),
            });
          if (minConfidence > 0)
            chips.push({
              id: "conf",
              label: "Min confidence",
              value: formatPercent(minConfidence),
              onClear: () => setMinConfidence(0),
            });
          if (!includeCandidates && statusScope === "validated_claim")
            chips.push({
              id: "blend",
              label: "Blend candidates",
              value: "off",
              onClear: () => setIncludeCandidates(true),
            });

          const clearAll = () => {
            setSearch("");
            setEntityType("");
            setPredicate("");
            setStatusScope("validated_claim");
            setMinConfidence(0);
            setIncludeCandidates(true);
          };

          return (
            <FilterPanel
              className="mb-4"
              chips={chips.length > 0 ? <FilterChips chips={chips} /> : undefined}
              onClearAll={chips.length > 0 ? clearAll : undefined}
            >
              <FilterSearch
                value={search}
                onChange={setSearch}
                placeholder="Search node or type…"
              />
              <FilterSelect
                label="Type"
                value={entityType}
                onChange={setEntityType}
                placeholder="All types"
                options={visual.types.map((t) => ({
                  value: t,
                  label: t,
                  hint: typeCounts.get(t),
                }))}
              />
              <FilterSelect
                label="Predicate"
                value={predicate}
                onChange={setPredicate}
                placeholder="All predicates"
                options={predicates.map((p) => ({ value: p, label: p }))}
              />
              <FilterSelect
                label="Status"
                value={statusScope === "validated_claim" ? "" : statusScope}
                onChange={(v) => setStatusScope(v || "validated_claim")}
                placeholder="Validated"
                allowEmpty
                options={Object.entries(statusLabels)
                  .filter(([k]) => k !== "validated_claim")
                  .map(([k, v]) => ({ value: k, label: v }))}
              />
              <FilterRange
                label="Min confidence"
                value={minConfidence}
                onChange={setMinConfidence}
                format={(v) => `${Math.round(v * 100)}%`}
              />
              <FilterToggle
                label="Blend candidates"
                checked={includeCandidates}
                onChange={setIncludeCandidates}
                disabled={statusScope !== "validated_claim"}
              />
            </FilterPanel>
          );
        })()}

        {/* Canvas */}
        <div className="relative mb-6 overflow-hidden rounded-lg border border-border bg-surface">
          <div className="flex items-center justify-between border-b border-border px-4 py-2.5">
            <div className="text-sm">
              <span className="font-medium text-foreground">
                Ontology Network
              </span>
              <span className="ml-2 text-muted-foreground">
                {visibleNodes.length} nodes / {visibleEdges.length} edges shown
              </span>
            </div>
            <div className="flex items-center gap-1">
              <Tooltip
                content={showLegend ? "Hide legend" : "Show legend"}
                shortcut="L"
              >
                <Button
                  variant="ghost"
                  size="icon"
                  aria-label="Toggle legend"
                  onClick={() => setShowLegend((v) => !v)}
                >
                  <Layers className="h-4 w-4" />
                </Button>
              </Tooltip>
              <Tooltip
                content={showMinimap ? "Hide minimap" : "Show minimap"}
                shortcut="M"
              >
                <Button
                  variant="ghost"
                  size="icon"
                  aria-label="Toggle minimap"
                  onClick={() => setShowMinimap((v) => !v)}
                >
                  <MapIcon className="h-4 w-4" />
                </Button>
              </Tooltip>
              <Tooltip content="Show keyboard shortcuts" shortcut="?">
                <Button
                  variant="ghost"
                  size="icon"
                  aria-label="Keyboard shortcuts"
                  onClick={() => setShortcutsOpen(true)}
                >
                  <Keyboard className="h-4 w-4" />
                </Button>
              </Tooltip>
            </div>
          </div>

          <div className="relative">
            {graph.isLoading ? (
              <Skeleton className="h-[640px]" />
            ) : visibleNodes.length === 0 ? (
              <EmptyState
                icon={GitBranch}
                title="No graph to show"
                description="No nodes match the current filters."
                variant="muted"
                compact
                className="h-[500px]"
              />
            ) : (
              <>
                <svg
                  viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
                  className="block h-[640px] w-full bg-background-subtle text-muted-foreground"
                  role="img"
                  aria-label="Ontology graph"
                >
                  <defs>
                    <marker
                      id="arrow"
                      markerWidth="10"
                      markerHeight="10"
                      refX="10"
                      refY="3"
                      orient="auto"
                      markerUnits="strokeWidth"
                    >
                      <path d="M0,0 L0,6 L9,3 z" fill="currentColor" />
                    </marker>
                  </defs>
                  {visibleEdges.map((edge) => {
                    const { source, target } = edgeEndpoint(edge, visibleNodes);
                    if (!source || !target) return null;
                    const midX = (source.x + target.x) / 2;
                    const midY = (source.y + target.y) / 2;
                    const isSelected =
                      selected && "predicate" in selected && selected.id === edge.id;
                    return (
                      <g
                        key={edge.id}
                        className="cursor-pointer"
                        onClick={() => selectNode(edge)}
                      >
                        <line
                          x1={source.x}
                          y1={source.y}
                          x2={target.x}
                          y2={target.y}
                          stroke="currentColor"
                          strokeWidth={1 + edge.confidence * 3}
                          strokeOpacity={isSelected ? 1 : 0.55}
                          markerEnd="url(#arrow)"
                          className={isSelected ? "text-brand-600 dark:text-brand-300" : ""}
                        />
                        <text
                          x={midX}
                          y={midY}
                          textAnchor="middle"
                          className="fill-muted-foreground text-[10px]"
                          paintOrder="stroke"
                          stroke="hsl(var(--background))"
                          strokeWidth="3"
                          strokeLinejoin="round"
                        >
                          {edge.predicate}
                        </text>
                      </g>
                    );
                  })}
                  {visibleNodes.map((node) => {
                    const isSelected =
                      selected && "id" in selected && selected.id === node.id;
                    return (
                      <g
                        key={node.id}
                        className="cursor-pointer"
                        onClick={() => selectNode(node)}
                      >
                        <circle
                          cx={node.x}
                          cy={node.y}
                          r={node.radius}
                          fill={node.color}
                          className={
                            isSelected
                              ? "stroke-foreground"
                              : "stroke-background"
                          }
                          strokeWidth={isSelected ? 3 : 2}
                        />
                        <text
                          x={node.x}
                          y={node.y + node.radius + 14}
                          textAnchor="middle"
                          className="fill-foreground text-[11px] font-medium"
                          paintOrder="stroke"
                          stroke="hsl(var(--background))"
                          strokeWidth="3"
                          strokeLinejoin="round"
                        >
                          {node.name.length > 24
                            ? `${node.name.slice(0, 24)}...`
                            : node.name}
                        </text>
                      </g>
                    );
                  })}
                </svg>

                {/* Legend overlay (top-right) */}
                {showLegend && (
                  <LegendPanel
                    types={visual.types}
                    typeCounts={typeCounts}
                    hiddenTypes={hiddenTypes}
                    onToggle={toggleType}
                  />
                )}

                {/* Minimap overlay (bottom-right) */}
                {showMinimap && visibleNodes.length > 0 && (
                  <Minimap nodes={visibleNodes} edges={visibleEdges} />
                )}
              </>
            )}
          </div>
        </div>

        {/* Bottom stats row */}
        <div className="grid gap-4 lg:grid-cols-2">
          <Card>
            <CardHeader>
              <CardTitle className="text-sm">Graph Health</CardTitle>
            </CardHeader>
            <CardContent className="space-y-3">
              <HealthRow label="Visible nodes" value={visibleNodes.length} />
              <HealthRow label="Visible edges" value={visibleEdges.length} />
              <HealthRow
                label="Avg confidence"
                value={
                  visibleEdges.length ? formatPercent(averageConfidence) : "—"
                }
              />
              <Progress value={averageConfidence * 100} />
            </CardContent>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle className="flex items-center gap-2 text-sm">
                <CircleDot className="h-4 w-4" />
                Predicate Counts
              </CardTitle>
            </CardHeader>
            <CardContent>
              <ul className="grid gap-1.5 sm:grid-cols-2">
                {predicates.map((name) => {
                  const count = (graph.data?.edges ?? []).filter(
                    (edge) => edge.predicate === name,
                  ).length;
                  return (
                    <li
                      key={name}
                      className="flex items-center justify-between rounded-md border border-border px-3 py-2 text-sm"
                    >
                      <span className="truncate">{name}</span>
                      <span className="font-medium tabular-nums">{count}</span>
                    </li>
                  );
                })}
                {predicates.length === 0 && (
                  <li className="col-span-full py-4 text-center text-sm text-muted-foreground">
                    No predicates found.
                  </li>
                )}
              </ul>
            </CardContent>
          </Card>
        </div>
      </div>

      {/* Node / Edge detail drawer */}
      <Drawer
        open={drawerOpen && !!selected}
        onOpenChange={(open) => {
          setDrawerOpen(open);
          if (!open) setSelected(null);
        }}
        size="md"
        title={
          selected
            ? "predicate" in selected
              ? `Edge · ${selected.predicate}`
              : `Node · ${selected.name}`
            : ""
        }
        description={
          selected && "predicate" in selected
            ? "Claim evidence and confidence"
            : "Entity details and metadata"
        }
        headerExtra={
          selected && (
            <Badge variant={"predicate" in selected ? "secondary" : "outline"}>
              {"predicate" in selected ? selected.predicate : selected.type}
            </Badge>
          )
        }
        footer={
          <Button
            variant="ghost"
            onClick={() => {
              setDrawerOpen(false);
              setSelected(null);
            }}
          >
            Close
          </Button>
        }
      >
        {selected && "predicate" in selected && (
          <div className="space-y-3">
            <Detail label="Claim" value={`#${selected.id}`} />
            <Detail label="Status" value={selected.raw.status} />
            <Detail
              label="Confidence"
              value={formatPercent(selected.confidence)}
            />
            <Detail
              label="Object"
              value={selected.raw.object?.name ?? selected.raw.target_value}
            />
            <Detail
              label="Last seen"
              value={formatDateTime(selected.raw.last_seen_at)}
            />
            <Detail
              label="Metadata"
              value={humanizeValue(selected.raw.metadata)}
            />
          </div>
        )}
        {selected && !("predicate" in selected) && (
          <div className="space-y-3">
            <Detail label="Name" value={selected.name} />
            <Detail label="Source" value={selected.source} />
            <Detail
              label="Canonical"
              value={selected.raw?.canonical_name}
            />
            <Detail
              label="Metadata"
              value={humanizeValue(selected.raw?.metadata)}
            />
            <Button
              variant="outline"
              size="sm"
              className="w-full"
              onClick={() => navigate(`/editor/${projectName}`)}
            >
              Open in Entity Manager
            </Button>
          </div>
        )}
      </Drawer>

      <ShortcutsOverlay
        open={shortcutsOpen}
        onOpenChange={setShortcutsOpen}
        groups={pageShortcutGroups}
        title="Graph View shortcuts"
      />
    </>
  );
}

/* ============================================================
 * Legend Panel — overlay, toggle visibility per type
 * ========================================================== */
function LegendPanel({
  types,
  typeCounts,
  hiddenTypes,
  onToggle,
}: {
  types: string[];
  typeCounts: Map<string, number>;
  hiddenTypes: Set<string>;
  onToggle: (type: string) => void;
}) {
  return (
    <div className="absolute right-3 top-3 w-52 rounded-md border border-border bg-popover/95 shadow-md backdrop-blur-sm">
      <div className="border-b border-border px-3 py-1.5 text-2xs font-semibold uppercase tracking-wider text-muted-foreground">
        Legend
      </div>
      <ul className="max-h-64 overflow-y-auto py-1">
        {types.map((type) => {
          const hidden = hiddenTypes.has(type);
          const count = typeCounts.get(type) ?? 0;
          return (
            <li key={type}>
              <button
                type="button"
                onClick={() => onToggle(type)}
                className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors hover:bg-accent/60"
                aria-pressed={!hidden}
              >
                <span
                  className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
                  style={{
                    backgroundColor: nodeColor(type, types),
                    opacity: hidden ? 0.3 : 1,
                  }}
                />
                <span
                  className={
                    hidden
                      ? "flex-1 truncate text-muted-foreground/60 line-through"
                      : "flex-1 truncate text-foreground"
                  }
                >
                  {type}
                </span>
                <span className="text-2xs text-muted-foreground tabular-nums">
                  {count}
                </span>
                {hidden ? (
                  <EyeOff className="h-3 w-3 text-muted-foreground/60" />
                ) : (
                  <Eye className="h-3 w-3 text-muted-foreground" />
                )}
              </button>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

/* ============================================================
 * Minimap — scaled overview of the whole graph
 * ========================================================== */
function Minimap({
  nodes,
  edges,
}: {
  nodes: VisualNode[];
  edges: VisualEdge[];
}) {
  const W = 180;
  const H = 130;
  // Bounds of visible nodes (with padding so circles don't clip)
  const pad = 24;
  const xs = nodes.map((n) => n.x);
  const ys = nodes.map((n) => n.y);
  const minX = Math.min(...xs) - pad;
  const maxX = Math.max(...xs) + pad;
  const minY = Math.min(...ys) - pad;
  const maxY = Math.max(...ys) + pad;
  const w = Math.max(maxX - minX, 1);
  const h = Math.max(maxY - minY, 1);
  const scale = Math.min(W / w, H / h);
  const offsetX = (W - w * scale) / 2;
  const offsetY = (H - h * scale) / 2;
  const tx = (x: number) => (x - minX) * scale + offsetX;
  const ty = (y: number) => (y - minY) * scale + offsetY;

  return (
    <div className="absolute bottom-3 right-3 rounded-md border border-border bg-popover/95 shadow-md backdrop-blur-sm">
      <div className="flex items-center justify-between border-b border-border px-2.5 py-1 text-2xs font-semibold uppercase tracking-wider text-muted-foreground">
        <span>Minimap</span>
        <span className="font-mono normal-case">
          {nodes.length}·{edges.length}
        </span>
      </div>
      <svg
        width={W}
        height={H}
        className="block text-muted-foreground"
        aria-hidden="true"
      >
        {edges.map((e) => {
          const s = nodes.find((n) => n.id === e.source);
          const t = nodes.find((n) => n.id === e.target);
          if (!s || !t) return null;
          return (
            <line
              key={e.id}
              x1={tx(s.x)}
              y1={ty(s.y)}
              x2={tx(t.x)}
              y2={ty(t.y)}
              stroke="currentColor"
              strokeOpacity="0.35"
              strokeWidth="0.5"
            />
          );
        })}
        {nodes.map((n) => (
          <circle
            key={n.id}
            cx={tx(n.x)}
            cy={ty(n.y)}
            r={1.8}
            fill={n.color}
          />
        ))}
      </svg>
    </div>
  );
}

function Detail({ label, value }: { label: string; value: unknown }) {
  return (
    <div className="rounded-md border border-border bg-background-subtle px-3 py-2 text-sm">
      <div className="text-2xs uppercase tracking-wider text-muted-foreground">
        {label}
      </div>
      <div className="mt-1 break-words font-medium text-foreground">
        {humanizeValue(value)}
      </div>
    </div>
  );
}

function HealthRow({ label, value }: { label: string; value: number | string }) {
  return (
    <div className="flex items-center justify-between text-sm">
      <span className="text-muted-foreground">{label}</span>
      <span className="font-medium tabular-nums">
        {typeof value === "number" ? value.toLocaleString() : value}
      </span>
    </div>
  );
}
