/**
 * Webhook delivery system for async extraction jobs.
 *
 * Provides webhook callback on job completion with HMAC-SHA256 signature
 * verification, exponential backoff retry, and delivery logging.
 */

import { createHmac, randomUUID } from 'node:crypto';
import { type ExtractionJob } from './jobs.js';

export interface WebhookConfig {
  url: string;
  secret: string;
  retryAttempts?: number;
  retryDelayMs?: number;
}

export interface WebhookDelivery {
  id: string;
  jobId: string;
  url: string;
  status: 'pending' | 'delivered' | 'failed';
  attempts: number;
  lastAttemptAt?: string;
  error?: string;
  responseStatus?: number;
  createdAt: string;
}

// In-memory delivery log (Cosmos persistence deferred)
const deliveryLog = new Map<string, WebhookDelivery>();

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

/**
 * Verify webhook signature.
 */
export function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
  const expected = signWebhookPayload(payload, secret);
  try {
    // Constant-time comparison to prevent timing attacks
    if (signature.length !== expected.length) return false;
    let result = 0;
    for (let i = 0; i < signature.length; i++) {
      result |= signature.charCodeAt(i) ^ expected.charCodeAt(i);
    }
    return result === 0;
  } catch {
    return false;
  }
}

/**
 * Build webhook payload for job completion.
 */
export function buildWebhookPayload(job: ExtractionJob): string {
  const payload = {
    event: 'job.completed',
    jobId: job.id,
    status: job.status,
    progress: job.progress,
    resultSummary: {
      totalInputs: job.inputs.length,
      successfulResults: job.results.filter(r => r.extractions.length > 0).length,
      errorCount: job.errors.length,
    },
    completedAt: job.completedAt,
    timestamp: new Date().toISOString(),
  };
  return JSON.stringify(payload);
}

/**
 * Deliver webhook with retry logic.
 */
async function deliverWebhook(
  delivery: WebhookDelivery,
  payload: string,
  config: WebhookConfig
): Promise<void> {
  const signature = signWebhookPayload(payload, config.secret);
  const maxAttempts = config.retryAttempts ?? 3;
  const baseDelay = config.retryDelayMs ?? 1000;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      delivery.attempts = attempt;
      delivery.lastAttemptAt = new Date().toISOString();

      const res = await fetch(config.url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Webhook-Signature': `sha256=${signature}`,
          'X-Webhook-ID': delivery.id,
          'X-Webhook-Event': 'job.completed',
          'User-Agent': 'ByteLyst-Extraction-Service/1.0',
        },
        body: payload,
        signal: AbortSignal.timeout(30000), // 30s timeout
      });

      delivery.responseStatus = res.status;

      if (res.ok) {
        delivery.status = 'delivered';
        return;
      }

      // Non-2xx response - will retry
      delivery.error = `HTTP ${res.status}: ${await res.text().catch(() => 'Unknown error')}`;

      // Don't retry on 4xx client errors (except 429 rate limit)
      if (res.status >= 400 && res.status < 500 && res.status !== 429) {
        break;
      }
    } catch (err) {
      delivery.error = err instanceof Error ? err.message : 'Network error';
    }

    // Exponential backoff before next attempt (except on final attempt)
    if (attempt < maxAttempts) {
      const delay = baseDelay * Math.pow(2, attempt - 1);
      await new Promise(resolve => globalThis.setTimeout(resolve, delay));
    }
  }

  delivery.status = 'failed';
}

/**
 * Trigger webhook for job completion.
 * Called by the job processor when a job finishes.
 */
export async function triggerJobWebhook(job: ExtractionJob, config: WebhookConfig): Promise<void> {
  const delivery: WebhookDelivery = {
    id: randomUUID(),
    jobId: job.id,
    url: config.url,
    status: 'pending',
    attempts: 0,
    createdAt: new Date().toISOString(),
  };

  deliveryLog.set(delivery.id, delivery);

  const payload = buildWebhookPayload(job);
  await deliverWebhook(delivery, payload, config);
}

/**
 * Get webhook delivery by ID.
 */
export function getWebhookDelivery(deliveryId: string): WebhookDelivery | undefined {
  return deliveryLog.get(deliveryId);
}

/**
 * List webhook deliveries for a job.
 */
export function listJobDeliveries(jobId: string): WebhookDelivery[] {
  return [...deliveryLog.values()]
    .filter(d => d.jobId === jobId)
    .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}

/**
 * Get delivery statistics.
 */
export function getDeliveryStats(): {
  total: number;
  delivered: number;
  failed: number;
  pending: number;
} {
  const deliveries = [...deliveryLog.values()];
  return {
    total: deliveries.length,
    delivered: deliveries.filter(d => d.status === 'delivered').length,
    failed: deliveries.filter(d => d.status === 'failed').length,
    pending: deliveries.filter(d => d.status === 'pending').length,
  };
}

/**
 * Retry a failed webhook delivery.
 */
export async function retryWebhookDelivery(
  deliveryId: string,
  job: ExtractionJob,
  config: WebhookConfig
): Promise<boolean> {
  const delivery = deliveryLog.get(deliveryId);
  if (!delivery) return false;

  delivery.status = 'pending';
  delivery.error = undefined;
  delivery.responseStatus = undefined;

  const payload = buildWebhookPayload(job);
  await deliverWebhook(delivery, payload, config);

  return true;
}

/**
 * Reset delivery log (for testing).
 */
export function resetDeliveryLog(): void {
  deliveryLog.clear();
}
