// theme.ts — owns the runtime side of the dark-mode toggle.
//
// The pre-paint inline <script> in PWAHead.tsx already sets
// `<html data-theme="dark">` from localStorage[paliad-theme] before the
// stylesheet loads (FOUC-prevention, m/paliad#2). This module owns the
// post-hydration behaviour:
//   - exposes the user-facing toggle helpers (cycleTheme, setThemePref)
//   - re-applies the data-theme attribute when the user changes preference
//   - listens for system prefers-color-scheme changes when pref === "auto"
//
// The pre-paint script and this module MUST share the same storage key
// and the same "auto + system → dark" decision logic. If you change one,
// change the other.

const STORAGE_KEY = "paliad-theme";

export type ThemePref = "auto" | "light" | "dark";

export function getThemePref(): ThemePref {
  const v = localStorage.getItem(STORAGE_KEY);
  if (v === "light" || v === "dark") return v;
  // Anything else (null, "auto", legacy values) collapses to "auto" so a
  // stale or hand-edited entry can't desync the toggle from the rest of
  // the system.
  return "auto";
}

export function setThemePref(pref: ThemePref): void {
  if (pref === "auto") {
    // Remove the key entirely on "auto" so the next read sees a clean
    // null (matching the FOUC script's "no entry → auto" fallback). This
    // also keeps localStorage tidy between users on shared machines.
    localStorage.removeItem(STORAGE_KEY);
  } else {
    localStorage.setItem(STORAGE_KEY, pref);
  }
  applyTheme();
  notifyChange();
}

function effectiveTheme(): "light" | "dark" {
  const pref = getThemePref();
  if (pref === "light" || pref === "dark") return pref;
  return matchMedia && matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light";
}

function applyTheme(): void {
  const t = effectiveTheme();
  if (t === "dark") {
    document.documentElement.dataset.theme = "dark";
  } else {
    delete document.documentElement.dataset.theme;
  }
}

const listeners: Array<() => void> = [];

function notifyChange(): void {
  for (const cb of listeners) cb();
}

// onThemeChange returns an unsubscribe function. Used by Sidebar.tsx's
// theme-toggle button to re-render its icon + label without polling.
export function onThemeChange(cb: () => void): () => void {
  listeners.push(cb);
  return () => {
    const i = listeners.indexOf(cb);
    if (i !== -1) listeners.splice(i, 1);
  };
}

// cycleTheme advances the preference: auto → light → dark → auto.
// Returns the new pref so callers can update aria-label / tooltip.
export function cycleTheme(): ThemePref {
  const next: Record<ThemePref, ThemePref> = {
    auto: "light",
    light: "dark",
    dark: "auto",
  };
  const n = next[getThemePref()];
  setThemePref(n);
  return n;
}

let initialized = false;

export function initTheme(): void {
  if (initialized) return;
  initialized = true;
  // Re-apply on hydration. The pre-paint script already covered the
  // initial frame, but matchMedia may not have been available there
  // (very old browsers, deep iframes), so we settle the state again.
  applyTheme();
  // Listen for OS-level changes — only matters when pref === "auto",
  // but the listener is cheap and the guard inside the handler keeps
  // explicit user choices stable.
  const mq = matchMedia("(prefers-color-scheme: dark)");
  const handler = () => {
    if (getThemePref() === "auto") {
      applyTheme();
      notifyChange();
    }
  };
  if (typeof mq.addEventListener === "function") {
    mq.addEventListener("change", handler);
  } else if (typeof (mq as MediaQueryList & { addListener?: (h: () => void) => void }).addListener === "function") {
    // Older Safari: the deprecated addListener still works.
    (mq as MediaQueryList & { addListener: (h: () => void) => void }).addListener(handler);
  }
}
