/**
 * tvScrollIntoView — robust scroll-into-view for Samsung Tizen and LG
 * webOS WebViews.
 *
 * Why a custom implementation?
 * ----------------------------
 * Native `Element.scrollIntoView({ behavior, block, inline })` is the
 * accepted way to keep a focused tile on-screen during D-pad
 * navigation, but it is **unreliable** on the TV runtimes Nadiplayer
 * ships to:
 *
 *   • Tizen 4-5  (Chromium 56-69) — `behavior: "smooth"` is a no-op
 *     and `block: "nearest"` mis-computes visibility when
 *     `content-visibility: auto` is in play, so the list "won't
 *     scroll past the visible rows".
 *   • Tizen 6-7  (Chromium 76-94) — improved, but `block: "center"`
 *     occasionally fires against the wrong scrollable ancestor when
 *     React renders a deep flex tree.
 *   • LG webOS 4-6 (Chromium 53-79) — same family of bugs as Tizen.
 *
 * What this util does
 * -------------------
 * 1. Walks up the DOM from the focused node looking for the **closest
 *    scrollable ancestor** (the element whose computed
 *    `overflow-y` is `auto` or `scroll`).
 * 2. Computes the node's offset relative to that ancestor and sets
 *    `ancestor.scrollTop` directly so the node is **centred** in the
 *    viewport. This bypasses Chromium's flaky `scrollIntoView()`
 *    plumbing entirely.
 * 3. Falls back to `node.scrollIntoView({ block: "center" })` if no
 *    scrollable ancestor is found (e.g. when the page itself is the
 *    scrolling context).
 *
 * BUG FIX (2026-01) — "channel list doesn't auto-scroll down when the
 * focus moves past the visible area" on Tizen / webOS. The previous
 * fix flipped `behavior` and `block` to TV-friendly values but still
 * relied on the buggy native API. This util replaces that path
 * completely on TV builds.
 */

/** Returns the nearest ancestor (including the node itself's parent
 *  chain) whose computed `overflow-y` is auto / scroll. The body /
 *  documentElement are excluded — those are handled by the native
 *  fallback path. */
function findScrollableAncestor(node: HTMLElement): HTMLElement | null {
  let cur: HTMLElement | null = node.parentElement;
  while (cur && cur !== document.body && cur !== document.documentElement) {
    const cs = window.getComputedStyle(cur);
    const oy = cs.overflowY;
    if (oy === "auto" || oy === "scroll" || oy === "overlay") {
      // Only count it if it actually has more scroll height than
      // visible height — otherwise it's just a passthrough container.
      if (cur.scrollHeight > cur.clientHeight + 1) return cur;
    }
    cur = cur.parentElement;
  }
  return null;
}

/**
 * Centres `node` inside its nearest scrollable ancestor.
 *
 * @param node The focused element to bring on-screen.
 */
export function tvScrollIntoView(node: HTMLElement): void {
  if (!node) return;
  try {
    const scroller = findScrollableAncestor(node);
    if (scroller) {
      // Use getBoundingClientRect deltas instead of `offsetTop` so we
      // don't have to assume the scrollable ancestor is also the
      // offsetParent (which is often broken with transformed parents).
      const nodeRect = node.getBoundingClientRect();
      const scrollerRect = scroller.getBoundingClientRect();
      const nodeCentre =
        nodeRect.top - scrollerRect.top + scroller.scrollTop + nodeRect.height / 2;
      const targetScroll = nodeCentre - scroller.clientHeight / 2;
      const clamped = Math.max(
        0,
        Math.min(targetScroll, scroller.scrollHeight - scroller.clientHeight),
      );
      // Some Tizen 4 builds silently ignore the setter when called
      // inside a focus event — schedule via rAF so the focus pipeline
      // has fully settled before we mutate scrollTop.
      const apply = () => {
        scroller.scrollTop = clamped;
      };
      apply();
      try {
        window.requestAnimationFrame(apply);
      } catch {
        /* rAF unavailable — single set is fine */
      }
      return;
    }
    // No scrollable ancestor: let the browser try its best.
    node.scrollIntoView({ block: "center", inline: "center" });
  } catch {
    // Absolute last resort — the boolean form is supported back to
    // Chromium v1 so it should never throw, but we still wrap it just
    // in case some weird WebView strips Element.prototype.scrollIntoView.
    try {
      node.scrollIntoView(true);
    } catch {
      /* give up */
    }
  }
}
