/**
 * Webhook 服务
 * 
 * 管理 Webhook 订阅和事件投递
 */

import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { SkipAssertAccess } from '@common/decorators/skip-assert-access.decorator';
import * as crypto from 'crypto';
import {
  CreateWebhookDto,
  UpdateWebhookDto,
  TestWebhookDto,
  WebhookResponse,
  WebhookLogResponse,
  QueryWebhookLogsDto,
} from '../dto/webhook.dto';
import { PaginatedResponse } from '../dto/query.dto';
import { RegionId } from '../decorators/region.decorator';
import { WebhookNotFoundException } from '../exceptions';
import { safeFindUnique } from '../utils/prisma-error-handler';

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

  constructor(private readonly prisma: PrismaService) {}

  // ============================================
  // 1. 创建 Webhook
  // ============================================

  async create(
    dto: CreateWebhookDto,
    organizationId: string | null,
    userId: string,
  ): Promise<WebhookResponse> {
    this.logger.log(`Creating webhook: ${dto.name} for organization ${organizationId || 'platform'}`);

    // 生成密钥
    const secret = this.generateSecret();

    const webhook = await this.prisma.formWebhook.create({
      data: {
        organizationId,
        name: dto.name,
        description: dto.description,
        url: dto.url,
        secret,
        events: dto.events,
        headers: dto.headers,
        maxRetries: dto.maxRetries ?? 3,
        timeoutMs: dto.timeoutMs ?? 5000,
        enabled: true,
        createdBy: userId,
      },
    });

    return this.mapToResponse(webhook);
  }

  // ============================================
  // 2. 获取 Webhook 列表
  // ============================================

  async findAll(organizationId?: string | null): Promise<WebhookResponse[]> {
    const where: any = {};
    if (organizationId !== undefined) {
      where.organizationId = organizationId;
    }
    
    const webhooks = await this.prisma.formWebhook.findMany({
      where,
      orderBy: { createdAt: 'desc' },
    });

    return webhooks.map((w) => this.mapToResponse(w));
  }

  // ============================================
  // 3. 获取 Webhook 详情
  // ============================================

  async findOne(id: string, organizationId?: string | null): Promise<WebhookResponse> {
    const webhook = await safeFindUnique(() =>
      this.prisma.formWebhook.findUnique({ where: { id } }),
    );

    if (!webhook) {
      throw new WebhookNotFoundException(id);
    }
    
    // 验证组织访问权限（如果指定了 organizationId）
    if (organizationId !== undefined && webhook.organizationId !== organizationId) {
      throw new WebhookNotFoundException(id);
    }

    return this.mapToResponse(webhook);
  }

  // ============================================
  // 4. 更新 Webhook
  // ============================================

  async update(
    id: string,
    dto: UpdateWebhookDto,
    organizationId?: string | null,
    userId?: string,
  ): Promise<WebhookResponse> {
    const webhook = await safeFindUnique(() =>
      this.prisma.formWebhook.findUnique({ where: { id } }),
    );

    if (!webhook) {
      throw new WebhookNotFoundException(id);
    }
    
    // 验证组织访问权限
    if (organizationId !== undefined && webhook.organizationId !== organizationId) {
      throw new WebhookNotFoundException(id);
    }

    const updated = await this.prisma.formWebhook.update({
      where: { id },
      data: {
        ...dto,
        updatedBy: userId,
      },
    });

    return this.mapToResponse(updated);
  }

  // ============================================
  // 5. 删除 Webhook
  // ============================================

  @SkipAssertAccess('Webhook 是平台级资源（organizationId 可为 null），删除权限由 Controller 层 @RequirePermissions(form:admin) 统一管控，无 IDOR 风险')
  async remove(id: string): Promise<{ deleted: boolean }> {
    const webhook = await safeFindUnique(() =>
      this.prisma.formWebhook.findUnique({ where: { id } }),
    );

    if (!webhook) {
      throw new WebhookNotFoundException(id);
    }

    await this.prisma.formWebhook.delete({ where: { id } });

    return { deleted: true };
  }

  // ============================================
  // 6. 发送测试事件
  // ============================================

  async sendTestEvent(
    id: string,
    dto: TestWebhookDto,
  ): Promise<WebhookLogResponse> {
    const webhook = await safeFindUnique(() =>
      this.prisma.formWebhook.findUnique({ where: { id } }),
    );

    if (!webhook) {
      throw new WebhookNotFoundException(id);
    }

    const eventType = dto.eventType || 'form.test';
    const payload = dto.testPayload || {
      event: eventType,
      timestamp: new Date().toISOString(),
      data: {
        message: 'This is a test event',
        webhookId: id,
      },
    };

    // 执行 HTTP 请求
    const result = await this.executeWebhook(webhook, eventType, payload);

    // 记录日志
    const log = await this.prisma.formWebhookLog.create({
      data: {
        webhookId: id,
        eventType,
        payload,
        statusCode: result.statusCode,
        response: result.response,
        success: result.success,
        error: result.error,
        duration: result.duration,
      },
    });

    return this.mapLogToResponse(log);
  }

  // ============================================
  // 7. 获取投递日志
  // ============================================

  async findLogs(
    id: string,
    query: QueryWebhookLogsDto,
  ): Promise<PaginatedResponse<WebhookLogResponse>> {
    const webhook = await safeFindUnique(() =>
      this.prisma.formWebhook.findUnique({ where: { id } }),
    );

    if (!webhook) {
      throw new WebhookNotFoundException(id);
    }

    const { page = 1, limit = 20, eventType, success } = query;

    const where: any = { webhookId: id };
    if (eventType) {
      where.eventType = eventType;
    }
    if (success !== undefined) {
      where.success = success;
    }

    const [logs, total] = await Promise.all([
      this.prisma.formWebhookLog.findMany({
        where,
        skip: (page - 1) * limit,
        take: limit,
        orderBy: { createdAt: 'desc' },
      }),
      this.prisma.formWebhookLog.count({ where }),
    ]);

    const totalPages = Math.ceil(total / limit);

    return {
      items: logs.map((l) => this.mapLogToResponse(l)),
      total,
      page,
      limit,
      totalPages,
      hasNext: page < totalPages,
      hasPrev: page > 1,
    };
  }

  // ============================================
  // 8. 触发事件（内部使用）
  // ============================================

  async triggerEvent(
    eventType: string,
    payload: any,
    regionId: RegionId,
  ): Promise<void> {
    this.logger.log(`Triggering event: ${eventType} in region ${regionId}`);

    // 查找订阅了该事件的所有启用的 Webhook
    // 注意：events 是 jsonb 数组（如 ["form.instance.submitted"]）。
    // Prisma `array_contains` 对应 PostgreSQL `@>`，不需要 path（path 用于导航到嵌套 key）。
    // 之前用 path: ['$'] 是 MongoDB jsonpath 语法，在 PG 下被当作查 key 名为 "$" 的字段，永远不命中。
    const webhooks = await this.prisma.formWebhook.findMany({
      where: {
        enabled: true,
        events: { array_contains: eventType },
      },
    });

    // 首轮同步等待，便于调用方观察结果；失败重试 fire-and-forget
    await Promise.allSettled(
      webhooks.map((webhook) =>
        this.deliverAndLog(webhook, eventType, payload, 0).then((result) => {
          if (!result.success && webhook.maxRetries > 0) {
            void this.scheduleRetries(webhook, eventType, payload).catch((err) =>
              this.logger.error(
                `Webhook ${webhook.id} retry chain crashed: ${err.message}`,
                err.stack,
              ),
            );
          }
        }),
      ),
    );
  }

  /// 执行一次投递并写日志，返回投递结果。
  private async deliverAndLog(
    webhook: any,
    eventType: string,
    payload: any,
    retryCount: number,
  ): Promise<{ success: boolean; error?: string }> {
    try {
      const result = await this.executeWebhook(webhook, eventType, payload);
      await this.prisma.formWebhookLog.create({
        data: {
          webhookId: webhook.id,
          eventType,
          payload,
          statusCode: result.statusCode,
          response: result.response,
          success: result.success,
          error: result.error,
          duration: result.duration,
          retryCount,
        },
      });
      return { success: result.success, error: result.error };
    } catch (error) {
      this.logger.error(
        `Webhook ${webhook.id} delivery error (attempt ${retryCount}): ${error.message}`,
      );
      return { success: false, error: error.message };
    }
  }

  /// 进程内 setTimeout 重试链；进程崩溃丢失剩余尝试（限制留待迁 bullmq）
  private async scheduleRetries(
    webhook: any,
    eventType: string,
    payload: any,
  ): Promise<void> {
    for (let attempt = 1; attempt <= webhook.maxRetries; attempt++) {
      const delayMs = Math.min(2 ** attempt * 1000, 60_000);
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      const result = await this.deliverAndLog(webhook, eventType, payload, attempt);
      if (result.success) {
        this.logger.log(
          `Webhook ${webhook.id} succeeded on retry ${attempt}/${webhook.maxRetries}`,
        );
        return;
      }
      this.logger.warn(
        `Webhook ${webhook.id} retry ${attempt}/${webhook.maxRetries} failed: ${result.error}`,
      );
    }
    this.logger.error(
      `Webhook ${webhook.id} exhausted ${webhook.maxRetries} retries`,
    );
  }

  // ============================================
  // 私有辅助方法
  // ============================================

  private generateSecret(): string {
    return crypto.randomBytes(32).toString('hex');
  }

  private generateSignature(payload: any, secret: string): string {
    const payloadString = JSON.stringify(payload);
    return crypto
      .createHmac('sha256', secret)
      .update(payloadString)
      .digest('hex');
  }

  private async executeWebhook(
    webhook: any,
    eventType: string,
    payload: any,
  ): Promise<{
    success: boolean;
    statusCode?: number;
    response?: string;
    error?: string;
    duration?: number;
  }> {
    const startTime = Date.now();

    try {
      const signature = this.generateSignature(payload, webhook.secret);

      const headers: Record<string, string> = {
        'Content-Type': 'application/json',
        'X-Webhook-Event': eventType,
        'X-Webhook-Signature': `sha256=${signature}`,
        'X-Webhook-Timestamp': new Date().toISOString(),
        ...(webhook.headers as Record<string, string> || {}),
      };

      const controller = new AbortController();
      const timeoutId = setTimeout(
        () => controller.abort(),
        webhook.timeoutMs,
      );

      const response = await fetch(webhook.url, {
        method: 'POST',
        headers,
        body: JSON.stringify(payload),
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      const duration = Date.now() - startTime;
      const responseText = await response.text();

      return {
        success: response.ok,
        statusCode: response.status,
        response: responseText.substring(0, 1000), // 限制响应长度
        duration,
      };
    } catch (error) {
      const duration = Date.now() - startTime;
      return {
        success: false,
        error: error.message,
        duration,
      };
    }
  }

  private mapToResponse(webhook: any): WebhookResponse {
    return {
      id: webhook.id,
      organizationId: webhook.organizationId || undefined,
      name: webhook.name,
      description: webhook.description || undefined,
      url: webhook.url,
      events: webhook.events as string[],
      enabled: webhook.enabled,
      maxRetries: webhook.maxRetries,
      timeoutMs: webhook.timeoutMs,
      headers: webhook.headers as Record<string, string> || undefined,
      createdAt: webhook.createdAt.toISOString(),
      updatedAt: webhook.updatedAt.toISOString(),
    };
  }

  private mapLogToResponse(log: any): WebhookLogResponse {
    return {
      id: log.id,
      webhookId: log.webhookId,
      eventType: log.eventType,
      payload: log.payload,
      statusCode: log.statusCode || undefined,
      response: log.response || undefined,
      success: log.success,
      error: log.error || undefined,
      retryCount: log.retryCount,
      duration: log.duration || undefined,
      createdAt: log.createdAt.toISOString(),
    };
  }
}
