import { InjectQueue, Processor, WorkerHost } from "@nestjs/bullmq";
import { Logger } from "@nestjs/common";
import { WebhookEventOutStatus } from "@prisma/client";
import axios, { AxiosError } from "axios";
import { Job, Queue } from "bullmq";
import { EncryptedPayload, EncryptionService } from "../common/crypto/encryption.service";
import { signWebhook } from "../common/crypto/hmac.util";
import { PrismaService } from "../prisma/prisma.service";
import {
  WEBHOOK_DELIVERY_QUEUE,
  WEBHOOK_MAX_ATTEMPTS,
  WEBHOOK_RETRY_SCHEDULE_MIN,
} from "./webhook-events.constants";

interface DeliverJobData {
  eventOutId: string;
}

const USER_AGENT = "NovasisPay-Webhook/1.0";
const REQUEST_TIMEOUT_MS = 15_000;

@Processor(WEBHOOK_DELIVERY_QUEUE, { concurrency: 8 })
export class WebhookDeliveryProcessor extends WorkerHost {
  private readonly logger = new Logger(WebhookDeliveryProcessor.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly encryption: EncryptionService,
    @InjectQueue(WEBHOOK_DELIVERY_QUEUE) private readonly queue: Queue,
  ) {
    super();
  }

  async process(job: Job<DeliverJobData>): Promise<void> {
    const event = await this.prisma.webhookEventOut.findUnique({
      where: { id: job.data.eventOutId },
      include: { endpoint: true },
    });
    if (!event) {
      this.logger.warn(`WebhookEventOut ${job.data.eventOutId} not found, dropping job`);
      return;
    }
    if (
      event.status === WebhookEventOutStatus.delivered ||
      event.status === WebhookEventOutStatus.dead
    ) {
      return;
    }
    if (!event.endpoint.active) {
      await this.prisma.webhookEventOut.update({
        where: { id: event.id },
        data: { status: WebhookEventOutStatus.failed, lastError: "endpoint inactive" },
      });
      return;
    }

    const secret = this.encryption.decrypt(
      JSON.parse(event.endpoint.secretHmac) as EncryptedPayload,
    );
    const rawBody = JSON.stringify(event.payloadJson);
    const signature = signWebhook(rawBody, secret);

    const attemptNumber = event.attempts + 1;
    const now = new Date();

    try {
      await axios.post(event.endpoint.url, rawBody, {
        timeout: REQUEST_TIMEOUT_MS,
        headers: {
          "Content-Type": "application/json",
          "User-Agent": USER_AGENT,
          "X-Payments-Signature": signature,
          "X-Payments-Event-Id": event.id,
          "X-Payments-Event-Type": event.eventType,
        },
        validateStatus: (s) => s >= 200 && s < 300,
      });

      await this.prisma.webhookEventOut.update({
        where: { id: event.id },
        data: {
          status: WebhookEventOutStatus.delivered,
          attempts: attemptNumber,
          lastAttemptAt: now,
          deliveredAt: now,
          lastError: null,
        },
      });
      this.logger.log(`Delivered ${event.eventType} → ${event.endpoint.url} (event ${event.id})`);
    } catch (err) {
      const errorMessage = this.formatError(err);
      const isLastAttempt = attemptNumber >= WEBHOOK_MAX_ATTEMPTS;
      const nextDelayMin = isLastAttempt ? null : WEBHOOK_RETRY_SCHEDULE_MIN[attemptNumber - 1];
      const nextAttemptAt = nextDelayMin
        ? new Date(now.getTime() + nextDelayMin * 60_000)
        : null;

      await this.prisma.webhookEventOut.update({
        where: { id: event.id },
        data: {
          status: isLastAttempt ? WebhookEventOutStatus.dead : WebhookEventOutStatus.pending,
          attempts: attemptNumber,
          lastAttemptAt: now,
          nextAttemptAt,
          lastError: errorMessage,
        },
      });

      if (isLastAttempt) {
        this.logger.error(
          `Webhook ${event.id} → ${event.endpoint.url} DEAD after ${attemptNumber} attempts: ${errorMessage}`,
        );
        return;
      }

      this.logger.warn(
        `Webhook ${event.id} attempt ${attemptNumber} failed (${errorMessage}); retrying in ${nextDelayMin}min`,
      );
      await this.queue.add(
        "deliver",
        { eventOutId: event.id },
        {
          jobId: `${event.id}:${attemptNumber + 1}`,
          delay: (nextDelayMin ?? 0) * 60_000,
          removeOnComplete: true,
          removeOnFail: 1000,
        },
      );
    }
  }

  private formatError(err: unknown): string {
    if (err instanceof AxiosError) {
      if (err.response) return `HTTP ${err.response.status}: ${err.code ?? err.message}`;
      return `${err.code ?? "NETWORK"}: ${err.message}`;
    }
    return err instanceof Error ? err.message : String(err);
  }
}
