/**
 * Safe-area (window inset) plumbing.
 *
 * TRACES: UR-066 | IR-031, DR-112
 *
 * ## Why this exists
 *
 * `MainActivity` calls `enableEdgeToEdge()`, and the app targets SDK 36 — from
 * SDK 35 edge-to-edge is mandatory and from SDK 36 the opt-out is ignored — so
 * the Tauri WebView always spans the **entire window**, underneath the status
 * bar, the navigation/gesture bar and the display cutout.
 *
 * CSS cannot discover that on its own:
 *
 *  - `env(safe-area-inset-*)` resolves to `0px` unless the viewport declares
 *    `viewport-fit=cover` (see `src/app.html`), and
 *  - even then, Android WebView only maps the **display cutout** into
 *    `env(safe-area-inset-*)`. The status bar and the navigation bar are never
 *    reported. Unlike iOS Safari, there is no CSS-visible system-bar inset.
 *
 * So native reads the real `WindowInsets` (`systemBars() | displayCutout()`)
 * and pushes them in as CSS custom properties; `src/app.css` folds them
 * together with `env()` via `max()` so iOS/desktop keep working unchanged:
 *
 * ```css
 * --safe-bottom: max(env(safe-area-inset-bottom, 0px), var(--jt-inset-bottom, 0px));
 * ```
 *
 * Two delivery paths, because either alone is insufficient:
 *
 *  - **push** — `WindowInsetsBridge` evaluates JS into the WebView on every
 *    inset change (rotation, nav-mode switch, PiP enter/exit). Needed because
 *    insets change after load.
 *  - **pull** — `initSafeArea()` reads `window.AndroidInsets.get()` at startup.
 *    Needed because the first inset pass usually lands *before* the SvelteKit
 *    document exists, and a page load wipes any inline style native had set.
 */

/** Window insets in CSS pixels, one per edge. */
export interface SafeAreaInsets {
  top: number;
  right: number;
  bottom: number;
  left: number;
}

/** No insets — the desktop/dev default. */
export const ZERO_INSETS: SafeAreaInsets = { top: 0, right: 0, bottom: 0, left: 0 };

/** DOM event native fires after pushing a new set of insets. */
export const INSETS_CHANGED_EVENT = "jellytau-insets-changed";

const EDGES = ["top", "right", "bottom", "left"] as const;

/** The native @JavascriptInterface installed by MainActivity (Android only). */
interface AndroidInsetsBridge {
  /** JSON `{top,right,bottom,left}` in CSS pixels. */
  get(): string;
}

declare global {
  interface Window {
    AndroidInsets?: AndroidInsetsBridge;
  }
}

/**
 * Coerce one edge to a non-negative finite number.
 *
 * Anything else becomes 0 rather than propagating: a `NaN` would serialise to
 * `"NaNpx"`, which invalidates the whole declaration and silently restores the
 * original bug.
 */
function edge(value: unknown): number {
  const n = typeof value === "string" ? Number(value) : value;
  if (typeof n !== "number" || !Number.isFinite(n) || n < 0) return 0;
  return n;
}

/**
 * Parse a native inset payload (JSON string or already-decoded object).
 * Returns `null` when the input is not an inset payload at all, so callers can
 * distinguish "no insets reported" from "insets are all zero".
 */
export function parseNativeInsets(raw: unknown): SafeAreaInsets | null {
  let value = raw;

  if (typeof value === "string") {
    try {
      value = JSON.parse(value);
    } catch {
      return null;
    }
  }

  if (typeof value !== "object" || value === null || Array.isArray(value)) return null;

  const record = value as Record<string, unknown>;
  if (!EDGES.some((e) => e in record)) return null;

  return {
    top: edge(record.top),
    right: edge(record.right),
    bottom: edge(record.bottom),
    left: edge(record.left),
  };
}

/** The CSS custom properties for a set of insets. */
export function safeAreaCssVars(insets: SafeAreaInsets): Record<string, string> {
  return Object.fromEntries(EDGES.map((e) => [`--jt-inset-${e}`, `${insets[e]}px`]));
}

/** Write the inset custom properties onto an element (normally `<html>`). */
export function applySafeAreaInsets(target: HTMLElement, insets: SafeAreaInsets): void {
  for (const [name, value] of Object.entries(safeAreaCssVars(insets))) {
    target.style.setProperty(name, value);
  }
}

/**
 * Read the current insets from the native bridge, or `null` when there is none.
 *
 * Never throws. WebView can hand JS a *stale proxy* after a page load — the
 * injected object stays truthy while its methods vanish (the exact failure that
 * broke the background-audio toggle, see `MainActivity.configureWebViewForMedia`).
 * Layout init must survive that.
 */
export function readNativeInsets(): SafeAreaInsets | null {
  if (typeof window === "undefined") return null;

  const bridge = window.AndroidInsets;
  if (!bridge || typeof bridge.get !== "function") return null;

  try {
    return parseNativeInsets(bridge.get());
  } catch (err) {
    console.warn("[SafeArea] AndroidInsets bridge unusable:", err);
    return null;
  }
}

/**
 * Prime the safe-area custom properties and keep them current.
 *
 * Call once, early in the root layout's `onMount` (synchronously — before any
 * `await`, so the very first paint is already inset-correct). Returns a
 * teardown that unsubscribes.
 *
 * With no native bridge this is a near no-op: it deliberately does NOT write
 * `0px`, so the `env(safe-area-inset-*)` half of the `max()` still wins on iOS
 * and desktop.
 */
export function initSafeArea(target?: HTMLElement): () => void {
  if (typeof window === "undefined") return () => {};

  const el = target ?? document.documentElement;

  const sync = () => {
    const insets = readNativeInsets();
    if (insets) applySafeAreaInsets(el, insets);
  };

  sync();
  window.addEventListener(INSETS_CHANGED_EVENT, sync);
  return () => window.removeEventListener(INSETS_CHANGED_EVENT, sync);
}
