/**
 * WebhookDeliveryService - Webhook Delivery Management
 *
 * Handles delivery tracking, retry logic, and execution of webhook deliveries.
 * Manages the lifecycle of webhook deliveries from pending to completed/failed.
 *
 * @module Webhooks
 */

import { Repository, LessThanOrEqual } from 'typeorm';
import * as crypto from 'crypto';
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
import { WebhookDelivery, DeliveryStatus } from '../entities/delivery.entity';
import { WebhookEndpoint } from '../entities/endpoint.entity';

// DTOs
export interface DeliveryFilters {
  endpointId?: string;
  eventType?: string;
  eventId?: string;
  status?: DeliveryStatus | DeliveryStatus[];
  dateFrom?: Date;
  dateTo?: Date;
  page?: number;
  limit?: number;
}

export interface DeliveryResult {
  deliveryId: string;
  success: boolean;
  statusCode?: number;
  responseTimeMs?: number;
  errorMessage?: string;
  willRetry: boolean;
  nextRetryAt?: Date;
}

export interface DeliveryStats {
  total: number;
  pending: number;
  delivered: number;
  failed: number;
  retrying: number;
  avgResponseTimeMs: number;
  successRate: number;
}

export interface RetryConfig {
  maxRetries: number;
  baseDelaySeconds: number;
  backoffMultiplier: number;
}

export class WebhookDeliveryService {
  constructor(
    private readonly repository: Repository<WebhookDelivery>,
    private readonly endpointRepository: Repository<WebhookEndpoint>
  ) {}

  /**
   * Generate HMAC signature for payload
   * Used for verifying webhook payloads
   */
  generateSignature(payload: string, secret: string): string {
    return crypto.createHmac('sha256', secret).update(payload).digest('hex');
  }

  /**
   * Calculate next retry time with exponential backoff
   */
  private calculateNextRetry(
    attemptNumber: number,
    baseDelaySeconds: number,
    backoffMultiplier: number
  ): Date {
    const delaySeconds = baseDelaySeconds * Math.pow(backoffMultiplier, attemptNumber - 1);
    const nextRetry = new Date();
    nextRetry.setSeconds(nextRetry.getSeconds() + delaySeconds);
    return nextRetry;
  }

  /**
   * Find all deliveries with filters and pagination
   */
  async findAll(
    ctx: ServiceContext,
    filters: DeliveryFilters = {}
  ): Promise<PaginatedResult<WebhookDelivery>> {
    const page = filters.page || 1;
    const limit = filters.limit || 20;

    const qb = this.repository
      .createQueryBuilder('d')
      .leftJoinAndSelect('d.endpoint', 'endpoint')
      .where('d.tenant_id = :tenantId', { tenantId: ctx.tenantId });

    if (filters.endpointId) {
      qb.andWhere('d.endpoint_id = :endpointId', { endpointId: filters.endpointId });
    }

    if (filters.eventType) {
      qb.andWhere('d.event_type = :eventType', { eventType: filters.eventType });
    }

    if (filters.eventId) {
      qb.andWhere('d.event_id = :eventId', { eventId: filters.eventId });
    }

    if (filters.status) {
      if (Array.isArray(filters.status)) {
        qb.andWhere('d.status IN (:...statuses)', { statuses: filters.status });
      } else {
        qb.andWhere('d.status = :status', { status: filters.status });
      }
    }

    if (filters.dateFrom) {
      qb.andWhere('d.created_at >= :dateFrom', { dateFrom: filters.dateFrom });
    }

    if (filters.dateTo) {
      qb.andWhere('d.created_at <= :dateTo', { dateTo: filters.dateTo });
    }

    const skip = (page - 1) * limit;
    qb.orderBy('d.created_at', 'DESC').skip(skip).take(limit);

    const [data, total] = await qb.getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  /**
   * Find delivery by ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<WebhookDelivery | null> {
    return this.repository.findOne({
      where: { id, tenantId: ctx.tenantId },
      relations: ['endpoint'],
    });
  }

  /**
   * Find deliveries ready for processing
   */
  async findPendingDeliveries(limit: number = 100): Promise<WebhookDelivery[]> {
    const now = new Date();

    return this.repository.find({
      where: [
        { status: 'pending' },
        { status: 'retrying', nextRetryAt: LessThanOrEqual(now) },
      ],
      order: { scheduledAt: 'ASC' },
      take: limit,
      relations: ['endpoint'],
    });
  }

  /**
   * Process a delivery (execute HTTP request)
   * NOTE: This is a simulation. Real implementation would use HTTP client.
   */
  async processDelivery(delivery: WebhookDelivery): Promise<DeliveryResult> {
    const startTime = Date.now();

    // Update status to sending
    delivery.status = 'sending';
    delivery.startedAt = new Date();
    await this.repository.save(delivery);

    try {
      // Get endpoint for signing secret
      const endpoint = delivery.endpoint || await this.endpointRepository.findOne({
        where: { id: delivery.endpointId },
      });

      if (!endpoint) {
        throw new Error('Endpoint not found');
      }

      // In a real implementation, this would make an HTTP request:
      // const response = await fetch(delivery.requestUrl, {
      //   method: delivery.requestMethod,
      //   headers: {
      //     ...delivery.requestHeaders,
      //     'X-Webhook-Signature': this.generateSignature(JSON.stringify(delivery.payload), endpoint.signingSecret),
      //     'X-Webhook-Event': delivery.eventType,
      //     'X-Webhook-Delivery': delivery.id,
      //   },
      //   body: JSON.stringify(delivery.payload),
      //   signal: AbortSignal.timeout(endpoint.timeoutSeconds * 1000),
      // });

      // Simulate successful delivery for now
      const responseTimeMs = Date.now() - startTime;
      const simulatedSuccess = Math.random() > 0.1; // 90% success rate simulation

      if (simulatedSuccess) {
        delivery.status = 'delivered';
        delivery.responseStatus = 200;
        delivery.responseBody = '{"success": true}';
        delivery.responseTimeMs = responseTimeMs;
        delivery.completedAt = new Date();

        await this.repository.save(delivery);

        // Update endpoint stats
        await this.endpointRepository
          .createQueryBuilder()
          .update(WebhookEndpoint)
          .set({
            totalDeliveries: () => 'total_deliveries + 1',
            successfulDeliveries: () => 'successful_deliveries + 1',
            lastDeliveryAt: new Date(),
            lastSuccessAt: new Date(),
          })
          .where('id = :id', { id: delivery.endpointId })
          .execute();

        return {
          deliveryId: delivery.id,
          success: true,
          statusCode: 200,
          responseTimeMs,
          willRetry: false,
        };
      } else {
        throw new Error('Simulated failure');
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Unknown error';
      const responseTimeMs = Date.now() - startTime;

      delivery.attemptNumber += 1;
      delivery.responseTimeMs = responseTimeMs;
      delivery.errorMessage = errorMessage;

      if (delivery.attemptNumber < delivery.maxAttempts) {
        // Schedule retry
        const endpoint = delivery.endpoint || await this.endpointRepository.findOne({
          where: { id: delivery.endpointId },
        });

        const nextRetry = this.calculateNextRetry(
          delivery.attemptNumber,
          endpoint?.retryDelaySeconds || 60,
          endpoint?.retryBackoffMultiplier || 2.0
        );

        delivery.status = 'retrying';
        delivery.nextRetryAt = nextRetry;

        await this.repository.save(delivery);

        return {
          deliveryId: delivery.id,
          success: false,
          errorMessage,
          responseTimeMs,
          willRetry: true,
          nextRetryAt: nextRetry,
        };
      } else {
        // Max retries reached
        delivery.status = 'failed';
        delivery.completedAt = new Date();

        await this.repository.save(delivery);

        // Update endpoint stats
        await this.endpointRepository
          .createQueryBuilder()
          .update(WebhookEndpoint)
          .set({
            totalDeliveries: () => 'total_deliveries + 1',
            failedDeliveries: () => 'failed_deliveries + 1',
            lastDeliveryAt: new Date(),
            lastFailureAt: new Date(),
          })
          .where('id = :id', { id: delivery.endpointId })
          .execute();

        return {
          deliveryId: delivery.id,
          success: false,
          errorMessage,
          responseTimeMs,
          willRetry: false,
        };
      }
    }
  }

  /**
   * Cancel a pending delivery
   */
  async cancel(ctx: ServiceContext, id: string): Promise<boolean> {
    const delivery = await this.findById(ctx, id);
    if (!delivery || !['pending', 'retrying'].includes(delivery.status)) {
      return false;
    }

    delivery.status = 'cancelled';
    delivery.completedAt = new Date();
    await this.repository.save(delivery);

    return true;
  }

  /**
   * Manually retry a failed delivery
   */
  async retry(ctx: ServiceContext, id: string): Promise<WebhookDelivery | null> {
    const delivery = await this.findById(ctx, id);
    if (!delivery || delivery.status !== 'failed') {
      return null;
    }

    // Reset for retry
    delivery.status = 'pending';
    delivery.attemptNumber = 0;
    delivery.nextRetryAt = undefined as any;
    delivery.errorMessage = undefined as any;
    delivery.completedAt = undefined as any;
    delivery.scheduledAt = new Date();

    return this.repository.save(delivery);
  }

  /**
   * Get delivery statistics for tenant
   */
  async getStats(ctx: ServiceContext, dateRange?: { from: Date; to: Date }): Promise<DeliveryStats> {
    const qb = this.repository
      .createQueryBuilder('d')
      .where('d.tenant_id = :tenantId', { tenantId: ctx.tenantId });

    if (dateRange) {
      qb.andWhere('d.created_at BETWEEN :from AND :to', {
        from: dateRange.from,
        to: dateRange.to,
      });
    }

    const deliveries = await qb.getMany();

    const stats: DeliveryStats = {
      total: deliveries.length,
      pending: deliveries.filter((d) => d.status === 'pending').length,
      delivered: deliveries.filter((d) => d.status === 'delivered').length,
      failed: deliveries.filter((d) => d.status === 'failed').length,
      retrying: deliveries.filter((d) => d.status === 'retrying').length,
      avgResponseTimeMs: 0,
      successRate: 0,
    };

    const completedDeliveries = deliveries.filter(
      (d) => d.status === 'delivered' || d.status === 'failed'
    );

    if (completedDeliveries.length > 0) {
      const totalResponseTime = completedDeliveries
        .filter((d) => d.responseTimeMs)
        .reduce((sum, d) => sum + (d.responseTimeMs || 0), 0);

      stats.avgResponseTimeMs = Math.round(totalResponseTime / completedDeliveries.length);
      stats.successRate = (stats.delivered / completedDeliveries.length) * 100;
    }

    return stats;
  }

  /**
   * Get deliveries by event ID
   */
  async findByEventId(ctx: ServiceContext, eventId: string): Promise<WebhookDelivery[]> {
    return this.repository.find({
      where: { eventId, tenantId: ctx.tenantId },
      relations: ['endpoint'],
      order: { createdAt: 'DESC' },
    });
  }

  /**
   * Get deliveries by endpoint ID
   */
  async findByEndpointId(
    ctx: ServiceContext,
    endpointId: string,
    limit: number = 50
  ): Promise<WebhookDelivery[]> {
    return this.repository.find({
      where: { endpointId, tenantId: ctx.tenantId },
      order: { createdAt: 'DESC' },
      take: limit,
    });
  }

  /**
   * Delete old deliveries for cleanup
   */
  async deleteOldDeliveries(olderThan: Date): Promise<number> {
    const result = await this.repository
      .createQueryBuilder()
      .delete()
      .from(WebhookDelivery)
      .where('created_at < :olderThan', { olderThan })
      .andWhere('status IN (:...statuses)', { statuses: ['delivered', 'failed', 'cancelled'] })
      .execute();

    return result.affected || 0;
  }

  /**
   * Get count of pending retries
   */
  async getPendingRetryCount(ctx: ServiceContext): Promise<number> {
    return this.repository.count({
      where: {
        tenantId: ctx.tenantId,
        status: 'retrying',
      },
    });
  }
}
