/**
 * Reusable webhook dispatcher with HMAC-SHA256 signing and exponential backoff retry.
 * Extracted from platform-service's dispatcher for cross-product reuse.
 */

import { randomUUID, createHmac } from 'node:crypto';
import type {
  WebhookTarget,
  WebhookPayload,
  DeliveryAttempt,
  DeliveryResult,
  DispatchOptions,
} from './types.js';

const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_TIMEOUT_MS = 5_000;
const DEFAULT_BACKOFF = [10_000, 60_000, 300_000];

/**
 * Sign a webhook payload body with HMAC-SHA256.
 */
export function signPayload(body: string, secret: string): string {
  return createHmac('sha256', secret).update(body).digest('hex');
}

/**
 * Dispatch an event to a single webhook target with retry and signing.
 */
export async function deliverToTarget(
  target: WebhookTarget,
  event: string,
  data: Record<string, unknown>,
  productId: string,
  options?: DispatchOptions
): Promise<DeliveryResult> {
  const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
  const backoff = options?.backoffIntervals ?? DEFAULT_BACKOFF;

  const deliveryId = randomUUID();
  const timestamp = new Date().toISOString();

  const payload: WebhookPayload = {
    id: deliveryId,
    event,
    productId,
    timestamp,
    data,
  };

  const body = JSON.stringify(payload);
  const signature = signPayload(body, target.secret);
  const attempts: DeliveryAttempt[] = [];

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    if (attempt > 0) {
      const delay = backoff[attempt - 1] ?? backoff[backoff.length - 1];
      await sleep(delay);
    }

    const start = Date.now();
    const attemptRecord: DeliveryAttempt = {
      attemptNumber: attempt + 1,
      durationMs: 0,
      attemptedAt: new Date().toISOString(),
    };

    try {
      const res = await fetch(target.url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Webhook-Signature': `sha256=${signature}`,
          'X-Webhook-Timestamp': timestamp,
          'X-Webhook-Event': event,
          'X-Webhook-Delivery-Id': deliveryId,
        },
        body,
        signal: AbortSignal.timeout(timeoutMs),
      });

      attemptRecord.durationMs = Date.now() - start;
      attemptRecord.responseCode = res.status;

      if (res.ok) {
        attempts.push(attemptRecord);
        const result: DeliveryResult = {
          deliveryId,
          targetId: target.id,
          event,
          status: 'success',
          attempts,
          completedAt: new Date().toISOString(),
        };
        await options?.onDelivery?.(result);
        return result;
      }

      attemptRecord.error = `HTTP ${res.status}`;
    } catch (err) {
      attemptRecord.durationMs = Date.now() - start;
      attemptRecord.error = err instanceof Error ? err.message : String(err);
    }

    attempts.push(attemptRecord);
  }

  const result: DeliveryResult = {
    deliveryId,
    targetId: target.id,
    event,
    status: 'failed',
    attempts,
    completedAt: new Date().toISOString(),
  };
  await options?.onDelivery?.(result);
  return result;
}

/**
 * Dispatch an event to all matching targets.
 * Fire-and-forget: errors are collected in results, never thrown.
 */
export async function dispatchToTargets(
  targets: WebhookTarget[],
  event: string,
  data: Record<string, unknown>,
  productId: string,
  options?: DispatchOptions
): Promise<DeliveryResult[]> {
  const matching = targets.filter(
    t => t.enabled && (t.events.length === 0 || t.events.includes(event))
  );

  if (matching.length === 0) return [];

  const results = await Promise.allSettled(
    matching.map(target => deliverToTarget(target, event, data, productId, options))
  );

  return results.map(r =>
    r.status === 'fulfilled'
      ? r.value
      : {
          deliveryId: randomUUID(),
          targetId: 'unknown',
          event,
          status: 'failed' as const,
          attempts: [],
          completedAt: new Date().toISOString(),
        }
  );
}

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}
