"use client";

/**
 * Corpus graph orchestrator. Owns filter + selection state, decides whether to
 * render the full graph or a focused node neighborhood (the Obsidian "local
 * graph"), and wires the filter sidebar, canvas, ranking panel, and node panel.
 *
 * Analytics (PR B): when color-by=community or size-by=pagerank/betweenness,
 * `metrics=true` is requested on the FULL graph (global importance). The
 * neighborhood endpoint does not compute metrics — instead we overlay the
 * cached full-graph metrics onto neighborhood nodes by id, so a node's
 * PageRank doesn't change just because you zoomed into it.
 */

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { ImageDown } from "lucide-react";
import { toast } from "sonner";

import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
  type CorpusGraph,
  type GraphNode,
  useCorpusGraph,
  useGraphFacets,
  useNodeNeighborhood,
} from "@/lib/api/graph";
import {
  type GraphControls,
  GraphFilterPanel,
} from "@/components/graph/graph-filter-panel";
import {
  type ColorBy,
  COMMUNITY_PALETTE,
  GraphCanvas,
  LEVEL_COLORS,
  PA_COLORS,
} from "@/components/graph/graph-canvas";
import { GraphNodePanel } from "@/components/graph/graph-node-panel";

const NODE_LIMIT = 400;

function useDebouncedValue<T>(value: T, ms: number): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), ms);
    return () => clearTimeout(id);
  }, [value, ms]);
  return debounced;
}

export function GraphView() {
  const [controls, setControls] = useState<GraphControls>({
    practiceArea: "",
    source: "",
    minCitations: 0,
    q: "",
    court: "",
    precedentLevel: "",
    chair: "",
    district: "",
    yearFrom: 0,
    yearTo: 0,
    colorBy: "type",
    sizeBy: "indegree",
    showTopics: true,
    showPracticeAreas: true,
    showHalachot: false,
    showGaps: false,
    showDigests: false,
  });
  const facets = useGraphFacets().data;
  const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
  const [focusNodeId, setFocusNodeId] = useState<string | null>(null);
  const [depth, setDepth] = useState(1);
  const canvasAreaRef = useRef<HTMLDivElement>(null);

  // ── URL deep-link (?focus=) — read once on mount, write on focus change ──
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const didInit = useRef(false);
  useEffect(() => {
    if (didInit.current) return;
    didInit.current = true;
    const f = searchParams.get("focus");
    if (f) setFocusNodeId(f);
  }, [searchParams]);
  useEffect(() => {
    if (!didInit.current) return;
    const params = new URLSearchParams(searchParams.toString());
    if (focusNodeId) params.set("focus", focusNodeId);
    else params.delete("focus");
    const qs = params.toString();
    router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [focusNodeId]);

  const onChange = (patch: Partial<GraphControls>) =>
    setControls((c) => ({ ...c, ...patch }));

  const nodeTypes = useMemo(() => {
    const t = ["precedent"];
    if (controls.showTopics) t.push("topic");
    if (controls.showPracticeAreas) t.push("practice_area");
    if (controls.showHalachot) t.push("halacha");
    if (controls.showGaps) t.push("gap");
    if (controls.showDigests) t.push("digest");
    return t.join(",");
  }, [
    controls.showTopics,
    controls.showPracticeAreas,
    controls.showHalachot,
    controls.showGaps,
    controls.showDigests,
  ]);

  // Metrics are needed when colouring by cluster or sizing by a centrality.
  const metricsOn =
    controls.sizeBy !== "indegree" || controls.colorBy === "community";

  const debouncedQ = useDebouncedValue(controls.q, 350);

  const filters = useMemo(
    () => ({
      practice_area: controls.practiceArea,
      source: controls.source,
      min_citations: controls.minCitations,
      node_types: nodeTypes,
      limit: NODE_LIMIT,
      q: debouncedQ,
      court: controls.court,
      precedent_level: controls.precedentLevel,
      chair: controls.chair,
      district: controls.district,
      year_from: controls.yearFrom,
      year_to: controls.yearTo,
      metrics: metricsOn,
    }),
    [
      controls.practiceArea,
      controls.source,
      controls.minCitations,
      controls.court,
      controls.precedentLevel,
      controls.chair,
      controls.district,
      controls.yearFrom,
      controls.yearTo,
      metricsOn,
      nodeTypes,
      debouncedQ,
    ],
  );

  const isFocused = !!focusNodeId;
  // Keep the full query alive when metrics are on so the overlay map stays warm.
  const full = useCorpusGraph(filters, !isFocused || metricsOn);
  const neighborhood = useNodeNeighborhood(focusNodeId, depth, nodeTypes);

  const active = isFocused ? neighborhood : full;
  const error = active.error as Error | undefined;

  // Cache of global metrics by node id, overlaid onto neighborhood nodes.
  const metricsMap = useMemo(() => {
    const m = new Map<
      string,
      { pagerank: number | null; betweenness: number | null; community: number | null }
    >();
    for (const n of full.data?.nodes ?? []) {
      if (n.pagerank != null || n.community != null) {
        m.set(n.id, {
          pagerank: n.pagerank,
          betweenness: n.betweenness,
          community: n.community,
        });
      }
    }
    return m;
  }, [full.data]);

  const data: CorpusGraph | undefined = useMemo(() => {
    if (!isFocused) return full.data;
    if (!neighborhood.data) return undefined;
    if (metricsMap.size === 0) return neighborhood.data;
    return {
      ...neighborhood.data,
      nodes: neighborhood.data.nodes.map((n) => {
        const mv = metricsMap.get(n.id);
        return mv ? { ...n, ...mv } : n;
      }),
    };
  }, [isFocused, full.data, neighborhood.data, metricsMap]);

  const handleNodeClick = (node: GraphNode) => {
    setSelectedNode(node);
    setFocusNodeId(node.id);
  };

  const backToFull = () => {
    setFocusNodeId(null);
    setSelectedNode(null);
  };

  const exportPng = useCallback(() => {
    const canvas = canvasAreaRef.current?.querySelector("canvas");
    if (!canvas) {
      toast.error("הגרף עדיין נטען");
      return;
    }
    try {
      const url = canvas.toDataURL("image/png");
      const a = document.createElement("a");
      a.href = url;
      a.download = "corpus-graph.png";
      a.click();
    } catch {
      toast.error("ייצוא ה-PNG נכשל");
    }
  }, []);

  const showRanking = metricsOn && !selectedNode && (full.data?.nodes.length ?? 0) > 0;

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between gap-3 text-xs text-ink-muted">
        <span className="inline-flex items-center gap-2 rounded-full border border-rule bg-surface px-3 py-1 tabular-nums">
          {data ? `${data.nodes.length} נקודות · ${data.edges.length} קשרים` : "—"}
        </span>
        <div className="flex items-center gap-3">
          {!isFocused && full.data?.truncated && (
            <span className="text-gold-deep">
              מוצגות {full.data.nodes.length} מתוך {full.data.total_available} —
              צמצמו את הסינון
            </span>
          )}
          <Button
            variant="ghost"
            size="sm"
            onClick={exportPng}
            className="h-7 gap-1.5 text-xs"
          >
            <ImageDown className="size-3.5" />
            ייצוא PNG
          </Button>
        </div>
      </div>

      <div className="flex gap-5 h-[calc(100vh-320px)] min-h-[560px] items-start">
        <GraphFilterPanel controls={controls} onChange={onChange} facets={facets} />

        <div
          ref={canvasAreaRef}
          className="relative flex-1 h-full rounded-lg border border-rule bg-gradient-to-b from-[#f3ecda] to-[#efe6cf] shadow-sm overflow-hidden"
        >
          {error ? (
            <div className="grid h-full place-items-center p-6 text-center">
              <div className="space-y-2">
                <p className="text-red-700 font-medium m-0">שגיאה בטעינת הגרף</p>
                <p className="text-ink-muted text-sm m-0">{error.message}</p>
                <Button variant="outline" size="sm" onClick={() => active.refetch()}>
                  נסה שוב
                </Button>
              </div>
            </div>
          ) : active.isLoading && !data ? (
            <div className="grid h-full place-items-center text-sm text-ink-muted">
              טוען גרף…
            </div>
          ) : data && data.nodes.length === 0 ? (
            <div className="grid h-full place-items-center text-sm text-ink-muted">
              אין נקודות התואמות לסינון.
            </div>
          ) : (
            <GraphCanvas
              data={data}
              selectedId={selectedNode?.id ?? null}
              onNodeClick={handleNodeClick}
              colorBy={controls.colorBy}
              sizeBy={controls.sizeBy}
            />
          )}

          {isFocused && (
            <div className="absolute top-3 start-3 flex items-center gap-2">
              <Button
                variant="outline"
                size="sm"
                onClick={backToFull}
                className="bg-surface/90 backdrop-blur"
              >
                ← חזרה לגרף המלא
              </Button>
              <div className="flex items-center gap-1.5 rounded-md bg-surface/90 backdrop-blur px-2 py-1 text-xs text-ink-muted">
                <span>עומק</span>
                <input
                  type="range"
                  min={1}
                  max={3}
                  step={1}
                  value={depth}
                  onChange={(e) => setDepth(Number(e.target.value))}
                  className="w-16 accent-gold"
                  aria-label="עומק שכנים"
                />
                <span className="tabular-nums">{depth}</span>
              </div>
            </div>
          )}

          <Legend colorBy={controls.colorBy} />

          {data ? (
            <div className="absolute bottom-3 start-3 rounded-full bg-surface/70 backdrop-blur px-2.5 py-1 text-[0.72rem] text-ink-muted tabular-nums">
              {data.nodes.length} צמתים מוצגים
            </div>
          ) : null}
        </div>

        {selectedNode ? (
          <GraphNodePanel node={selectedNode} onClose={() => setSelectedNode(null)} />
        ) : showRanking ? (
          <RankingPanel nodes={full.data!.nodes} onPick={handleNodeClick} />
        ) : null}
      </div>
    </div>
  );
}

function RankingPanel({
  nodes,
  onPick,
}: {
  nodes: GraphNode[];
  onPick: (n: GraphNode) => void;
}) {
  const precedents = nodes.filter((n) => n.type === "precedent");
  const byPagerank = [...precedents]
    .filter((n) => n.pagerank != null)
    .sort((a, b) => (b.pagerank ?? 0) - (a.pagerank ?? 0))
    .slice(0, 12);
  const byBetweenness = [...precedents]
    .filter((n) => n.betweenness != null)
    .sort((a, b) => (b.betweenness ?? 0) - (a.betweenness ?? 0))
    .slice(0, 12);

  const communities = new Set(
    nodes.map((n) => n.community).filter((c) => c != null),
  ).size;

  return (
    <Card className="bg-surface border-rule shadow-sm w-[300px] shrink-0 max-h-full overflow-y-auto">
      <CardContent className="p-4 space-y-4">
        <div className="text-[0.8rem] font-semibold text-navy border-b border-rule-soft pb-2">
          אנליטיקה
        </div>
        <Tabs defaultValue="pagerank">
          <TabsList className="w-full">
            <TabsTrigger value="pagerank" className="flex-1">
              המשפיעות
            </TabsTrigger>
            <TabsTrigger value="betweenness" className="flex-1">
              גשרים
            </TabsTrigger>
          </TabsList>
          <TabsContent value="pagerank">
            <RankList items={byPagerank} metric="pagerank" onPick={onPick} />
          </TabsContent>
          <TabsContent value="betweenness">
            <RankList items={byBetweenness} metric="betweenness" onPick={onPick} />
          </TabsContent>
        </Tabs>
        {communities > 0 ? (
          <div className="flex items-center gap-2 border-t border-rule-soft pt-3 text-sm text-ink-soft">
            אשכולות:
            <b className="text-navy text-lg tabular-nums">{communities}</b>
          </div>
        ) : null}
      </CardContent>
    </Card>
  );
}

function RankList({
  items,
  metric,
  onPick,
}: {
  items: GraphNode[];
  metric: "pagerank" | "betweenness";
  onPick: (n: GraphNode) => void;
}) {
  if (items.length === 0) {
    return <p className="text-ink-muted text-xs mt-3">אין נתונים.</p>;
  }
  return (
    <ol className="mt-2">
      {items.map((n, i) => (
        <li key={n.id} className="border-b border-rule-soft last:border-b-0">
          <button
            type="button"
            onClick={() => onPick(n)}
            className="flex w-full items-baseline gap-2 px-1 py-1.5 text-start text-sm hover:bg-gold-wash rounded"
          >
            <span className="w-4 shrink-0 text-ink-muted text-xs tabular-nums">
              {i + 1}
            </span>
            <span className="truncate text-ink-soft">{n.label}</span>
            <span className="ms-auto text-gold-deep font-semibold text-xs tabular-nums shrink-0">
              {((n[metric] ?? 0) * 100).toFixed(0)}
            </span>
          </button>
        </li>
      ))}
    </ol>
  );
}

const LEGENDS: Record<ColorBy, { color: string; label: string }[]> = {
  type: [
    { color: "#1e3a5f", label: "פסיקה" },
    { color: "#b45309", label: "הלכה" },
    { color: "#a97d3a", label: "נושא" },
    { color: "#475569", label: "תחום" },
  ],
  practice_area: [
    { color: PA_COLORS.rishuy_uvniya, label: "רישוי ובנייה" },
    { color: PA_COLORS.betterment_levy, label: "היטל השבחה" },
    { color: PA_COLORS.compensation_197, label: "פיצויים" },
  ],
  precedent_level: [
    { color: LEVEL_COLORS["עליון"], label: "עליון" },
    { color: LEVEL_COLORS["מנהלי"], label: "מנהלי" },
    { color: LEVEL_COLORS["ועדת_ערר_מחוזית"], label: "ועדת ערר" },
  ],
  community: [
    { color: COMMUNITY_PALETTE[0], label: "אשכול עיקרי" },
    { color: COMMUNITY_PALETTE[1], label: "אשכול נוסף" },
    { color: COMMUNITY_PALETTE[2], label: "אשכול נוסף" },
  ],
  recency: [
    { color: "rgb(100,116,139)", label: "ישן (1994)" },
    { color: "rgb(169,125,58)", label: "עדכני (2026)" },
  ],
};

const EDGE_LEGEND: { color: string; label: string }[] = [
  { color: "rgba(80,90,110,0.7)", label: "ציטוט" },
  { color: "rgba(169,125,58,0.5)", label: "נושא/תחום" },
  { color: "rgba(47,111,122,0.7)", label: "יומון מסכם" },
  { color: "rgba(180,83,9,0.6)", label: "כלל/אזכור" },
];

function Legend({ colorBy }: { colorBy: ColorBy }) {
  const items = LEGENDS[colorBy] ?? LEGENDS.type;
  return (
    <div className="absolute bottom-3 end-3 flex flex-col gap-1 rounded-md bg-surface/85 backdrop-blur px-3 py-2 text-xs text-ink-muted">
      {items.map((i) => (
        <div key={i.label} className="flex items-center gap-2">
          <span
            className="inline-block size-2.5 rounded-full"
            style={{ backgroundColor: i.color }}
          />
          {i.label}
        </div>
      ))}
      <div className="my-0.5 h-px bg-rule" />
      {EDGE_LEGEND.map((e) => (
        <div key={e.label} className="flex items-center gap-2">
          <span
            className="inline-block w-3.5 h-px"
            style={{ backgroundColor: e.color, height: 2 }}
          />
          {e.label}
        </div>
      ))}
    </div>
  );
}
