/**
 * Webhook Delivery Service
 *
 * Handles webhook delivery for translation job completion notifications
 */

import crypto from 'crypto';
import axios, { AxiosError } from 'axios';
import { PrismaClient, TranslationJobStatus } from '@prisma/client';
import { WebhookPayload, WebhookDelivery } from '../types';
import { logger } from '../utils/logger';
import { config } from '../config';
import { mapStoredTranslation } from './translationService';
import { trackWebhookDelivery } from '../utils/metrics';

const prisma = new PrismaClient();

export function getStructuredWebhookResult(rawTranslation: string | null) {
  return mapStoredTranslation(rawTranslation);
}


/**
 * Deliver webhook notification to callback URL
 *
 * Sends POST request with HMAC signature for verification
 * Records delivery attempt in database
 */
export async function deliverWebhook(
  jobId: string,
  payload: WebhookPayload,
  callbackUrl: string,
  callbackSecret: string,
  stableDeliveryId?: string
): Promise<WebhookDelivery> {
  const startTime = Date.now();
  let success = false;
  let httpStatus: number | undefined;
  let responseBody: string | undefined;
  let errorMessage: string | undefined;
  const deliveryId = stableDeliveryId || crypto.randomUUID();
  const deliveryRecordId = crypto.randomUUID();

  try {
    // Add timestamp and delivery identifier for replay protection
    const timestamp = Date.now().toString();
    const signedPayload = { ...payload, deliveryId };
    const body = JSON.stringify(signedPayload);

    const signature = crypto
      .createHmac('sha256', callbackSecret)
      .update(`${timestamp}.${body}`)
      .digest('hex');

    // Prepare headers
    const headers = {
      'Content-Type': 'application/json',
      'X-Webhook-Signature': signature,
      'X-TPZ-Timestamp': timestamp, // For replay attack prevention
      'User-Agent': 'TranslatePressZone-Webhook/1.0',
    };

    // Send webhook
    logger.info('Delivering webhook', {
      jobId,
      callbackUrl,
      event: payload.event,
    });

    const response = await axios.post(callbackUrl, body, {
      headers,
      timeout: 10000, // 10 second timeout
      validateStatus: (status) => status >= 200 && status < 300,
    });

    success = true;
    httpStatus = response.status;
    responseBody = JSON.stringify(response.data).substring(0, 1000); // Limit to 1000 chars

    logger.info('Webhook delivered successfully', {
      jobId,
      callbackUrl,
      httpStatus,
    });
  } catch (error) {
    success = false;

    if (axios.isAxiosError(error)) {
      const axiosError = error as AxiosError;
      httpStatus = axiosError.response?.status;
      responseBody = axiosError.response?.data
        ? JSON.stringify(axiosError.response.data).substring(0, 1000)
        : undefined;
      errorMessage = axiosError.message;
    } else if (error instanceof Error) {
      errorMessage = error.message;
    } else {
      errorMessage = 'Unknown error during webhook delivery';
    }

    logger.error('Webhook delivery failed', {
      jobId,
      callbackUrl,
      httpStatus,
      errorMessage,
    });
  }

  const duration = Date.now() - startTime;

  // Track metrics
  trackWebhookDelivery(success, duration);

  // Record delivery attempt in database
  try {
    const delivery = await prisma.webhookDelivery.create({
      data: {
        id: deliveryRecordId,
        job_id: jobId,
        attempt_number: 1, // Will be updated for retries
        success,
        http_status: httpStatus,
        response_body: responseBody,
        error_message: errorMessage,
      },
    });

    return delivery as unknown as WebhookDelivery;
  } catch (dbError) {
    logger.error('Failed to record webhook delivery', {
      jobId,
      error: dbError,
    });
    throw dbError;
  }
}

/**
 * Retry failed webhook delivery with exponential backoff
 *
 * Implements retry logic with configurable max retries
 * Uses exponential backoff: delay = baseDelay * (2 ^ attemptNumber)
 */
export async function retryFailedWebhook(
  jobId: string,
  attemptNumber: number
): Promise<WebhookDelivery> {
  if (attemptNumber > config.webhookMaxRetries) {
    throw new Error(
      `Maximum retry attempts (${config.webhookMaxRetries}) exceeded for job ${jobId}`
    );
  }

  // Get job details
  const job = await prisma.translationJob.findUnique({
    where: { id: jobId },
    select: {
      id: true,
      user_id: true,
      client_job_id: true,
      status: true,
      source_lang: true,
      target_lang: true,
      model: true,
      tone: true,
      translation: true,
      characters_used: true,
      tokens_used: true,
      cost: true,
      customer_cost: true,
      error_message: true,
      processing_time_ms: true,
      callback_url: true,
      callback_secret: true,
      completed_at: true,
    },
  });

  if (!job) {
    throw new Error(`Translation job ${jobId} not found`);
  }

  if (!job.callback_url || !job.callback_secret) {
    throw new Error(`Job ${jobId} has no callback URL or secret configured`);
  }

  // Reconstruct payload
  const storedTranslation = getStructuredWebhookResult(job.translation);
  const payload: WebhookPayload = {
    event:
      job.status === TranslationJobStatus.completed
        ? 'translation.completed'
        : 'translation.failed',
    jobId: job.id,
    clientJobId: job.client_job_id ?? undefined,
    status: job.status as any,
    ...storedTranslation,
    charactersUsed: job.characters_used,
    cost: job.customer_cost.toNumber(),
    errorMessage: job.error_message ?? undefined,
    processingTimeMs: job.processing_time_ms ?? undefined,
    timestamp: job.completed_at?.toISOString() ?? new Date().toISOString(),
  };

  // Calculate exponential backoff delay
  const delayMs = config.webhookRetryDelayMs * Math.pow(2, attemptNumber - 1);

  logger.info('Retrying webhook delivery', {
    jobId,
    attemptNumber,
    delayMs,
  });

  // Wait for backoff period
  await new Promise((resolve) => setTimeout(resolve, delayMs));

  // Attempt delivery
  const startTime = Date.now();
  let success = false;
  let httpStatus: number | undefined;
  let responseBody: string | undefined;
  let errorMessage: string | undefined;
  const deliveryId = crypto.randomUUID();

  try {
    // Add timestamp and delivery identifier for replay protection
    const timestamp = Date.now().toString();
    const signedPayload = { ...payload, deliveryId };
    const body = JSON.stringify(signedPayload);

    const signature = crypto
      .createHmac('sha256', job.callback_secret)
      .update(`${timestamp}.${body}`)
      .digest('hex');

    // Prepare headers
    const headers = {
      'Content-Type': 'application/json',
      'X-Webhook-Signature': signature,
      'X-TPZ-Timestamp': timestamp, // For replay attack prevention
      'User-Agent': 'TranslatePressZone-Webhook/1.0',
      'X-Retry-Attempt': attemptNumber.toString(),
    };

    // Send webhook
    const response = await axios.post(job.callback_url, body, {
      headers,
      timeout: 10000,
      validateStatus: (status) => status >= 200 && status < 300,
    });

    success = true;
    httpStatus = response.status;
    responseBody = JSON.stringify(response.data).substring(0, 1000);

    logger.info('Webhook retry successful', {
      jobId,
      attemptNumber,
      httpStatus,
    });
  } catch (error) {
    success = false;

    if (axios.isAxiosError(error)) {
      const axiosError = error as AxiosError;
      httpStatus = axiosError.response?.status;
      responseBody = axiosError.response?.data
        ? JSON.stringify(axiosError.response.data).substring(0, 1000)
        : undefined;
      errorMessage = axiosError.message;
    } else if (error instanceof Error) {
      errorMessage = error.message;
    } else {
      errorMessage = 'Unknown error during webhook retry';
    }

    logger.error('Webhook retry failed', {
      jobId,
      attemptNumber,
      httpStatus,
      errorMessage,
    });
  }

  const duration = Date.now() - startTime;

  // Track metrics
  trackWebhookDelivery(success, duration);

  // Record delivery attempt
  const delivery = await prisma.webhookDelivery.create({
    data: {
      id: deliveryId,
      job_id: jobId,
      attempt_number: attemptNumber,
      success,
      http_status: httpStatus,
      response_body: responseBody,
      error_message: errorMessage,
    },
  });

  return delivery as unknown as WebhookDelivery;
}

/**
 * Get all webhook delivery attempts for a job
 *
 * Returns delivery history ordered by attempt number
 */
export async function getWebhookDeliveries(
  jobId: string
): Promise<WebhookDelivery[]> {
  try {
    const deliveries = await prisma.webhookDelivery.findMany({
      where: { job_id: jobId },
      orderBy: { attempt_number: 'asc' },
    });

    return deliveries as unknown as unknown as WebhookDelivery[];
  } catch (error) {
    logger.error('Failed to get webhook deliveries', { jobId, error });
    throw error;
  }
}

/**
 * Check if webhook delivery should be retried
 *
 * Returns true if job has failed webhook deliveries that haven't exceeded max retries
 */
export async function shouldRetryWebhook(jobId: string): Promise<boolean> {
  try {
    const lastDelivery = await prisma.webhookDelivery.findFirst({
      where: { job_id: jobId },
      orderBy: { attempt_number: 'desc' },
    });

    if (!lastDelivery) {
      return false; // No delivery attempts yet
    }

    if (lastDelivery.success) {
      return false; // Last attempt succeeded
    }

    if (lastDelivery.attempt_number >= config.webhookMaxRetries) {
      return false; // Max retries exceeded
    }

    return true; // Should retry
  } catch (error) {
    logger.error('Failed to check webhook retry status', { jobId, error });
    throw error;
  }
}

/**
 * Get next retry attempt number for a job
 *
 * Returns the next attempt number or null if max retries exceeded
 */
export async function getNextRetryAttempt(
  jobId: string
): Promise<number | null> {
  try {
    const lastDelivery = await prisma.webhookDelivery.findFirst({
      where: { job_id: jobId },
      orderBy: { attempt_number: 'desc' },
    });

    if (!lastDelivery) {
      return 1; // First attempt
    }

    const nextAttempt = lastDelivery.attempt_number + 1;

    if (nextAttempt > config.webhookMaxRetries) {
      return null; // Max retries exceeded
    }

    return nextAttempt;
  } catch (error) {
    logger.error('Failed to get next retry attempt', { jobId, error });
    throw error;
  }
}
