/**
 * Wires a persistent scroll container to the per-route scroll memory.
 *
 * The decision logic is pure and lives in `scrollRestore.ts`; this is the thin
 * DOM/SvelteKit half. Call it once at component init (SvelteKit's navigation
 * hooks must be registered during initialisation, not from `onMount`), passing
 * a getter for the element — the element itself is bound later, so a getter is
 * the only way to hand it over from the top of `<script>`.
 *
 *   let scroller: HTMLElement | undefined = $state();
 *   useScrollRestore(() => scroller, "library");
 *   …
 *   <div bind:this={scroller} class="flex-1 overflow-y-auto">
 *
 * Memories are keyed by container id and held at module scope, not per call.
 * Two containers must never share one (the root, home and library scrollers
 * hold different content for the same URL, so a shared map would restore one
 * into another) — but a container that *remounts* has to find its offsets again
 * when it comes back. The home scroller is destroyed on every navigation away,
 * so a memory owned by the component instance would be empty on return and Back
 * could only ever land at the top.
 *
 * TRACES: UR-072 | DR-156
 */

import { beforeNavigate, afterNavigate } from "$app/navigation";
import { tick } from "svelte";
import { ScrollMemory, classifyNavigation, scrollKey } from "./scrollRestore";

/** Container id → its offsets. Outlives the components that mount them. */
const memories = new Map<string, ScrollMemory>();

function memoryFor(containerId: string): ScrollMemory {
  let memory = memories.get(containerId);
  if (!memory) {
    memory = new ScrollMemory();
    memories.set(containerId, memory);
  }
  return memory;
}

/** Forget every container's offsets. For sign-out and tests. */
export function clearScrollMemories(): void {
  memories.clear();
}

export function useScrollRestore(
  getElement: () => HTMLElement | null | undefined,
  containerId: string
): void {
  const memory = memoryFor(containerId);

  // Record where we were before the route changes. `nav.from` is absent on the
  // very first navigation, which is exactly when there is nothing to save.
  beforeNavigate((nav) => {
    const element = getElement();
    if (!element || !nav.from) return;
    memory.save(scrollKey(nav.from.url), element.scrollTop);
  });

  afterNavigate(async (nav) => {
    const target = nav.to;
    if (!target) return;

    const action = memory.decide(scrollKey(target.url), classifyNavigation(nav));
    if (action.kind === "none") return;

    const top = action.kind === "restore" ? action.top : 0;

    // Wait for the new route's markup to be in the DOM before moving the
    // scroller — setting scrollTop past the current content height is clamped,
    // and a reset applied too early is undone by the incoming render.
    await tick();
    const element = getElement();
    if (!element) return;

    element.scrollTop = top;

    // A restore often targets content that is still loading (a library grid
    // fetches after mount), so the offset would clamp to a short page. Re-apply
    // on the next frame, once, which is enough for the common case without
    // fighting a user who has already started scrolling.
    if (action.kind === "restore" && top > 0) {
      requestAnimationFrame(() => {
        const el = getElement();
        if (el && el.scrollTop < top) el.scrollTop = top;
      });
    }
  });
}
