/**
 * Webhook Service
 *
 * Handles webhook management and triggering
 */

import { Injectable, Logger, Inject } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { v4 as uuidv4 } from 'uuid';
import * as crypto from 'crypto';
import { firstValueFrom } from 'rxjs';
import { Webhook, WebhookEvent } from '@domain/entities/webhook.entity';
import {
  WebhookRepository,
  WEBHOOK_REPOSITORY,
  WebhookFilters,
} from '@domain/ports/out/webhook.repository';

export interface CreateWebhookInput {
  organizationId: string;
  url: string;
  events: WebhookEvent[];
  description?: string;
  headers?: Record<string, string>;
}

export interface UpdateWebhookInput {
  url?: string;
  events?: WebhookEvent[];
  description?: string;
  headers?: Record<string, string>;
}

export interface WebhookPayload {
  event: WebhookEvent;
  timestamp: string;
  data: any;
  organizationId: string;
}

@Injectable()
export class WebhookService {
  private readonly logger = new Logger(WebhookService.name);
  private readonly MAX_RETRIES = 3;
  private readonly RETRY_DELAY_MS = 5000;

  constructor(
    @Inject(WEBHOOK_REPOSITORY)
    private readonly webhookRepository: WebhookRepository,
    private readonly httpService: HttpService
  ) {}

  /**
   * Create a new webhook
   */
  async createWebhook(input: CreateWebhookInput): Promise<Webhook> {
    const secret = this.generateSecret();

    const webhook = Webhook.create({
      id: uuidv4(),
      organizationId: input.organizationId,
      url: input.url,
      events: input.events,
      secret,
      description: input.description,
      headers: input.headers,
    });

    await this.webhookRepository.save(webhook);

    this.logger.log(`Webhook created: ${webhook.id} for organization ${input.organizationId}`);

    return webhook;
  }

  /**
   * Get webhook by ID
   */
  async getWebhookById(id: string): Promise<Webhook | null> {
    return this.webhookRepository.findById(id);
  }

  /**
   * Get webhooks by organization
   */
  async getWebhooksByOrganization(organizationId: string): Promise<Webhook[]> {
    return this.webhookRepository.findByOrganization(organizationId);
  }

  /**
   * Get webhooks with filters
   */
  async getWebhooks(filters: WebhookFilters): Promise<Webhook[]> {
    return this.webhookRepository.findByFilters(filters);
  }

  /**
   * Update webhook
   */
  async updateWebhook(id: string, updates: UpdateWebhookInput): Promise<Webhook> {
    const webhook = await this.webhookRepository.findById(id);
    if (!webhook) {
      throw new Error('Webhook not found');
    }

    const updatedWebhook = webhook.update(updates);
    await this.webhookRepository.save(updatedWebhook);

    this.logger.log(`Webhook updated: ${id}`);

    return updatedWebhook;
  }

  /**
   * Activate webhook
   */
  async activateWebhook(id: string): Promise<void> {
    const webhook = await this.webhookRepository.findById(id);
    if (!webhook) {
      throw new Error('Webhook not found');
    }

    const activatedWebhook = webhook.activate();
    await this.webhookRepository.save(activatedWebhook);

    this.logger.log(`Webhook activated: ${id}`);
  }

  /**
   * Deactivate webhook
   */
  async deactivateWebhook(id: string): Promise<void> {
    const webhook = await this.webhookRepository.findById(id);
    if (!webhook) {
      throw new Error('Webhook not found');
    }

    const deactivatedWebhook = webhook.deactivate();
    await this.webhookRepository.save(deactivatedWebhook);

    this.logger.log(`Webhook deactivated: ${id}`);
  }

  /**
   * Delete webhook
   */
  async deleteWebhook(id: string): Promise<void> {
    await this.webhookRepository.delete(id);
    this.logger.log(`Webhook deleted: ${id}`);
  }

  /**
   * Trigger webhooks for an event
   */
  async triggerWebhooks(event: WebhookEvent, organizationId: string, data: any): Promise<void> {
    try {
      const webhooks = await this.webhookRepository.findActiveByEvent(event, organizationId);

      if (webhooks.length === 0) {
        this.logger.debug(`No active webhooks found for event: ${event}`);
        return;
      }

      const payload: WebhookPayload = {
        event,
        timestamp: new Date().toISOString(),
        data,
        organizationId,
      };

      // Trigger all webhooks in parallel
      await Promise.allSettled(webhooks.map(webhook => this.triggerWebhook(webhook, payload)));

      this.logger.log(`Triggered ${webhooks.length} webhooks for event: ${event}`);
    } catch (error: any) {
      this.logger.error(
        `Error triggering webhooks: ${error?.message || 'Unknown error'}`,
        error?.stack
      );
    }
  }

  /**
   * Trigger a single webhook with retries
   */
  private async triggerWebhook(webhook: Webhook, payload: WebhookPayload): Promise<void> {
    let lastError: Error | null = null;

    for (let attempt = 0; attempt < this.MAX_RETRIES; attempt++) {
      try {
        if (attempt > 0) {
          await this.delay(this.RETRY_DELAY_MS * attempt);
        }

        // Generate signature
        const signature = this.generateSignature(payload, webhook.secret);

        // Prepare headers
        const headers = {
          'Content-Type': 'application/json',
          'X-Webhook-Signature': signature,
          'X-Webhook-Event': payload.event,
          'X-Webhook-Timestamp': payload.timestamp,
          ...webhook.headers,
        };

        // Send HTTP request
        const response = await firstValueFrom(
          this.httpService.post(webhook.url, payload, {
            headers,
            timeout: 10000, // 10 seconds
          })
        );

        if (response && response.status >= 200 && response.status < 300) {
          // Success - record trigger
          const updatedWebhook = webhook.recordTrigger();
          await this.webhookRepository.save(updatedWebhook);

          this.logger.log(`Webhook triggered successfully: ${webhook.id} (attempt ${attempt + 1})`);
          return;
        }

        lastError = new Error(
          `HTTP ${response?.status || 'Unknown'}: ${response?.statusText || 'Unknown error'}`
        );
      } catch (error: any) {
        lastError = error;
        this.logger.warn(
          `Webhook trigger attempt ${attempt + 1} failed: ${webhook.id} - ${error?.message}`
        );
      }
    }

    // All retries failed - mark webhook as failed
    const failedWebhook = webhook.markAsFailed();
    await this.webhookRepository.save(failedWebhook);

    this.logger.error(
      `Webhook failed after ${this.MAX_RETRIES} attempts: ${webhook.id} - ${lastError?.message}`
    );
  }

  /**
   * Generate webhook secret
   */
  private generateSecret(): string {
    return crypto.randomBytes(32).toString('hex');
  }

  /**
   * Generate HMAC signature for webhook payload
   */
  private generateSignature(payload: any, secret: string): string {
    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(JSON.stringify(payload));
    return hmac.digest('hex');
  }

  /**
   * Verify webhook signature
   */
  verifySignature(payload: any, signature: string, secret: string): boolean {
    const expectedSignature = this.generateSignature(payload, secret);
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
  }

  /**
   * Delay helper for retries
   */
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}
