import { randomUUID, createHmac } from 'node:crypto';
import type { FastifyBaseLogger } from 'fastify';
import * as repo from './repository.js';
import type {
  WebhookEventType,
  WebhookOutboundPayload,
  WebhookDeliveryDoc,
  WebhookDeliveryAttempt,
  WebhookSubscriptionDoc,
} from './types.js';

const MAX_RETRIES = parseInt(process.env.WEBHOOK_MAX_RETRIES ?? '3', 10);
const TIMEOUT_MS = parseInt(process.env.WEBHOOK_DELIVERY_TIMEOUT_MS ?? '5000', 10);
const BACKOFF_INTERVALS = [10_000, 60_000, 300_000]; // 10s, 60s, 5min
const AUTO_DISABLE_THRESHOLD = 10;

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

/**
 * Dispatch a webhook event to all matching subscriptions for a product.
 * Fire-and-forget: errors are logged, never thrown to the caller.
 */
export async function dispatchEvent(
  productId: string,
  event: WebhookEventType,
  data: Record<string, unknown>,
  log?: FastifyBaseLogger
): Promise<void> {
  let subs: WebhookSubscriptionDoc[];
  try {
    subs = await repo.findSubscriptionsForEvent(productId, event);
  } catch (err) {
    log?.error({ err, event, productId }, 'Failed to query webhook subscriptions');
    return;
  }

  if (subs.length === 0) return;

  const promises = subs.map(sub => deliverToSubscription(sub, event, data, productId, log));
  await Promise.allSettled(promises);
}

async function deliverToSubscription(
  sub: WebhookSubscriptionDoc,
  event: WebhookEventType,
  data: Record<string, unknown>,
  productId: string,
  log?: FastifyBaseLogger
): Promise<void> {
  const deliveryId = randomUUID();
  const timestamp = new Date().toISOString();
  const now = () => new Date().toISOString();

  const outbound: WebhookOutboundPayload = {
    id: deliveryId,
    event,
    productId,
    timestamp,
    data,
  };

  const body = JSON.stringify(outbound);
  const signature = signPayload(body, sub.secret);

  const delivery: WebhookDeliveryDoc = {
    id: deliveryId,
    subscriptionId: sub.id,
    pk: `${sub.id}:${timestamp.slice(0, 7)}`,
    event,
    payload: outbound as unknown as Record<string, unknown>,
    status: 'retrying',
    attempts: [],
    createdAt: timestamp,
  };

  try {
    await repo.createDelivery(delivery);
  } catch (err) {
    log?.error({ err, subscriptionId: sub.id }, 'Failed to create webhook delivery record');
  }

  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
    if (attempt > 0) {
      const delay = BACKOFF_INTERVALS[attempt - 1] ?? BACKOFF_INTERVALS.at(-1)!;
      await sleep(delay);
    }

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

    try {
      const res = await fetch(sub.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(TIMEOUT_MS),
      });

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

      if (res.ok) {
        delivery.status = 'success';
        delivery.completedAt = now();
        delivery.attempts.push(attemptRecord);
        await safeUpdateDelivery(delivery, log);
        await repo.resetFailureCount(sub.id, sub.productId).catch(() => {});
        return;
      }

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

    delivery.attempts.push(attemptRecord);
  }

  // All retries exhausted
  delivery.status = 'failed';
  delivery.completedAt = now();
  await safeUpdateDelivery(delivery, log);

  // Increment failure count, auto-disable after threshold
  const shouldDisable = sub.failureCount + 1 >= AUTO_DISABLE_THRESHOLD;
  await repo.incrementFailureCount(sub.id, sub.productId, shouldDisable).catch(() => {});

  if (shouldDisable) {
    log?.warn(
      { subscriptionId: sub.id, url: sub.url },
      'Webhook subscription auto-disabled after consecutive failures'
    );
  }
}

async function safeUpdateDelivery(
  delivery: WebhookDeliveryDoc,
  log?: FastifyBaseLogger
): Promise<void> {
  try {
    await repo.updateDelivery(delivery);
  } catch (err) {
    log?.error({ err, deliveryId: delivery.id }, 'Failed to update webhook delivery record');
  }
}

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