<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { api } from "../api/client";
import { useNotesStore, type Note } from "../stores/notes";
import { NOTE_NODE_FILL, type NoteColor } from "../notes/colors";
import NoteEditor from "../components/NoteEditor.vue";

type NodeKind = "note" | "label";

interface GNode {
  id: string;
  title: string;
  color: string;
  kind: NodeKind;
  labelId?: string;
  x: number;
  y: number;
  vx: number;
  vy: number;
}
interface GEdge {
  source: string;
  target: string;
  kind?: string;
}

const WIDTH = 1000;
const HEIGHT = 700;

const router = useRouter();
const notes = useNotesStore();
const allNodes = ref<GNode[]>([]);
const edges = ref<GEdge[]>([]);
const loading = ref(true);
const error = ref("");
const editing = ref<Note | null>(null);
// Unlinked notes float in the space by default — the graph is a gentle overview,
// not a links-only surface. Label hubs are on by default so tags cluster notes.
const showAll = ref(true);
const showLabels = ref(true);

const svgRef = ref<SVGSVGElement | null>(null);
const gRef = ref<SVGGElement | null>(null);

// Pan/zoom applied to the inner <g>.
const scale = ref(1);
const tx = ref(0);
const ty = ref(0);

let frame = 0;
let raf = 0;

// Label membership edges drop out when the label hubs are hidden.
const visibleEdges = computed(() =>
  showLabels.value ? edges.value : edges.value.filter((e) => e.kind !== "label"),
);

const connectedIds = computed(() => {
  const s = new Set<string>();
  for (const e of visibleEdges.value) {
    s.add(e.source);
    s.add(e.target);
  }
  return s;
});

// Hide label hubs when toggled off; otherwise show everything (unlinked notes
// float too) unless "show unlinked" is off, in which case keep only connected nodes.
const activeNodes = computed(() =>
  allNodes.value.filter((n) => {
    if (n.kind === "label" && !showLabels.value) return false;
    if (showAll.value) return true;
    return connectedIds.value.has(n.id);
  }),
);
const activeIds = computed(() => new Set(activeNodes.value.map((n) => n.id)));

const edgeLines = computed(() => {
  const byId = new Map(allNodes.value.map((n) => [n.id, n]));
  const out: { x1: number; y1: number; x2: number; y2: number; label: boolean }[] = [];
  for (const e of visibleEdges.value) {
    if (!activeIds.value.has(e.source) || !activeIds.value.has(e.target)) continue;
    const s = byId.get(e.source);
    const t = byId.get(e.target);
    if (s && t) out.push({ x1: s.x, y1: s.y, x2: t.x, y2: t.y, label: e.kind === "label" });
  }
  return out;
});

function fill(color: string): string {
  return NOTE_NODE_FILL[color as NoteColor] ?? NOTE_NODE_FILL.default;
}

async function loadGraph() {
  loading.value = true;
  error.value = "";
  try {
    const res = await api.get<{
      nodes: { id: string; title: string; color: string; kind: NodeKind; label_id?: string }[];
      edges: GEdge[];
    }>("/api/graph");
    const cx = WIDTH / 2;
    const cy = HEIGHT / 2;
    const count = Math.max(res.nodes.length, 1);
    allNodes.value = res.nodes.map((n, i) => {
      const angle = (i / count) * Math.PI * 2;
      return {
        id: n.id,
        title: n.title,
        color: n.color,
        kind: n.kind,
        labelId: n.label_id,
        x: cx + Math.cos(angle) * 220 + (Math.random() - 0.5) * 40,
        y: cy + Math.sin(angle) * 220 + (Math.random() - 0.5) * 40,
        vx: 0,
        vy: 0,
      };
    });
    edges.value = res.edges;
  } catch (e) {
    error.value = (e as { error?: string }).error ?? "Couldn't load the graph.";
    allNodes.value = [];
    edges.value = [];
  } finally {
    loading.value = false;
  }
  reheat();
}

function reheat() {
  frame = 0;
  cancelAnimationFrame(raf);
  raf = 0;
  if (activeNodes.value.length) simulate();
}

function simulate() {
  const list = activeNodes.value;
  const cx = WIDTH / 2;
  const cy = HEIGHT / 2;
  const byId = new Map(list.map((n) => [n.id, n]));

  for (let i = 0; i < list.length; i++) {
    for (let j = i + 1; j < list.length; j++) {
      const a = list[i];
      const b = list[j];
      let dx = a.x - b.x;
      let dy = a.y - b.y;
      let d2 = dx * dx + dy * dy;
      if (d2 < 0.01) {
        d2 = 0.01;
        dx = Math.random();
        dy = Math.random();
      }
      const d = Math.sqrt(d2);
      const force = 6000 / d2;
      const fx = (dx / d) * force;
      const fy = (dy / d) * force;
      a.vx += fx;
      a.vy += fy;
      b.vx -= fx;
      b.vy -= fy;
    }
  }

  for (const e of visibleEdges.value) {
    const s = byId.get(e.source);
    const t = byId.get(e.target);
    if (!s || !t) continue;
    const dx = t.x - s.x;
    const dy = t.y - s.y;
    const d = Math.sqrt(dx * dx + dy * dy) || 0.01;
    // Label-membership springs sit a touch longer so hubs ring their notes.
    const rest = e.kind === "label" ? 150 : 130;
    const diff = (d - rest) * 0.02;
    const fx = (dx / d) * diff;
    const fy = (dy / d) * diff;
    s.vx += fx;
    s.vy += fy;
    t.vx -= fx;
    t.vy -= fy;
  }

  for (const n of list) {
    if (n === dragNode) {
      n.vx = 0;
      n.vy = 0;
      continue; // pinned to the cursor while dragging
    }
    n.vx += (cx - n.x) * 0.002;
    n.vy += (cy - n.y) * 0.002;
    n.vx *= 0.85;
    n.vy *= 0.85;
    n.x += n.vx;
    n.y += n.vy;
  }

  frame++;
  // Keep running while cooling, or indefinitely while a node is being dragged.
  raf = frame < 400 || dragNode ? requestAnimationFrame(simulate) : 0;
}

// --- pointer interaction: drag a node, pan the background, wheel-zoom ---
let dragNode: GNode | null = null;
let dragMoved = false;
let downPos = { x: 0, y: 0 };
let panning = false;
let panLast = { x: 0, y: 0 };

function toLocal(el: SVGGraphicsElement | null, e: MouseEvent) {
  const ctm = el?.getScreenCTM();
  if (!ctm) return { x: 0, y: 0 };
  const p = new DOMPoint(e.clientX, e.clientY).matrixTransform(ctm.inverse());
  return { x: p.x, y: p.y };
}

function onNodeDown(n: GNode, e: MouseEvent) {
  e.stopPropagation();
  dragNode = n;
  dragMoved = false;
  downPos = { x: e.clientX, y: e.clientY };
  reheat();
  window.addEventListener("mousemove", onMove);
  window.addEventListener("mouseup", onUp);
}

function onBgDown(e: MouseEvent) {
  panning = true;
  panLast = toLocal(svgRef.value, e);
  window.addEventListener("mousemove", onMove);
  window.addEventListener("mouseup", onUp);
}

function onMove(e: MouseEvent) {
  if (dragNode) {
    if (Math.hypot(e.clientX - downPos.x, e.clientY - downPos.y) > 3) dragMoved = true;
    const p = toLocal(gRef.value, e);
    dragNode.x = p.x;
    dragNode.y = p.y;
  } else if (panning) {
    const p = toLocal(svgRef.value, e);
    tx.value += p.x - panLast.x;
    ty.value += p.y - panLast.y;
    panLast = p;
  }
}

function onUp() {
  window.removeEventListener("mousemove", onMove);
  window.removeEventListener("mouseup", onUp);
  const node = dragNode;
  const moved = dragMoved;
  dragNode = null;
  panning = false;
  // A press without a drag is a click.
  if (node && !moved) clickNode(node);
}

function clickNode(n: GNode) {
  // Label hub → jump to that label's board lens (one space, many lenses).
  if (n.kind === "label" && n.labelId) {
    void router.push(`/label/${n.labelId}`);
    return;
  }
  void openNode(n);
}

function onWheel(e: WheelEvent) {
  e.preventDefault();
  const vb = toLocal(svgRef.value, e);
  const gx = (vb.x - tx.value) / scale.value;
  const gy = (vb.y - ty.value) / scale.value;
  const factor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
  scale.value = Math.min(Math.max(scale.value * factor, 0.3), 3);
  tx.value = vb.x - gx * scale.value;
  ty.value = vb.y - gy * scale.value;
}

function resetView() {
  scale.value = 1;
  tx.value = 0;
  ty.value = 0;
}

function toggleAll() {
  showAll.value = !showAll.value;
  reheat();
}

function toggleLabels() {
  showLabels.value = !showLabels.value;
  reheat();
}

async function openNode(n: GNode) {
  const found = notes.items.find((x) => x.id === n.id);
  editing.value = found ?? (await notes.fetchOne(n.id));
}
function closeEditor() {
  editing.value = null;
}
async function onNavigate(id: string) {
  const found = notes.items.find((x) => x.id === id);
  editing.value = found ?? (await notes.fetchOne(id));
}

onMounted(loadGraph);
onBeforeUnmount(() => {
  cancelAnimationFrame(raf);
  window.removeEventListener("mousemove", onMove);
  window.removeEventListener("mouseup", onUp);
});
</script>

<template>
  <div class="flex h-full flex-col p-4">
    <div class="mb-3 flex flex-wrap items-center justify-between gap-3">
      <h1 class="text-lg font-semibold">Graph</h1>
      <div class="flex items-center gap-3 text-sm">
        <label class="flex cursor-pointer items-center gap-1.5 text-neutral-600 dark:text-neutral-300">
          <input type="checkbox" class="accent-brand" :checked="showLabels" @change="toggleLabels" />
          Show labels
        </label>
        <label class="flex cursor-pointer items-center gap-1.5 text-neutral-600 dark:text-neutral-300">
          <input type="checkbox" class="accent-brand" :checked="showAll" @change="toggleAll" />
          Show unlinked notes
        </label>
        <button
          type="button"
          class="rounded-md border border-neutral-300 px-2 py-1 text-xs hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:hover:bg-neutral-800"
          @click="resetView"
        >
          Reset view
        </button>
      </div>
    </div>

    <div v-if="loading" class="py-24 text-center text-sm text-neutral-400">Loading graph…</div>

    <div v-else-if="error" class="py-24 text-center">
      <h2 class="text-lg font-semibold text-neutral-700 dark:text-neutral-200">Couldn't load the graph</h2>
      <p class="mt-1 text-sm text-neutral-400">{{ error }}</p>
      <button
        type="button"
        class="mt-3 rounded-md border border-neutral-300 px-3 py-1.5 text-sm hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:hover:bg-neutral-800"
        @click="loadGraph"
      >
        Retry
      </button>
    </div>

    <div v-else-if="allNodes.length === 0" class="py-24 text-center">
      <h2 class="text-lg font-semibold text-neutral-700 dark:text-neutral-200">No notes yet</h2>
      <p class="mt-1 text-sm text-neutral-400">
        Create notes and link them with
        <span class="font-mono text-brand-700 dark:text-brand">[[Note title]]</span> to see them here.
      </p>
    </div>

    <div v-else-if="activeNodes.length === 0" class="py-24 text-center">
      <h2 class="text-lg font-semibold text-neutral-700 dark:text-neutral-200">Nothing connected yet</h2>
      <p class="mt-1 text-sm text-neutral-400">
        Link notes with <span class="font-mono text-brand-700 dark:text-brand">[[Note title]]</span>, add
        <span class="font-mono text-brand-700 dark:text-brand">#tags</span>, or
        <button type="button" class="text-brand-700 underline dark:text-brand" @click="toggleAll">
          show all notes
        </button>.
      </p>
    </div>

    <div
      v-else
      class="min-h-[500px] flex-1 overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-950"
    >
      <svg
        ref="svgRef"
        :viewBox="`0 0 ${WIDTH} ${HEIGHT}`"
        class="h-full w-full cursor-grab select-none touch-none"
        preserveAspectRatio="xMidYMid meet"
        @mousedown="onBgDown"
        @wheel="onWheel"
      >
        <g ref="gRef" :transform="`translate(${tx} ${ty}) scale(${scale})`">
          <line
            v-for="(l, i) in edgeLines"
            :key="`e${i}`"
            :x1="l.x1"
            :y1="l.y1"
            :x2="l.x2"
            :y2="l.y2"
            class="stroke-neutral-300 dark:stroke-neutral-700"
            :stroke-width="l.label ? 1 : 1.5"
            :stroke-dasharray="l.label ? '3 3' : undefined"
          />
          <g v-for="n in activeNodes" :key="n.id" class="cursor-pointer" @mousedown="onNodeDown(n, $event)">
            <!-- Label hubs read as a larger ringed node so tags stand out from notes. -->
            <circle
              v-if="n.kind === 'label'"
              :cx="n.x"
              :cy="n.y"
              r="12"
              :fill="fill(n.color)"
              fill-opacity="0.9"
              class="stroke-neutral-50 dark:stroke-neutral-950"
              stroke-width="3"
            />
            <circle
              v-else
              :cx="n.x"
              :cy="n.y"
              r="8"
              :fill="fill(n.color)"
              class="stroke-neutral-50 dark:stroke-neutral-950"
              stroke-width="1.5"
            />
            <text
              :x="n.x"
              :y="n.kind === 'label' ? n.y - 17 : n.y - 13"
              text-anchor="middle"
              :class="
                n.kind === 'label'
                  ? 'fill-neutral-700 text-[12px] font-semibold dark:fill-neutral-100'
                  : 'fill-neutral-600 text-[12px] dark:fill-neutral-300'
              "
            >
              {{ n.title }}
            </text>
          </g>
        </g>
      </svg>
    </div>

    <template v-if="editing">
      <NoteEditor :note="editing" @close="closeEditor" @navigate="onNavigate" />
    </template>
  </div>
</template>
