// ── <wcp-payment-fields> custom element ─────────────────────────
//
// Form-associated custom element that renders the React composite in **light
// DOM** (NOT Shadow DOM). Style scoping relies on the `.wcp-pf__*` BEM class
// prefix rather than structural isolation.
//
// Why not Shadow DOM: Clover's `element.mount('#selector')` uses
// `document.querySelector` to find the mount point, and that does NOT pierce
// shadowRoot boundaries. Iframes never mount if the mount-points live inside
// a shadowRoot. A future v0.2 can re-introduce Shadow DOM via slot projection
// (mount-points in light DOM, visual layout in shadowRoot) — the architectural
// cost wasn't worth blocking the v0.1 ship.
//
// Lifecycle:
// - constructor: attach element internals (form-associated)
// - connectedCallback: parse attributes, build PaymentFieldsConfig,
//   construct PaymentFieldsMachine, mount React composite, subscribe for events
// - disconnectedCallback: unsubscribe, unmount React, destroy machine
//
// Imperative methods (`tokenize`, `runThreeDs`, `resetToIdle`) are exposed on
// the element instance for hosts that need direct control (e.g., the WC
// Classic adapter wires `tokenize()` to the `checkout_place_order` jQuery
// event, since WC owns the submit lifecycle).
//
// Custom events bubble + cross the shadowRoot boundary (`composed: true`):
//   wcp-gpay-start   — Clover paymentMethodStart fired
//   wcp-gpay-ready   — Clover paymentMethod fired (token captured, awaiting Place Order)
//   wcp-gpay-cancel  — user cancelled the GPay sheet
//   wcp-3ds-start    — entering threeds_method or threeds_challenge
//   wcp-3ds-done     — terminal success after 3DS
//   wcp-tokenized    — token is ready (manual card OR GPay), form value is set
//   wcp-error        — anything went wrong

import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import {
  PaymentFieldsMachine,
  type HeadersProvider,
  type PaymentFieldsConfig,
  type PaymentFieldsError,
  type PaymentFieldsSnapshot,
  type PaymentFieldsState,
  type ThreeDsInterim,
  type ThreeDsResult,
  type TokenizationResult,
} from '../core/index';
import { PaymentFields } from '../react/PaymentFields';

/**
 * Optional setter for the host's headers provider — must be set BEFORE the
 * element is connected (e.g., via `el.headersProvider = ...; document.body.appendChild(el)`).
 */
export interface WcpPaymentFieldsElement extends HTMLElement {
  headersProvider?: HeadersProvider;
  tokenize: () => Promise<TokenizationResult>;
  runThreeDs: (
    interim: ThreeDsInterim,
    options?: { readonly extraBody?: Record<string, unknown> },
  ) => Promise<ThreeDsResult>;
  resetToIdle: () => void;
  readonly machine: PaymentFieldsMachine | null;
}

const STATE_EVENT_MAP: Partial<Record<PaymentFieldsState, string>> = {
  gpay_opening: 'wcp-gpay-start',
  gpay_ready: 'wcp-gpay-ready',
  gpay_cancelled: 'wcp-gpay-cancel',
  threeds_method: 'wcp-3ds-start',
  threeds_challenge: 'wcp-3ds-start',
  done: 'wcp-3ds-done',
};

export class WcpPaymentFields extends HTMLElement implements WcpPaymentFieldsElement {
  /** Tells the form machinery that this element participates in form submission. */
  static readonly formAssociated = true;

  private internals: ElementInternals;
  /** Light-DOM mount container for the React tree. NOT a shadowRoot. */
  private container: HTMLDivElement | null = null;
  private root: Root | null = null;
  private _machine: PaymentFieldsMachine | null = null;
  private unsubscribe: (() => void) | null = null;
  private lastTokenizedFor: string | null = null;
  private lastErrorCode: string | null = null;
  private lastState: PaymentFieldsState | null = null;

  /** Caller-provided headers function (for 3DS finalize POST). Property — not an attribute. */
  headersProvider?: HeadersProvider;

  constructor() {
    super();
    this.internals = this.attachInternals();
  }

  // ── lifecycle ──

  connectedCallback(): void {
    console.log('[wcp-pf] <wcp-payment-fields> connectedCallback fired');
    if (this.root) {
      console.log('[wcp-pf] already has root, skipping');
      return;
    }

    const config = this.parseConfig();
    if (!config) {
      console.warn('[wcp-pf] missing required attributes; element will not render', {
        pakms: this.getAttribute('pakms-key'),
        merchant: this.getAttribute('merchant-id'),
        cloverSdkUrl: this.getAttribute('clover-sdk-url'),
      });
      return;
    }
    console.log('[wcp-pf] config parsed', config);

    this._machine = new PaymentFieldsMachine(config);
    this.subscribeMachineToEvents();

    const lockIconSrc = this.getAttribute('lock-icon-src') ?? '';
    const trustLogosSrc = this.getAttribute('trust-logos-src') ?? '';
    const trustText = this.getAttribute('trust-text') ?? undefined;

    // Light-DOM container — Clover's `element.mount('#id')` uses
    // `document.querySelector` which cannot pierce shadowRoots, so the mount
    // points must be reachable from the document root.
    this.container = document.createElement('div');
    this.appendChild(this.container);

    console.log('[wcp-pf] rendering React composite into light-DOM container');
    this.root = createRoot(this.container);
    this.root.render(
      createElement(PaymentFields, {
        machine: this._machine,
        lockIconSrc,
        trustLogosSrc,
        trustText,
      }),
    );
    console.log('[wcp-pf] React render initiated');

    // Notify the host that this element instance is connected so it can
    // (re-)assign per-instance state like `headersProvider`. WooCommerce's
    // `update_order_review` AJAX disconnects + reconnects a fresh element on
    // every shipping/billing change, and the host needs a hook to refresh its
    // bindings on each new instance. Bubbles + composes so the host can
    // listen on `document.body` for any future element instance.
    this.dispatchTyped('wcp-mounted', { pakmsKey: this.getAttribute('pakms-key') });
  }

  disconnectedCallback(): void {
    console.log('[wcp-pf] <wcp-payment-fields> disconnectedCallback fired');
    this.unsubscribe?.();
    this.unsubscribe = null;
    this.root?.unmount();
    this.root = null;
    this.container?.remove();
    this.container = null;
    this._machine?.destroy();
    this._machine = null;
    this.internals.setFormValue(null);
  }

  // ── public imperative API ──

  get machine(): PaymentFieldsMachine | null {
    return this._machine;
  }

  async tokenize(): Promise<TokenizationResult> {
    if (!this._machine) {
      throw new Error('<wcp-payment-fields> not connected.');
    }
    return this._machine.tokenize();
  }

  async runThreeDs(
    interim: ThreeDsInterim,
    options?: { readonly extraBody?: Record<string, unknown> },
  ): Promise<ThreeDsResult> {
    if (!this._machine) {
      throw new Error('<wcp-payment-fields> not connected.');
    }
    return this._machine.runThreeDs(interim, options);
  }

  resetToIdle(): void {
    this._machine?.resetToIdle();
  }

  // ── form-association callbacks ──

  formResetCallback(): void {
    this.resetToIdle();
  }

  // ── internal ──

  private subscribeMachineToEvents(): void {
    if (!this._machine) {
      return;
    }
    this.unsubscribe = this._machine.subscribe((snapshot) => this.onSnapshot(snapshot));
  }

  private onSnapshot(snapshot: PaymentFieldsSnapshot): void {
    // State transition events (each only fires when the state actually changes).
    if (snapshot.state !== this.lastState) {
      const eventName = STATE_EVENT_MAP[snapshot.state];
      if (eventName) {
        this.dispatchTyped(eventName, snapshot);
      }
      this.lastState = snapshot.state;
    }

    // Token ready (either path). Form value is set so the host `<form>` submits it.
    if (snapshot.result && snapshot.result.token !== this.lastTokenizedFor) {
      this.lastTokenizedFor = snapshot.result.token;
      this.internals.setFormValue(snapshot.result.token);
      this.dispatchTyped('wcp-tokenized', snapshot.result);
    }

    // Error transitions
    if (snapshot.error && snapshot.error.code !== this.lastErrorCode) {
      this.lastErrorCode = snapshot.error.code;
      this.dispatchTyped('wcp-error', snapshot.error);
    } else if (!snapshot.error) {
      this.lastErrorCode = null;
    }
  }

  private dispatchTyped(name: string, detail: unknown): void {
    this.dispatchEvent(
      new CustomEvent(name, {
        detail,
        bubbles: true,
        composed: true, // crosses shadowRoot boundary
      }),
    );
  }

  private parseConfig(): PaymentFieldsConfig | null {
    const pakmsKey = this.getAttribute('pakms-key');
    const merchantId = this.getAttribute('merchant-id');
    const cloverSdkUrl = this.getAttribute('clover-sdk-url');

    if (!pakmsKey || !merchantId || !cloverSdkUrl) {
      return null;
    }

    const locale = this.getAttribute('locale') ?? 'en';
    const currency = this.getAttribute('currency') ?? 'CAD';
    const country = this.getAttribute('country') ?? undefined;
    const cartTotal = parseIntAttr(this.getAttribute('cart-total'), 0);
    const clover3DSSdkUrl = this.getAttribute('clover-3ds-sdk-url') ?? undefined;

    const features = parseJsonAttr<PaymentFieldsConfig['features']>(
      this.getAttribute('features'),
    );
    const endpointsRaw = parseJsonAttr<{ threeDsFinalize?: string }>(
      this.getAttribute('endpoints'),
    );
    const theme = parseJsonAttr<PaymentFieldsConfig['theme']>(this.getAttribute('theme'));

    // Defer the headersProvider lookup until the SDK actually calls it. The
    // host (e.g., wc-classic.ts) assigns `el.headersProvider` from its
    // DOMContentLoaded bootstrap, which fires AFTER the custom element has
    // already connected — capturing `this.headersProvider` here would freeze
    // it at undefined and the 3DS finalize POST would go out without
    // `X-WP-Nonce`. A closure that re-reads `this.headersProvider` at call
    // time sees whatever the host has assigned by then.
    const lazyHeaders: HeadersProvider = async () => {
      const provider = this.headersProvider;
      if (!provider) {
        return {};
      }
      return provider();
    };

    return {
      pakmsKey,
      merchantId,
      locale,
      cartTotal,
      currency,
      country,
      cloverSdkUrl,
      clover3DSSdkUrl,
      features,
      endpoints: {
        threeDsFinalize: endpointsRaw?.threeDsFinalize,
        headers: lazyHeaders,
      },
      theme,
    };
  }
}

function parseJsonAttr<T>(value: string | null): T | undefined {
  if (!value) {
    return undefined;
  }
  try {
    return JSON.parse(value) as T;
  } catch (error) {
    console.warn('[wcp-payment-fields] failed to parse JSON attribute', value, error);
    return undefined;
  }
}

function parseIntAttr(value: string | null, fallback: number): number {
  if (!value) {
    return fallback;
  }
  const parsed = parseInt(value, 10);
  return Number.isFinite(parsed) ? parsed : fallback;
}

// Re-export the error type for consumers reading event.detail
export type { PaymentFieldsError };
