// Webhook Service
// Manage webhook subscriptions and event delivery

import { Prisma } from '@prisma/client';
import prisma from '@/shared/database/prisma';
import { v4 as uuidv4 } from 'uuid';
import crypto from 'crypto';
import { DbisError, ErrorCode } from '@/shared/types';
import {
  WebhookSubscription,
  WebhookSubscriptionCreateRequest,
  WebhookEvent,
  WebhookEventType,
} from './nostro-vostro.types';

export class WebhookService {
  /**
   * Create webhook subscription
   */
  async createSubscription(request: WebhookSubscriptionCreateRequest): Promise<WebhookSubscription> {
    // Verify participant exists
    const participant = await prisma.nostroVostroParticipant.findUnique({
      where: { participantId: request.participantId },
    });

    if (!participant) {
      throw new DbisError(ErrorCode.NOT_FOUND, `Participant ${request.participantId} not found`);
    }

    // Validate webhook URL
    try {
      new URL(request.webhookUrl);
    } catch {
      throw new DbisError(ErrorCode.VALIDATION_ERROR, `Invalid webhook URL: ${request.webhookUrl}`);
    }

    const subscriptionId = `WH-${uuidv4()}`;
    const secret = crypto.randomBytes(32).toString('hex'); // Generate webhook secret

    const subscription = await prisma.nostroVostroWebhookSubscription.create({
      data: {
        subscriptionId,
        participantId: request.participantId,
        webhookUrl: request.webhookUrl,
        eventTypes: request.eventTypes,
        secret,
        status: 'ACTIVE',
        metadata: (request.metadata || {}) as Prisma.InputJsonValue,
      },
    });

    return this.mapToSubscription(subscription);
  }

  /**
   * Get subscription by ID
   */
  async getSubscription(subscriptionId: string): Promise<WebhookSubscription | null> {
    const subscription = await prisma.nostroVostroWebhookSubscription.findUnique({
      where: { subscriptionId },
    });

    return subscription ? this.mapToSubscription(subscription) : null;
  }

  /**
   * List subscriptions for a participant
   */
  async listSubscriptions(
    participantId: string,
    filters?: {
      status?: string;
      page?: number;
      pageSize?: number;
    }
  ): Promise<{
    data: WebhookSubscription[];
    pagination: {
      page: number;
      pageSize: number;
      total: number;
      totalPages: number;
    };
  }> {
    const page = filters?.page || 1;
    const pageSize = filters?.pageSize || 50;
    const skip = (page - 1) * pageSize;

    const where: any = {
      participantId,
    };

    if (filters?.status) {
      where.status = filters.status;
    }

    const [subscriptions, total] = await Promise.all([
      prisma.nostroVostroWebhookSubscription.findMany({
        where,
        skip,
        take: pageSize,
        orderBy: { createdAt: 'desc' },
      }),
      prisma.nostroVostroWebhookSubscription.count({ where }),
    ]);

    return {
      data: subscriptions.map((s) => this.mapToSubscription(s)),
      pagination: {
        page,
        pageSize,
        total,
        totalPages: Math.ceil(total / pageSize),
      },
    };
  }

  /**
   * Update subscription status
   */
  async updateSubscriptionStatus(
    subscriptionId: string,
    status: 'ACTIVE' | 'SUSPENDED' | 'INACTIVE'
  ): Promise<WebhookSubscription> {
    const subscription = await prisma.nostroVostroWebhookSubscription.findUnique({
      where: { subscriptionId },
    });

    if (!subscription) {
      throw new DbisError(ErrorCode.NOT_FOUND, `Subscription ${subscriptionId} not found`);
    }

    const updated = await prisma.nostroVostroWebhookSubscription.update({
      where: { subscriptionId },
      data: { status },
    });

    return this.mapToSubscription(updated);
  }

  /**
   * Delete subscription
   */
  async deleteSubscription(subscriptionId: string): Promise<void> {
    const subscription = await prisma.nostroVostroWebhookSubscription.findUnique({
      where: { subscriptionId },
    });

    if (!subscription) {
      throw new DbisError(ErrorCode.NOT_FOUND, `Subscription ${subscriptionId} not found`);
    }

    await prisma.nostroVostroWebhookSubscription.delete({
      where: { subscriptionId },
    });
  }

  /**
   * Create and queue webhook event
   */
  async createWebhookEvent(
    subscriptionId: string,
    eventType: WebhookEventType,
    payload: Record<string, unknown>
  ): Promise<WebhookEvent> {
    const subscription = await prisma.nostroVostroWebhookSubscription.findUnique({
      where: { subscriptionId },
    });

    if (!subscription) {
      throw new DbisError(ErrorCode.NOT_FOUND, `Subscription ${subscriptionId} not found`);
    }

    if (subscription.status !== 'ACTIVE') {
      throw new DbisError(ErrorCode.VALIDATION_ERROR, `Subscription ${subscriptionId} is not active`);
    }

    // Check if event type is subscribed
    if (!subscription.eventTypes.includes(eventType)) {
      throw new DbisError(
        ErrorCode.VALIDATION_ERROR,
        `Event type ${eventType} is not subscribed for subscription ${subscriptionId}`
      );
    }

    const eventId = `EVT-${uuidv4()}`;

    const event = await prisma.nostroVostroWebhookEvent.create({
      data: {
        eventId,
        subscriptionId,
        eventType,
        payload,
        status: 'PENDING',
        deliveryAttempts: 0,
      },
    });

    // Queue for delivery (in production, this would use a job queue)
    // For now, we'll deliver synchronously
    this.deliverWebhookEvent(eventId).catch((error) => {
      console.error(`Failed to deliver webhook event ${eventId}:`, error);
    });

    return this.mapToEvent(event);
  }

  /**
   * Deliver webhook event (internal method)
   */
  private async deliverWebhookEvent(eventId: string): Promise<void> {
    const event = await prisma.nostroVostroWebhookEvent.findUnique({
      where: { eventId },
      include: { subscription: true },
    });

    if (!event || event.status === 'DELIVERED') {
      return;
    }

    const subscription = event.subscription;

    try {
      // Generate signature
      const timestamp = Date.now();
      const payload = JSON.stringify(event.payload);
      const signature = this.generateSignature(payload, subscription.secret, timestamp);

      // Deliver webhook
      const response = await fetch(subscription.webhookUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-DBIS-Event-Type': event.eventType,
          'X-DBIS-Event-ID': event.eventId,
          'X-DBIS-Timestamp': timestamp.toString(),
          'X-DBIS-Signature': signature,
        },
        body: JSON.stringify({
          eventId: event.eventId,
          eventType: event.eventType,
          timestamp: new Date().toISOString(),
          payload: event.payload,
        }),
      });

      if (response.ok) {
        await prisma.nostroVostroWebhookEvent.update({
          where: { eventId },
          data: {
            status: 'DELIVERED',
            deliveredAt: new Date(),
            deliveryAttempts: { increment: 1 },
            lastAttemptAt: new Date(),
          },
        });

        await prisma.nostroVostroWebhookSubscription.update({
          where: { subscriptionId: subscription.subscriptionId },
          data: {
            lastDeliveryAt: new Date(),
            failureCount: 0,
          },
        });
      } else {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
    } catch (error) {
      const attempts = event.deliveryAttempts + 1;
      const maxAttempts = 5;

      if (attempts >= maxAttempts) {
        await prisma.nostroVostroWebhookEvent.update({
          where: { eventId },
          data: {
            status: 'FAILED',
            deliveryAttempts: attempts,
            lastAttemptAt: new Date(),
            errorMessage: error instanceof Error ? error.message : 'Unknown error',
          },
        });

        await prisma.nostroVostroWebhookSubscription.update({
          where: { subscriptionId: subscription.subscriptionId },
          data: {
            failureCount: { increment: 1 },
          },
        });
      } else {
        await prisma.nostroVostroWebhookEvent.update({
          where: { eventId },
          data: {
            status: 'RETRYING',
            deliveryAttempts: attempts,
            lastAttemptAt: new Date(),
            errorMessage: error instanceof Error ? error.message : 'Unknown error',
          },
        });
      }
    }
  }

  /**
   * Generate webhook signature
   */
  private generateSignature(payload: string, secret: string, timestamp: number): string {
    const message = `${timestamp}.${payload}`;
    return crypto.createHmac('sha256', secret).update(message).digest('hex');
  }

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

  /**
   * Get webhook events for a subscription
   */
  async listEvents(
    subscriptionId: string,
    filters?: {
      eventType?: string;
      status?: string;
      page?: number;
      pageSize?: number;
    }
  ): Promise<{
    data: WebhookEvent[];
    pagination: {
      page: number;
      pageSize: number;
      total: number;
      totalPages: number;
    };
  }> {
    const page = filters?.page || 1;
    const pageSize = filters?.pageSize || 50;
    const skip = (page - 1) * pageSize;

    const where: any = {
      subscriptionId,
    };

    if (filters?.eventType) {
      where.eventType = filters.eventType;
    }

    if (filters?.status) {
      where.status = filters.status;
    }

    const [events, total] = await Promise.all([
      prisma.nostroVostroWebhookEvent.findMany({
        where,
        skip,
        take: pageSize,
        orderBy: { createdAt: 'desc' },
      }),
      prisma.nostroVostroWebhookEvent.count({ where }),
    ]);

    return {
      data: events.map((e) => this.mapToEvent(e)),
      pagination: {
        page,
        pageSize,
        total,
        totalPages: Math.ceil(total / pageSize),
      },
    };
  }

  /**
   * Map Prisma model to Subscription type
   */
  private mapToSubscription(subscription: any): WebhookSubscription {
    return {
      subscriptionId: subscription.subscriptionId,
      participantId: subscription.participantId,
      webhookUrl: subscription.webhookUrl,
      eventTypes: subscription.eventTypes as WebhookEventType[],
      status: subscription.status as 'ACTIVE' | 'SUSPENDED' | 'INACTIVE',
      lastDeliveryAt: subscription.lastDeliveryAt || undefined,
      failureCount: subscription.failureCount,
      metadata: subscription.metadata as Record<string, unknown> | undefined,
      createdAt: subscription.createdAt,
      updatedAt: subscription.updatedAt,
    };
  }

  /**
   * Map Prisma model to Event type
   */
  private mapToEvent(event: any): WebhookEvent {
    return {
      eventId: event.eventId,
      subscriptionId: event.subscriptionId,
      eventType: event.eventType as WebhookEventType,
      payload: event.payload as Record<string, unknown>,
      status: event.status as 'PENDING' | 'DELIVERED' | 'FAILED' | 'RETRYING',
      deliveryAttempts: event.deliveryAttempts,
      lastAttemptAt: event.lastAttemptAt || undefined,
      deliveredAt: event.deliveredAt || undefined,
      errorMessage: event.errorMessage || undefined,
      createdAt: event.createdAt,
      updatedAt: event.updatedAt,
    };
  }
}

export const webhookService = new WebhookService();

