/**
 * Shared aria-live regions for the `announcer` trait.
 *
 * Strategy: two body-level singleton `<div>` elements — one with
 * `aria-live="polite"`, one with `aria-live="assertive"` — both with
 * `aria-atomic="true"` so AT re-reads the full message on every change
 * (instead of diffing the previous value, which can drop short
 * messages or read fragments). The regions are visually clipped via
 * the `.adia-sr-only` class so sighted users never see them, but
 * remain in the accessibility tree so screen readers announce their
 * contents.
 *
 * Both regions are created lazily on first `getRegion()` call and
 * never torn down — they're cheap (two empty divs + one `<style>`)
 * and many traits can share them. Multiple announcer instances on
 * the same page route through the same two regions.
 *
 * The clip-path / sr-only recipe is the canonical "visually hidden,
 * AT-visible" pattern: zero clip-path, position absolute off-screen,
 * 1px size with overflow hidden. WCAG-friendly; works in every
 * browser the project supports (Chromium 125+, Safari 18.0+,
 * Firefox 129+).
 *
 * Notes:
 * - happy-dom does not actually announce anything — these tests assert
 *   the *contents* of the region, not the AT speech. Real-browser
 *   verification happens on the demo page.
 * - The regions are created on first call from any module, so importing
 *   this file is side-effect-free until something invokes `getRegion()`.
 */

const POLITE_ID = 'adia-live-polite';
const ASSERTIVE_ID = 'adia-live-assertive';
const STYLE_ID = 'adia-live-styles';

let politeRegion = null;
let assertiveRegion = null;
let stylesInjected = false;

/**
 * Get (or lazily create) one of the two singleton live regions.
 * `priority` selects which region: `'polite'` for non-urgent updates
 * (the default), `'assertive'` for urgent messages that should
 * interrupt current speech (validation errors, destructive
 * confirmations).
 *
 * Returns the region element. If `document` is unavailable (SSR,
 * worker), returns `null` — callers should bail.
 */
export function getRegion(priority = 'polite') {
  if (typeof document === 'undefined') return null;
  ensureStyles();

  if (priority === 'assertive') {
    if (assertiveRegion && assertiveRegion.isConnected) return assertiveRegion;
    const existing = document.getElementById(ASSERTIVE_ID);
    if (existing) {
      assertiveRegion = existing;
      return assertiveRegion;
    }
    assertiveRegion = createRegion(ASSERTIVE_ID, 'assertive');
    return assertiveRegion;
  }

  if (politeRegion && politeRegion.isConnected) return politeRegion;
  const existing = document.getElementById(POLITE_ID);
  if (existing) {
    politeRegion = existing;
    return politeRegion;
  }
  politeRegion = createRegion(POLITE_ID, 'polite');
  return politeRegion;
}

function createRegion(id, liveValue) {
  const el = document.createElement('div');
  el.id = id;
  el.setAttribute('aria-live', liveValue);
  el.setAttribute('aria-atomic', 'true');
  el.setAttribute('role', liveValue === 'assertive' ? 'alert' : 'status');
  el.className = 'adia-sr-only';
  // Defensive inline styles — if the consumer's CSS strips the class
  // or the stylesheet failed to inject, the region must still be
  // visually hidden. Belt-and-suspenders for production reliability.
  el.style.cssText =
    'position:absolute;width:1px;height:1px;padding:0;margin:-1px;' +
    'overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
  document.body.appendChild(el);
  return el;
}

/**
 * Inject the `.adia-sr-only` class once. Idempotent. The class is
 * the canonical "visually hidden, AT-visible" recipe — zero in the
 * visual layout, full presence in the accessibility tree.
 */
function ensureStyles() {
  if (stylesInjected) return;
  if (typeof document === 'undefined') return;
  if (document.getElementById(STYLE_ID)) {
    stylesInjected = true;
    return;
  }
  const styleEl = document.createElement('style');
  styleEl.id = STYLE_ID;
  styleEl.textContent = `
    .adia-sr-only {
      position: absolute !important;
      width: 1px !important;
      height: 1px !important;
      padding: 0 !important;
      margin: -1px !important;
      overflow: hidden !important;
      clip: rect(0, 0, 0, 0) !important;
      white-space: nowrap !important;
      border: 0 !important;
    }
  `;
  document.head?.appendChild(styleEl);
  stylesInjected = true;
}

/**
 * Write a message into the chosen live region. `aria-atomic="true"`
 * means AT re-reads the full new value on every change — but if the
 * caller writes the *same* string back-to-back, some screen readers
 * skip the re-announce. We blank the region first via a microtask
 * gap, then write the message, to defeat that diffing.
 */
export function announce(message, priority = 'polite') {
  const region = getRegion(priority);
  if (!region) return false;
  // Blank then write. The microtask ensures AT sees a true content
  // change even when the same message fires twice in a row.
  region.textContent = '';
  // queueMicrotask (or a microtask via Promise.resolve) keeps this
  // synchronous-feeling for callers but gives the AT layer a chance
  // to register the empty state before the new message lands.
  queueMicrotask(() => {
    region.textContent = String(message ?? '');
  });
  return true;
}

/**
 * Test-only: tear down both regions + the style block so test cases
 * don't leak state across each other. Not part of the public API.
 */
export function _resetRegions() {
  if (politeRegion && politeRegion.parentNode) politeRegion.remove();
  if (assertiveRegion && assertiveRegion.parentNode) assertiveRegion.remove();
  const styleEl = typeof document !== 'undefined' ? document.getElementById(STYLE_ID) : null;
  if (styleEl) styleEl.remove();
  politeRegion = null;
  assertiveRegion = null;
  stylesInjected = false;
}
