import {
  BadRequestException,
  Injectable,
  Logger,
  UnauthorizedException,
} from '@nestjs/common';
import { Prisma, novasis_pay_cobro_status } from '@prisma/client';
import { createHmac, timingSafeEqual } from 'crypto';
import { PrismaService } from 'src/prisma/prisma.service';
import { EcommercePedidosService } from 'src/ecommerce/pedidos/ecommerce-pedidos.service';

/**
 * Tolerancia para el timestamp firmado: 5 minutos.
 * Coincide con la del gateway (`verifyWebhook` en novasis-pay/.../hmac.util.ts).
 * Si el reloj del ERP se desfasa más que esto, los webhooks fallan a propósito
 * para evitar replays viejos.
 */
const SIGNATURE_TOLERANCE_SECONDS = 300;

interface WebhookHeaders {
  signature: string | undefined;
  eventId: string | undefined;
  eventType: string | undefined;
}

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly ecommercePedidos: EcommercePedidosService,
  ) {}

  /**
   * Si el intent corresponde a un pedido de la tienda online, aplica el
   * resultado del pago sobre la sesión de checkout. En caso de aprobación
   * delega en `confirmarPagoDesdeGateway`, que crea el pedido ERP y reserva
   * stock (la misma vía que la confirmación manual del operador).
   *
   * Devuelve true si la sesión existía (para no reportar `cobro_no_encontrado`).
   */
  private async tryApplyToEcommerceSession(
    empresaId: string,
    intentId: string,
    status: novasis_pay_cobro_status,
  ): Promise<boolean> {
    const session = await this.prisma.ecommerce_checkout_session.findFirst({
      where: { empresa_id: empresaId, payment_intent_id: intentId },
      select: { id: true },
    });
    if (!session) return false;

    if (status === novasis_pay_cobro_status.aprobado) {
      await this.ecommercePedidos.confirmarPagoDesdeGateway(empresaId, session.id);
    } else if (status === novasis_pay_cobro_status.rechazado) {
      // Sólo si sigue esperando el pago: no pisar un pedido ya en preparación.
      await this.prisma.ecommerce_checkout_session.updateMany({
        where: { id: session.id, estado: 'pendiente_pago' },
        data: { estado: 'cancelado', updated_at: new Date() },
      });
    }
    return true;
  }

  async processWebhook(rawBody: string, headers: WebhookHeaders) {
    if (!rawBody) {
      throw new BadRequestException('Body vacío');
    }
    if (!headers.signature || !headers.eventId || !headers.eventType) {
      throw new BadRequestException(
        'Faltan headers X-Payments-Signature, X-Payments-Event-Id o X-Payments-Event-Type',
      );
    }

    let payload: WebhookPayload;
    try {
      payload = JSON.parse(rawBody) as WebhookPayload;
    } catch {
      throw new BadRequestException('Body no es JSON válido');
    }

    const merchantId = payload?.data?.merchant_id;
    if (!merchantId) {
      throw new BadRequestException('Payload sin data.merchant_id');
    }

    // Routing multi-tenant: cada empresa tiene su propio merchant_id en el gateway.
    const cfg = await this.prisma.novasis_pay_config.findFirst({
      where: { merchant_id: merchantId },
    });
    if (!cfg) {
      // No respondemos 404 al gateway porque eso dispara reintentos eternos.
      // Loggeamos y dejamos asentado el rechazo, pero ACK 200 para que el gateway
      // marque el evento como entregado y deje de reintentar.
      this.logger.warn(
        `Webhook ${headers.eventId} (${headers.eventType}) para merchant_id desconocido ${merchantId}`,
      );
      await this.recordEvent({
        eventId: headers.eventId,
        eventType: headers.eventType,
        empresaId: null,
        intentId: payload?.data?.payment_intent_id ?? null,
        payload,
        signature: headers.signature,
        result: 'merchant_no_encontrado',
        errorMessage: `merchant_id=${merchantId}`,
      });
      return { received: true, ignored: 'merchant_not_found' };
    }

    if (!cfg.webhook_secret) {
      this.logger.error(
        `Empresa ${cfg.empresa_id} no tiene webhook_secret configurado, no se puede verificar firma`,
      );
      throw new UnauthorizedException('Webhook secret no configurado');
    }

    if (!this.verifySignature(rawBody, headers.signature, cfg.webhook_secret)) {
      await this.recordEvent({
        eventId: headers.eventId,
        eventType: headers.eventType,
        empresaId: cfg.empresa_id,
        intentId: payload?.data?.payment_intent_id ?? null,
        payload,
        signature: headers.signature,
        result: 'firma_invalida',
        errorMessage: null,
      });
      throw new UnauthorizedException('Firma HMAC inválida o expirada');
    }

    // Idempotencia: si ya procesamos este event_id, devolvemos 200 sin reaplicar.
    const existing = await this.prisma.novasis_pay_webhook_event_in.findUnique({
      where: { event_id: headers.eventId },
    });
    if (existing) {
      return { received: true, ignored: 'duplicate' };
    }

    const nextStatus = this.mapEventToStatus(headers.eventType);
    let result = 'aplicado';
    let errorMessage: string | null = null;

    if (nextStatus) {
      const intentId = payload?.data?.payment_intent_id;
      if (!intentId) {
        result = 'sin_intent_id';
        errorMessage = 'payload.data.payment_intent_id faltante';
      } else {
        const updated = await this.prisma.novasis_pay_cobro.updateMany({
          where: { empresa_id: cfg.empresa_id, intent_id: intentId },
          data: { status: nextStatus, updated_at: new Date() },
        });

        // Pedidos de la tienda online: el checkout ecommerce NO crea un
        // `novasis_pay_cobro`, guarda el intent en `ecommerce_checkout_session`.
        // Sin esto, el webhook no encontraba nada y el cliente podía pagar sin
        // que el pedido saliera nunca de `pendiente_pago`.
        const ecommerce = await this.tryApplyToEcommerceSession(
          cfg.empresa_id,
          intentId,
          nextStatus,
        );

        if (updated.count === 0 && !ecommerce) {
          // Webhook llegó antes de que el INSERT del cobro haya commiteado,
          // o el cobro vive en otra empresa. Lo dejamos asentado para revisión
          // pero ACK 200 para no provocar reintentos.
          result = 'cobro_no_encontrado';
          errorMessage = `intent_id=${intentId}`;
        } else if (updated.count === 0 && ecommerce) {
          result = 'aplicado_ecommerce';
        }
      }
    } else {
      result = 'evento_ignorado';
    }

    try {
      await this.recordEvent({
        eventId: headers.eventId,
        eventType: headers.eventType,
        empresaId: cfg.empresa_id,
        intentId: payload?.data?.payment_intent_id ?? null,
        payload,
        signature: headers.signature,
        result,
        errorMessage,
      });
    } catch (err) {
      if (
        err instanceof Prisma.PrismaClientKnownRequestError &&
        err.code === 'P2002'
      ) {
        // Carrera: otro worker insertó el mismo event_id entre el findUnique y el create.
        return { received: true, ignored: 'duplicate_race' };
      }
      throw err;
    }

    return { received: true, result };
  }

  private verifySignature(rawBody: string, header: string, secret: string): boolean {
    // Formato emitido por el gateway: "t=<unix_seconds>,v1=<hex_hmac>".
    const parts = header.split(',').reduce<Record<string, string>>((acc, p) => {
      const [k, v] = p.split('=');
      if (k && v) acc[k.trim()] = v.trim();
      return acc;
    }, {});
    const ts = parts.t;
    const v1 = parts.v1;
    if (!ts || !v1) return false;

    const tsNum = Number(ts);
    if (!Number.isFinite(tsNum)) return false;
    const nowSec = Math.floor(Date.now() / 1000);
    if (Math.abs(nowSec - tsNum) > SIGNATURE_TOLERANCE_SECONDS) return false;

    const signed = `${ts}.${rawBody}`;
    const expected = createHmac('sha256', secret).update(signed).digest('hex');

    const a = Buffer.from(expected, 'hex');
    const b = Buffer.from(v1, 'hex');
    if (a.length !== b.length) return false;
    return timingSafeEqual(a, b);
  }

  private mapEventToStatus(eventType: string): novasis_pay_cobro_status | null {
    switch (eventType) {
      case 'payment_intent.processing':
        return novasis_pay_cobro_status.procesando;
      case 'payment_intent.succeeded':
        return novasis_pay_cobro_status.aprobado;
      case 'payment_intent.failed':
        return novasis_pay_cobro_status.rechazado;
      case 'payment_intent.expired':
      case 'checkout_session.expired':
        return novasis_pay_cobro_status.expirado;
      default:
        // payment_intent.created, checkout_session.completed, refunds, etc.
        // No los mapeamos por ahora: el estado del cobro lo gobierna succeeded/failed/expired.
        return null;
    }
  }

  private async recordEvent(args: {
    eventId: string;
    eventType: string;
    empresaId: string | null;
    intentId: string | null;
    payload: unknown;
    signature: string;
    result: string;
    errorMessage: string | null;
  }) {
    await this.prisma.novasis_pay_webhook_event_in.create({
      data: {
        event_id: args.eventId,
        event_type: args.eventType,
        empresa_id: args.empresaId,
        intent_id: args.intentId,
        payload: args.payload as Prisma.InputJsonValue,
        signature: args.signature,
        result: args.result,
        error_message: args.errorMessage,
      },
    });
  }
}

interface WebhookPayload {
  id?: string;
  event_type?: string;
  created_at?: string;
  data?: {
    merchant_id?: string;
    payment_intent_id?: string;
    external_reference?: string | null;
    amount?: string;
    currency?: string;
    country?: string;
    status?: string;
    [k: string]: unknown;
  };
}
