/**
 * WebhookEventService - Webhook Event Management
 *
 * Handles creation and dispatch of webhook events.
 * Creates events and queues them for delivery to subscribed endpoints.
 *
 * @module Webhooks
 */

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

// DTOs
export interface CreateEventDto {
  eventType: string;
  payload: Record<string, any>;
  resourceType?: string;
  resourceId?: string;
  idempotencyKey?: string;
  metadata?: Record<string, any>;
  expiresAt?: Date;
}

export interface EventFilters {
  eventType?: string;
  status?: WebhookEventStatus | WebhookEventStatus[];
  resourceType?: string;
  resourceId?: string;
  dateFrom?: Date;
  dateTo?: Date;
  page?: number;
  limit?: number;
}

export interface DispatchResult {
  eventId: string;
  endpointsMatched: number;
  deliveriesCreated: number;
}

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

  /**
   * Generate idempotency key for deduplication
   */
  private generateIdempotencyKey(dto: CreateEventDto, tenantId: string): string {
    const data = `${tenantId}:${dto.eventType}:${dto.resourceType || ''}:${dto.resourceId || ''}:${Date.now()}`;
    return crypto.createHash('sha256').update(data).digest('hex').substring(0, 32);
  }

  /**
   * Create a webhook event and queue for delivery
   */
  async create(ctx: ServiceContext, dto: CreateEventDto): Promise<WebhookEvent> {
    // Check for duplicate using idempotency key
    if (dto.idempotencyKey) {
      const existing = await this.repository.findOne({
        where: { tenantId: ctx.tenantId, idempotencyKey: dto.idempotencyKey },
      });

      if (existing) {
        return existing;
      }
    }

    const event = this.repository.create({
      tenantId: ctx.tenantId,
      eventType: dto.eventType,
      payload: dto.payload,
      resourceType: dto.resourceType,
      resourceId: dto.resourceId,
      triggeredBy: ctx.userId,
      status: 'pending',
      idempotencyKey: dto.idempotencyKey || this.generateIdempotencyKey(dto, ctx.tenantId),
      metadata: dto.metadata || {},
      expiresAt: dto.expiresAt,
    });

    return this.repository.save(event);
  }

  /**
   * Create and dispatch event to all subscribed endpoints
   */
  async createAndDispatch(ctx: ServiceContext, dto: CreateEventDto): Promise<DispatchResult> {
    const event = await this.create(ctx, dto);

    // Find all endpoints subscribed to this event type
    const endpoints = await this.endpointRepository
      .createQueryBuilder('e')
      .where('e.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('e.is_active = true')
      .andWhere(':eventType = ANY(e.subscribed_events)', { eventType: dto.eventType })
      .getMany();

    let deliveriesCreated = 0;

    // Create delivery records for each endpoint
    for (const endpoint of endpoints) {
      // Apply endpoint filters if any
      if (!this.matchesFilters(dto.payload, endpoint.filters)) {
        continue;
      }

      const delivery = this.deliveryRepository.create({
        endpointId: endpoint.id,
        tenantId: ctx.tenantId,
        eventType: dto.eventType,
        eventId: event.id,
        payload: dto.payload,
        payloadHash: this.hashPayload(dto.payload),
        requestUrl: endpoint.url,
        requestMethod: endpoint.httpMethod,
        requestHeaders: this.buildRequestHeaders(endpoint),
        status: 'pending',
        maxAttempts: endpoint.retryEnabled ? endpoint.maxRetries : 1,
        scheduledAt: new Date(),
      });

      await this.deliveryRepository.save(delivery);
      deliveriesCreated++;
    }

    // Update event status
    event.status = deliveriesCreated > 0 ? 'processing' : 'dispatched';
    event.dispatchedEndpoints = deliveriesCreated;
    event.processedAt = new Date();
    await this.repository.save(event);

    return {
      eventId: event.id,
      endpointsMatched: endpoints.length,
      deliveriesCreated,
    };
  }

  /**
   * Check if payload matches endpoint filters
   */
  private matchesFilters(
    payload: Record<string, any>,
    filters: Record<string, any>
  ): boolean {
    if (!filters || Object.keys(filters).length === 0) {
      return true;
    }

    for (const [key, value] of Object.entries(filters)) {
      const payloadValue = this.getNestedValue(payload, key);
      if (payloadValue !== value) {
        return false;
      }
    }

    return true;
  }

  /**
   * Get nested value from object using dot notation
   */
  private getNestedValue(obj: Record<string, any>, path: string): any {
    return path.split('.').reduce((current, key) => current?.[key], obj);
  }

  /**
   * Hash payload for deduplication
   */
  private hashPayload(payload: Record<string, any>): string {
    return crypto
      .createHash('sha256')
      .update(JSON.stringify(payload))
      .digest('hex');
  }

  /**
   * Build request headers for endpoint
   */
  private buildRequestHeaders(endpoint: WebhookEndpoint): Record<string, string> {
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      'User-Agent': 'ERP-Construccion-Webhooks/1.0',
      'X-Webhook-Event': '', // Will be set by delivery service
      'X-Webhook-Delivery': '', // Will be set by delivery service
    };

    // Add custom headers
    if (endpoint.customHeaders) {
      Object.assign(headers, endpoint.customHeaders);
    }

    return headers;
  }

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

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

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

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

    if (filters.resourceType) {
      qb.andWhere('e.resource_type = :resourceType', { resourceType: filters.resourceType });
    }

    if (filters.resourceId) {
      qb.andWhere('e.resource_id = :resourceId', { resourceId: filters.resourceId });
    }

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

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

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

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

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

  /**
   * Find event by ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<WebhookEvent | null> {
    return this.repository.findOne({
      where: { id, tenantId: ctx.tenantId },
    });
  }

  /**
   * Find pending events for processing
   */
  async findPendingEvents(limit: number = 100): Promise<WebhookEvent[]> {
    return this.repository.find({
      where: { status: 'pending' },
      order: { createdAt: 'ASC' },
      take: limit,
    });
  }

  /**
   * Update event status
   */
  async updateStatus(
    id: string,
    status: WebhookEventStatus,
    failedEndpoints?: number
  ): Promise<void> {
    const update: Partial<WebhookEvent> = { status };

    if (status === 'dispatched' || status === 'failed') {
      update.processedAt = new Date();
    }

    if (failedEndpoints !== undefined) {
      update.failedEndpoints = failedEndpoints;
    }

    await this.repository.update(id, update);
  }

  /**
   * Delete expired events
   */
  async deleteExpired(): Promise<number> {
    const result = await this.repository
      .createQueryBuilder()
      .delete()
      .from(WebhookEvent)
      .where('expires_at IS NOT NULL')
      .andWhere('expires_at < :now', { now: new Date() })
      .execute();

    return result.affected || 0;
  }

  /**
   * Get event counts by status
   */
  async getCountsByStatus(ctx: ServiceContext): Promise<Record<string, number>> {
    const results = await this.repository
      .createQueryBuilder('e')
      .select('e.status', 'status')
      .addSelect('COUNT(*)', 'count')
      .where('e.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .groupBy('e.status')
      .getRawMany();

    const counts: Record<string, number> = {};
    for (const r of results) {
      counts[r.status] = parseInt(r.count, 10);
    }

    return counts;
  }

  /**
   * Retry failed event
   */
  async retryEvent(ctx: ServiceContext, id: string): Promise<DispatchResult | null> {
    const event = await this.findById(ctx, id);
    if (!event || event.status !== 'failed') {
      return null;
    }

    // Reset event status
    event.status = 'pending';
    event.processedAt = undefined as any;
    event.failedEndpoints = 0;
    await this.repository.save(event);

    // Re-dispatch
    return this.createAndDispatch(ctx, {
      eventType: event.eventType,
      payload: event.payload,
      resourceType: event.resourceType,
      resourceId: event.resourceId,
      idempotencyKey: `${event.idempotencyKey}-retry-${Date.now()}`,
      metadata: { ...event.metadata, retriedFrom: event.id },
    });
  }
}
