import { BadRequestException, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
import {
  PaymentAttempt,
  PaymentAttemptStatus,
  PaymentIntent,
  PaymentIntentStatus,
  Prisma,
  Provider,
} from "@prisma/client";
import { InvoiceService } from "../billing/invoice.service";
import { newId } from "../common/ids/id.util";
import { PrismaService } from "../prisma/prisma.service";
import { ProviderRouterService } from "../providers/provider-router.service";
import { WebhookEventService } from "../webhook-events/webhook-event.service";
import { WEBHOOK_EVENT_TYPES } from "../webhook-events/webhook-events.constants";

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly router: ProviderRouterService,
    private readonly webhookEvents: WebhookEventService,
    private readonly invoices: InvoiceService,
  ) {}

  async receive(
    provider: Provider,
    providerConfigId: string,
    rawBody: string,
    headers: Record<string, string>,
  ): Promise<{ received: true; eventId: string; deduped?: boolean }> {
    const config = await this.prisma.merchantProviderConfig.findUnique({
      where: { id: providerConfigId },
    });
    if (!config || config.provider !== provider) {
      throw new BadRequestException("Provider config not found or mismatched provider");
    }

    const route = await this.router.getById(config.merchantId, config.id);
    const parsed = route.adapter.parseWebhook(rawBody, headers);
    if (!parsed) {
      // Persistimos igual para tener trazabilidad de intentos inválidos.
      const failed = await this.prisma.webhookEventIn.create({
        data: {
          id: newId("webhookEventIn"),
          provider,
          payloadJson: this.safeJson(rawBody),
          headersJson: headers as unknown as Prisma.InputJsonValue,
          signature: this.extractSignature(headers),
          processed: false,
          error: "invalid signature or malformed payload",
        },
      });
      this.logger.warn(`Rejected inbound webhook ${failed.id} (provider=${provider})`);
      throw new UnauthorizedException("Invalid webhook signature");
    }

    const attempt = await this.prisma.paymentAttempt.findFirst({
      where: { provider, providerPaymentId: parsed.providerPaymentId },
      include: { intent: true },
      orderBy: { createdAt: "desc" },
    });

    const dedup = attempt
      ? await this.prisma.webhookEventIn.findFirst({
          where: {
            attemptId: attempt.id,
            provider,
            signature: this.extractSignature(headers),
            processed: true,
          },
          select: { id: true },
        })
      : null;

    const eventIn = await this.prisma.webhookEventIn.create({
      data: {
        id: newId("webhookEventIn"),
        provider,
        attemptId: attempt?.id ?? null,
        payloadJson: this.safeJson(rawBody),
        headersJson: headers as unknown as Prisma.InputJsonValue,
        signature: this.extractSignature(headers),
        processed: false,
      },
    });

    if (dedup) {
      await this.prisma.webhookEventIn.update({
        where: { id: eventIn.id },
        data: { processed: true, processedAt: new Date(), error: "deduplicated" },
      });
      return { received: true, eventId: eventIn.id, deduped: true };
    }

    if (!attempt) {
      await this.prisma.webhookEventIn.update({
        where: { id: eventIn.id },
        data: {
          processed: true,
          processedAt: new Date(),
          error: `no attempt for providerPaymentId=${parsed.providerPaymentId}`,
        },
      });
      this.logger.warn(
        `Inbound webhook ${eventIn.id}: no attempt for ${provider}/${parsed.providerPaymentId}`,
      );
      return { received: true, eventId: eventIn.id };
    }

    try {
      // El estado autoritativo lo da el provider, no el payload entrante: re-confirmamos
      // con getPayment antes de aplicar. Esto blinda contra webhooks falsificados cuando
      // la firma del provider no es reproducible localmente (ej. Dpago firma en el body
      // con una fórmula no documentada). Si la reconfirmación falla, caemos al status del webhook.
      let effectiveStatus = parsed.status;
      let effectiveRaw: unknown = parsed.raw;
      try {
        const confirmed = await route.adapter.getPayment(parsed.providerPaymentId);
        effectiveStatus = confirmed.status;
        effectiveRaw = { webhook: parsed.raw, confirmed: confirmed.rawResponse };
      } catch (e) {
        this.logger.warn(
          `No se pudo re-confirmar ${provider}/${parsed.providerPaymentId}: ${
            e instanceof Error ? e.message : String(e)
          }. Uso el status del webhook.`,
        );
      }
      await this.applyStatusChange(attempt, attempt.intent, effectiveStatus, effectiveRaw);
      await this.prisma.webhookEventIn.update({
        where: { id: eventIn.id },
        data: { processed: true, processedAt: new Date() },
      });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      await this.prisma.webhookEventIn.update({
        where: { id: eventIn.id },
        data: { processed: false, error: msg },
      });
      throw err;
    }

    return { received: true, eventId: eventIn.id };
  }

  private async applyStatusChange(
    attempt: PaymentAttempt,
    intent: PaymentIntent,
    providerStatus: "pending" | "processing" | "approved" | "rejected" | "expired" | "error",
    raw: unknown,
  ) {
    const attemptStatus = this.mapAttemptStatus(providerStatus);
    const intentStatus = this.mapIntentStatus(providerStatus);
    const now = new Date();

    if (attempt.status === attemptStatus && intent.status === intentStatus) {
      return; // no-op
    }

    await this.prisma.paymentAttempt.update({
      where: { id: attempt.id },
      data: {
        status: attemptStatus,
        approvedAt: attemptStatus === PaymentAttemptStatus.approved ? now : attempt.approvedAt,
        rawResponseJson: raw as Prisma.InputJsonValue,
      },
    });

    const updatedIntent = await this.prisma.paymentIntent.update({
      where: { id: intent.id },
      data: { status: intentStatus },
    });

    if (intentStatus === PaymentIntentStatus.approved) {
      await this.invoices.onIntentApproved(updatedIntent.id, updatedIntent.metadata);
    }

    const eventType = this.eventTypeFor(intentStatus);
    if (eventType) {
      await this.webhookEvents.emit({
        merchantId: updatedIntent.merchantId,
        eventType,
        data: {
          payment_intent_id: updatedIntent.id,
          merchant_id: updatedIntent.merchantId,
          external_reference: updatedIntent.externalReference,
          amount: updatedIntent.amount.toString(),
          currency: updatedIntent.currency,
          country: updatedIntent.country,
          status: updatedIntent.status,
        },
        paymentIntentId: updatedIntent.id,
      });
    }
  }

  private mapAttemptStatus(s: string): PaymentAttemptStatus {
    switch (s) {
      case "approved":
        return PaymentAttemptStatus.approved;
      case "rejected":
      case "expired":
        return PaymentAttemptStatus.rejected;
      case "error":
        return PaymentAttemptStatus.error;
      case "processing":
        return PaymentAttemptStatus.processing;
      default:
        return PaymentAttemptStatus.pending;
    }
  }

  private mapIntentStatus(s: string): PaymentIntentStatus {
    switch (s) {
      case "approved":
        return PaymentIntentStatus.approved;
      case "rejected":
      case "error":
        return PaymentIntentStatus.rejected;
      case "expired":
        return PaymentIntentStatus.expired;
      case "processing":
        return PaymentIntentStatus.processing;
      default:
        return PaymentIntentStatus.pending;
    }
  }

  private eventTypeFor(status: PaymentIntentStatus) {
    switch (status) {
      case PaymentIntentStatus.approved:
        return WEBHOOK_EVENT_TYPES.paymentIntentSucceeded;
      case PaymentIntentStatus.rejected:
        return WEBHOOK_EVENT_TYPES.paymentIntentFailed;
      case PaymentIntentStatus.processing:
        return WEBHOOK_EVENT_TYPES.paymentIntentProcessing;
      case PaymentIntentStatus.expired:
        return WEBHOOK_EVENT_TYPES.paymentIntentExpired;
      default:
        return null;
    }
  }

  private extractSignature(headers: Record<string, string>): string | null {
    return (
      headers["x-dlocalgo-signature"] ??
      headers["x-payments-signature"] ??
      headers["x-signature"] ??
      null
    );
  }

  private safeJson(rawBody: string): Prisma.InputJsonValue {
    try {
      return JSON.parse(rawBody) as Prisma.InputJsonValue;
    } catch {
      return { _raw: rawBody } as Prisma.InputJsonValue;
    }
  }
}
