// ── Clover element lifecycle ────────────────────────────────────
//
// Owns `elements.create()` per FieldKey and the subsequent `.mount()` calls.
// Decoupled from React reconciliation so iframes persist across renders.
//
// Clover's SDK does not expose unmount/destroy on its elements. Cleanup is
// best-effort: we drop our refs and let GC collect the iframes when their
// host divs are removed from the DOM.

import {
  type CloverElement,
  type CloverElementType,
  type CloverElementStyles,
  type CloverInstance,
  type CloverPaymentRequestData,
  type FieldKey,
  FIELD_KEY_TO_CLOVER_TYPE,
  type MountTargets,
} from './types';

export interface ElementRegistryOptions {
  readonly clover: CloverInstance;
  readonly styles: CloverElementStyles;
  /** Required when creating the `paymentRequestButton` field. */
  readonly paymentRequestData?: CloverPaymentRequestData;
}

export class ElementRegistry {
  private readonly elements = new Map<FieldKey, CloverElement>();
  private readonly options: ElementRegistryOptions;
  private destroyed = false;

  constructor(options: ElementRegistryOptions) {
    this.options = options;
  }

  /**
   * Create a Clover element for each key. Skips keys already created.
   * GPay button creation can throw on devices without PaymentRequest support —
   * caught + logged + skipped, never breaks the rest of the form.
   */
  create(keys: readonly FieldKey[]): this {
    this.assertAlive();
    const cloverElements = this.options.clover.elements();
    for (const key of keys) {
      if (this.elements.has(key)) {
        continue;
      }
      const type: CloverElementType = FIELD_KEY_TO_CLOVER_TYPE[key];
      const data: CloverElementStyles | CloverPaymentRequestData =
        key === 'paymentRequestButton' && this.options.paymentRequestData
          ? this.options.paymentRequestData
          : this.options.styles;
      try {
        const el = cloverElements.create(type, data);
        this.elements.set(key, el);
      } catch (error) {
        console.warn(`[wcp-payment-fields] failed to create element "${key}"`, error);
      }
    }
    return this;
  }

  /**
   * Mount each created element at its target CSS selector. Skips fields whose
   * target is missing or whose host div doesn't exist in the DOM yet.
   */
  mount(targets: MountTargets): this {
    this.assertAlive();
    this.mountIfTargeted('cardNumber', targets.cardNumber);
    this.mountIfTargeted('cardDate', targets.cardDate);
    this.mountIfTargeted('cardCvv', targets.cardCvv);
    this.mountIfTargeted('cardPostalCode', targets.cardPostalCode);
    this.mountIfTargeted('cardName', targets.cardName);
    this.mountIfTargeted('cardEmail', targets.cardEmail);
    this.mountIfTargeted('paymentRequestButton', targets.paymentRequestButton);
    return this;
  }

  get(key: FieldKey): CloverElement | undefined {
    return this.elements.get(key);
  }

  entries(): IterableIterator<[FieldKey, CloverElement]> {
    return this.elements.entries();
  }

  destroy(): void {
    this.destroyed = true;
    this.elements.clear();
  }

  private mountIfTargeted(key: FieldKey, selector: string | undefined): void {
    if (!selector) {
      return;
    }
    const el = this.elements.get(key);
    if (!el) {
      return;
    }
    const host = document.querySelector(selector);
    if (!host) {
      return;
    }
    try {
      el.mount(selector);
    } catch (error) {
      console.warn(`[wcp-payment-fields] failed to mount "${key}" at "${selector}"`, error);
      return;
    }
    // Clover's iframe document occasionally renders content a sub-pixel taller
    // than its viewport (Firefox shows a fading overlay scrollbar; Chrome a
    // permanent thin one). The CSS-only fixes don't fully suppress it because
    // (a) Clover's own `updateFrameStyles` postMessage sets inline `height`
    // on the iframe at runtime, racing our stylesheet, and (b) overflow on
    // an iframe element is honored inconsistently across browsers. The
    // deprecated `scrolling="no"` HTML attribute is still honored by every
    // browser we ship to and is the only reliable kill switch for cross-
    // origin iframe scrollbars. Apply it after Clover injects the iframe.
    suppressIframeScrollbars(host);
  }

  private assertAlive(): void {
    if (this.destroyed) {
      throw new Error('ElementRegistry has been destroyed.');
    }
  }
}

/**
 * Apply `scrolling="no"` to every iframe Clover injects into the host element.
 * Runs once synchronously (Clover's mount is sync) and then via MutationObserver
 * in case Clover swaps the iframe later (e.g., re-creating it on `update()`).
 */
function suppressIframeScrollbars(host: Element): void {
  const apply = (frame: HTMLIFrameElement): void => {
    frame.setAttribute('scrolling', 'no');
    // Belt-and-suspenders for browsers that honor CSS overflow on iframes.
    frame.style.overflow = 'hidden';
  };

  for (const frame of host.querySelectorAll('iframe')) {
    apply(frame as HTMLIFrameElement);
  }

  // Observe in case Clover replaces the iframe (e.g., on PaymentRequest
  // re-init when cart total changes). The observer is cheap and the host
  // node lives as long as the SDK does.
  const observer = new MutationObserver((mutations) => {
    for (const m of mutations) {
      for (const node of m.addedNodes) {
        if (node instanceof HTMLIFrameElement) {
          apply(node);
        }
      }
    }
  });
  observer.observe(host, { childList: true, subtree: true });
}
