import { defineTrait } from '../define.js';
import { announce, getRegion } from './announcer-stage.js';

/**
 * `announcer` — aria-live mirror for AT (assistive tech) listeners.
 *
 * Every audio/haptic/visual trait above WCAG 4.1.3 (Status Messages)
 * leaves AT users behind: count-up tickers, validation flips, typewriter
 * reveals — none of these announce themselves. `announcer` is the missing
 * pair. It hooks any host event into one of two body-level singleton
 * `aria-live` regions; AT speaks the message, sighted users see nothing.
 *
 * Triggers
 * --------
 *   data-announce-on        the event name to listen for on the host
 *   data-announce-message   the message to write. Supports `{detail}`
 *                           which substitutes `event.detail` — useful for
 *                           validation errors / count-up final values.
 *                           If `event.detail` is an object, the
 *                           `message` field is preferred when present.
 *   data-announce-priority  'polite' (default) | 'assertive'
 *   data-announce-throttle  minimum ms between two announcements from
 *                           the same trait instance (default 1000).
 *                           Burst-protect against rapid-fire updates.
 *
 * Singleton regions
 * -----------------
 * Two body-level regions are created lazily on first announce — one
 * polite, one assertive. Both are visually clipped via `.adia-sr-only`
 * so sighted users never see them, but stay in the accessibility tree
 * so screen readers announce the contents. Multiple announcer traits
 * share the same two regions; cleanup leaves them in place.
 *
 * Why a microtask blank-then-write
 * --------------------------------
 * `aria-atomic="true"` re-reads the full content on every change, but
 * many screen readers skip a write if the new value equals the previous
 * one. The stage helper blanks the region in a microtask, then writes
 * the message — defeats the diff and forces a re-announce.
 */
export const announcer = defineTrait({
  name: 'announcer',
  category: 'audio-haptics-sensory',
  description: 'aria-live mirror for AT — announces host state changes via singleton polite/assertive regions',
  attributes: ['data-announcer-active'],
  events: ['announcement-made'],
  config: [
    'data-announce-on',
    'data-announce-message',
    'data-announce-priority',
    'data-announce-throttle',
  ],
  setup({ host }) {
    const eventName = host.getAttribute('data-announce-on');
    if (!eventName) {
      // No trigger configured — quietly no-op. The trait still flips its
      // active flag so authors can debug "is the trait wired?" via DevTools.
      host.setAttribute('data-announcer-active', '');
      return () => {
        host.removeAttribute('data-announcer-active');
      };
    }

    const throttleMs = parseInt(host.getAttribute('data-announce-throttle'), 10);
    const throttle = Number.isFinite(throttleMs) && throttleMs >= 0 ? throttleMs : 1000;

    // `null` so the first announce always wins regardless of throttle —
    // the throttle is "minimum gap between two announcements," not a
    // delay before the first one. Subsequent events compare against
    // the previous timestamp.
    let lastAnnouncedAt = null;

    function readPriority() {
      const raw = host.getAttribute('data-announce-priority');
      return raw === 'assertive' ? 'assertive' : 'polite';
    }

    function resolveMessage(event) {
      // Priority for message resolution:
      //   1. event.detail.message (object detail with explicit field)
      //   2. event.detail (scalar / serializable)
      //   3. data-announce-message (with `{detail}` placeholder)
      //   4. host.getAttribute('data-validation-message') as last-ditch
      //      fallback — pairs cleanly with the validation trait.
      const tmpl = host.getAttribute('data-announce-message');
      const detail = event?.detail;

      if (tmpl) {
        const detailStr = detail == null
          ? ''
          : (typeof detail === 'object'
              ? (detail.message ?? JSON.stringify(detail))
              : String(detail));
        return tmpl.includes('{detail}') ? tmpl.replace(/\{detail\}/g, detailStr) : tmpl;
      }

      if (detail != null) {
        if (typeof detail === 'object') {
          if (typeof detail.message === 'string') return detail.message;
          // Fall through — opaque object detail without a message field
          // isn't useful to AT. Return null to skip.
          return null;
        }
        return String(detail);
      }

      const fallback = host.getAttribute('data-validation-message');
      return fallback || null;
    }

    function onTrigger(event) {
      const message = resolveMessage(event);
      if (!message) return;

      const now = (typeof performance !== 'undefined' && performance.now)
        ? performance.now()
        : Date.now();
      if (throttle > 0 && lastAnnouncedAt != null && now - lastAnnouncedAt < throttle) return;
      lastAnnouncedAt = now;

      const priority = readPriority();
      const ok = announce(message, priority);
      if (!ok) return;

      host.dispatchEvent(new CustomEvent('announcement-made', {
        bubbles: true,
        detail: { message, priority },
      }));
    }

    host.addEventListener(eventName, onTrigger);
    host.setAttribute('data-announcer-active', '');

    // Pre-warm both regions so the first real announce doesn't pay
    // the create-+-microtask-gap cost serially. Cheap (two empty divs)
    // and idempotent if any other announcer instance already did it.
    getRegion('polite');
    if (readPriority() === 'assertive') getRegion('assertive');

    return () => {
      host.removeEventListener(eventName, onTrigger);
      host.removeAttribute('data-announcer-active');
    };
  },
});
