// ── 3DS state machine ───────────────────────────────────────────
//
// Drives Clover's `_3DSUtil` through method (fingerprinting) and challenge
// flows, listens for the `executePatch` event Clover dispatches on `document`
// when its iframe completes, POSTs the outcome to the host's finalize URL,
// and recurses on escalation (method → challenge or vice versa).

import { parseInterim, postFinalize } from './threeds-finalize-client';
import type { HeadersProvider, ThreeDSUtil, ThreeDsInterim, ThreeDsResult } from './types';

const DEFAULT_METHOD_TIMEOUT_MS = 30 * 1000;
const DEFAULT_CHALLENGE_TIMEOUT_MS = 5 * 60 * 1000;

interface ExecutePatchDetail {
  _3DSStatus?: string;
}

export interface ThreeDsMachineOptions {
  readonly util: ThreeDSUtil;
  readonly finalizeUrl: string;
  readonly headers?: HeadersProvider;
  /** Extra fields the host needs in the finalize body (order_id, order_key, nonce). */
  readonly extraBody?: Record<string, unknown>;
  readonly methodTimeoutMs?: number;
  readonly challengeTimeoutMs?: number;
}

export class ThreeDsMachine {
  private readonly options: ThreeDsMachineOptions;
  private cancelled = false;

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

  cancel(): void {
    this.cancelled = true;
  }

  /**
   * Run a single 3DS interim payload to a terminal result. For escalation
   * (method completes → Clover wants challenge, or vice versa), the result
   * is returned with `kind: 'escalation'`; the caller (PaymentFieldsMachine)
   * re-enters the loop with the next interim.
   */
  async run(interim: ThreeDsInterim): Promise<ThreeDsResult> {
    if (this.cancelled) {
      return { kind: 'failure', message: 'Cancelled.' };
    }

    if (interim.state === 'method_required') {
      try {
        const status = await this.runMethod(interim);
        if (this.cancelled) {
          return { kind: 'failure', message: 'Cancelled.' };
        }
        return this.finalize(interim, status);
      } catch (error) {
        return { kind: 'failure', message: (error as Error).message };
      }
    }

    if (interim.state === 'challenge_required') {
      const cleanup = this.styleChallengeContainer();
      try {
        const status = await this.runChallenge(interim);
        if (this.cancelled) {
          return { kind: 'failure', message: 'Cancelled.' };
        }
        return await this.finalize(interim, status);
      } catch (error) {
        return { kind: 'failure', message: (error as Error).message };
      } finally {
        cleanup();
      }
    }

    return {
      kind: 'failure',
      message: `Unsupported 3DS state: ${(interim as { state: string }).state}`,
    };
  }

  // ── flow handlers ──

  private runMethod(interim: ThreeDsInterim): Promise<string> {
    const f = interim.fields;
    if (!f.threeDsServerTransactionId || !f.methodUrl || !f.methodNotificationUrl) {
      return Promise.reject(new Error('3DS method prerequisites missing.'));
    }
    return this.awaitExecutePatch(
      this.options.methodTimeoutMs ?? DEFAULT_METHOD_TIMEOUT_MS,
      'method flow timed out',
      () =>
        this.options.util.perform3DSFingerPrinting({
          threeDSServerTransID: f.threeDsServerTransactionId!,
          threeDSMethodUrl: f.methodUrl!,
          methodNotificationUrl: f.methodNotificationUrl!,
        }),
    );
  }

  private runChallenge(interim: ThreeDsInterim): Promise<string> {
    const f = interim.fields;
    if (!f.acsUrl || !f.acsTransactionId || !f.threeDsServerTransactionId || !f.protocolVersion) {
      return Promise.reject(new Error('3DS challenge prerequisites missing.'));
    }
    return this.awaitExecutePatch(
      this.options.challengeTimeoutMs ?? DEFAULT_CHALLENGE_TIMEOUT_MS,
      'challenge flow timed out',
      () =>
        this.options.util.perform3DSChallenge({
          messageVersion: f.protocolVersion!,
          acsTransID: f.acsTransactionId!,
          acsUrl: f.acsUrl!,
          threeDSServerTransID: f.threeDsServerTransactionId!,
        }),
    );
  }

  private awaitExecutePatch(timeoutMs: number, timeoutMessage: string, kickoff: () => void): Promise<string> {
    return new Promise<string>((resolve, reject) => {
      let settled = false;
      const finalize = (status: string): void => {
        if (settled) return;
        settled = true;
        window.clearTimeout(timeoutId);
        document.removeEventListener('executePatch', handler);
        resolve(status);
      };
      const handler = (ev: Event): void => {
        const detail = (ev as CustomEvent<ExecutePatchDetail>).detail;
        finalize(detail?._3DSStatus ?? '');
      };
      const timeoutId = window.setTimeout(() => {
        if (settled) return;
        settled = true;
        document.removeEventListener('executePatch', handler);
        reject(new Error(timeoutMessage));
      }, timeoutMs);
      document.addEventListener('executePatch', handler);
      try {
        kickoff();
      } catch (error) {
        settled = true;
        window.clearTimeout(timeoutId);
        document.removeEventListener('executePatch', handler);
        reject(error as Error);
      }
    });
  }

  // ── finalize ──

  private async finalize(interim: ThreeDsInterim, flowStatus: string): Promise<ThreeDsResult> {
    const body: Record<string, unknown> = {
      charge_id: interim.chargeId,
      flow_status: flowStatus,
      ...(this.options.extraBody ?? {}),
    };

    let envelope;
    try {
      envelope = await postFinalize({
        url: this.options.finalizeUrl,
        headers: this.options.headers,
        body,
      });
    } catch (error) {
      return { kind: 'failure', message: (error as Error).message };
    }

    const data = envelope.data;
    if (!data) {
      return { kind: 'failure', message: 'Verification response unreadable.' };
    }

    if (data.weeconnectpay_threeds_interim) {
      const next = parseInterim(data.weeconnectpay_threeds_interim);
      if (next) {
        return { kind: 'escalation', next };
      }
    }

    if (envelope.ok && data.success === true) {
      return { kind: 'success', redirect: data.redirect, data };
    }

    return {
      kind: 'failure',
      message: data.message ?? 'Payment verification failed.',
    };
  }

  // ── challenge container styling ──
  //
  // Clover injects `#threedsContainer` into the DOM when `perform3DSChallenge`
  // runs. We style it (overlay + centered card) via a MutationObserver so we
  // don't depend on Clover's render timing.

  private styleChallengeContainer(): () => void {
    const container = document.getElementById('threedsContainer');
    if (!container) {
      return () => undefined;
    }

    container.style.cssText =
      'position:fixed;inset:0;z-index:99999;display:flex;flex-direction:column;' +
      'align-items:center;justify-content:center;background:rgba(0,0,0,0.6);padding:1rem;';

    const styleIframe = (): boolean => {
      const iframe = container.querySelector('iframe');
      if (!iframe) {
        return false;
      }
      iframe.style.background = 'white';
      iframe.style.borderRadius = '0.75rem';
      iframe.style.boxShadow = '0 25px 50px -12px rgba(0,0,0,0.4)';
      iframe.style.maxWidth = '100%';
      iframe.style.display = 'block';
      return true;
    };

    let observer: MutationObserver | null = null;
    if (!styleIframe()) {
      observer = new MutationObserver(() => {
        if (styleIframe()) {
          observer?.disconnect();
          observer = null;
        }
      });
      observer.observe(container, { childList: true });
    }

    return () => {
      observer?.disconnect();
      container.style.cssText = '';
    };
  }
}
