import {
  BadRequestException,
  ConflictException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { timingSafeEqual } from 'crypto';
import { PrismaService } from 'src/prisma/prisma.service';
import { BancardTokenService } from './bancard-token.service';
import { BancardConfigService } from './bancard-config.service';
import { BancardLogService } from './bancard-log.service';
import type { IPaymentGateway, PaymentInitResult, PaymentConfirmResult, PaymentRefundResult } from './interfaces/payment-gateway.interface';

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly tokenService: BancardTokenService,
    private readonly configService: BancardConfigService,
    private readonly logService: BancardLogService,
  ) {}

  async initPayment(params: {
    empresaId: string;
    monto: number;
    moneda: string;
    descripcion: string;
    clienteId?: string;
    usuarioId?: string;
    origenModulo: string;
    origenId?: string;
  }): Promise<PaymentInitResult> {
    const { empresaId, monto, moneda, descripcion, clienteId, usuarioId, origenModulo, origenId } = params;

    const shopProcessId = await this.generateShopProcessId(empresaId);
    const privateKey = await this.configService.getPrivateKey(empresaId);
    const publicKey = await this.configService.getPublicKey(empresaId);
    const baseUrl = await this.configService.getBaseUrl(empresaId);

    const token = this.tokenService.generateSingleBuyToken(privateKey, shopProcessId, monto, moneda);

    const returnUrl = `${(await this.configService.getFullConfig(empresaId)).return_url_base}?spid=${shopProcessId}`;
    const cancelUrl = `${(await this.configService.getFullConfig(empresaId)).cancel_url_base}?spid=${shopProcessId}`;

    const requestBody = {
      public_key: publicKey,
      operation: {
        token,
        shop_process_id: shopProcessId.toString(),
        amount: monto.toFixed(2),
        currency: moneda,
        additional_data: '',
        description: descripcion.substring(0, 199),
        return_url: returnUrl,
        cancel_url: cancelUrl,
      },
    };

    const start = Date.now();
    const response = await fetch(`${baseUrl}/single_buy`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody),
    });
    const latencia = Date.now() - start;
    const responseJson = await response.json() as any;

    const pago = await this.prisma.pago_bancard.create({
      data: {
        empresa_id: empresaId,
        shop_process_id: shopProcessId,
        tipo_operacion: 'single_buy',
        origen_modulo: origenModulo as any,
        origen_id: origenId ?? null,
        cliente_id: clienteId ?? null,
        usuario_id: usuarioId ?? null,
        monto: monto,
        moneda,
        estado: 'pendiente',
        meta_json: responseJson,
      },
    });

    await this.logService.logOut({
      pagoId: pago.id,
      empresaId,
      endpoint: `${baseUrl}/single_buy`,
      requestBody,
      responseBody: responseJson,
      httpStatus: response.status,
      latenciaMs: latencia,
    });

    if (!response.ok || responseJson.status !== 'success') {
      throw new BadRequestException(
        responseJson.messages?.[0]?.dsc ?? 'Error iniciando pago Bancard',
      );
    }

    const processUrl = responseJson.process_url;
    await this.prisma.pago_bancard.update({
      where: { id: pago.id },
      data: { process_id: responseJson.process_id ?? null },
    });

    return { processUrl, shopProcessId, pagoId: pago.id };
  }

  async confirmPayment(shopProcessId: bigint, empresaId: string): Promise<PaymentConfirmResult> {
    const pago = await this.prisma.pago_bancard.findFirst({
      where: { shop_process_id: shopProcessId, empresa_id: empresaId },
    });
    if (!pago) throw new NotFoundException('Pago no encontrado');

    if (pago.estado === 'aprobado') {
      return { aprobado: true, responseCode: '00', responseDescription: 'Ya confirmado' };
    }

    const privateKey = await this.configService.getPrivateKey(empresaId);
    const publicKey = await this.configService.getPublicKey(empresaId);
    const baseUrl = await this.configService.getBaseUrl(empresaId);
    const token = this.tokenService.generateConfirmationsToken(privateKey, shopProcessId);

    const requestBody = {
      public_key: publicKey,
      operation: { token, shop_process_id: shopProcessId.toString() },
    };

    const start = Date.now();
    const response = await fetch(`${baseUrl}/single_buy/confirmations`, {
      method: 'GET',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody),
    });
    const latencia = Date.now() - start;
    const responseJson = await response.json() as any;

    await this.logService.logOut({
      pagoId: pago.id,
      empresaId,
      endpoint: `${baseUrl}/single_buy/confirmations`,
      requestBody,
      responseBody: responseJson,
      httpStatus: response.status,
      latenciaMs: latencia,
    });

    const confirmation = responseJson.confirmation ?? responseJson.confirmations?.[0];
    const aprobado = confirmation?.response === 'S' || responseJson.status === 'success';
    const responseCode = confirmation?.response_code ?? '';
    const responseDescription = confirmation?.response_details ?? responseJson.messages?.[0]?.dsc ?? '';

    const nuevoEstado = aprobado ? 'aprobado' : 'rechazado';
    await this.prisma.pago_bancard.update({
      where: { id: pago.id },
      data: {
        estado: nuevoEstado as any,
        response_code: responseCode,
        response_description: responseDescription,
        authorization_number: confirmation?.authorization_number ?? null,
        ticket_number: confirmation?.ticket_number ?? null,
        fecha_confirmacion: aprobado ? new Date() : null,
        updated_at: new Date(),
      },
    });

    return { aprobado, responseCode, responseDescription, authorizationNumber: confirmation?.authorization_number, ticketNumber: confirmation?.ticket_number };
  }

  async refund(pagoId: string, empresaId: string, monto?: number): Promise<PaymentRefundResult> {
    const pago = await this.prisma.pago_bancard.findFirst({
      where: { id: pagoId, empresa_id: empresaId },
    });
    if (!pago) throw new NotFoundException('Pago no encontrado');
    if (pago.estado !== 'aprobado') throw new BadRequestException('Solo se pueden devolver pagos aprobados');

    const privateKey = await this.configService.getPrivateKey(empresaId);
    const publicKey = await this.configService.getPublicKey(empresaId);
    const baseUrl = await this.configService.getBaseUrl(empresaId);
    const montoDevolucion = monto ?? Number(pago.monto);
    const token = this.tokenService.generateRefundToken(privateKey, pago.shop_process_id, montoDevolucion, pago.moneda);

    const requestBody = {
      public_key: publicKey,
      operation: {
        token,
        shop_process_id: pago.shop_process_id.toString(),
        amount: montoDevolucion.toFixed(2),
        currency: pago.moneda,
      },
    };

    const start = Date.now();
    const response = await fetch(`${baseUrl}/single_buy/refund`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody),
    });
    const latencia = Date.now() - start;
    const responseJson = await response.json() as any;

    await this.logService.logOut({
      pagoId: pago.id,
      empresaId,
      endpoint: `${baseUrl}/single_buy/refund`,
      requestBody,
      responseBody: responseJson,
      httpStatus: response.status,
      latenciaMs: latencia,
    });

    const success = responseJson.status === 'success';
    if (success) {
      const esParcial = montoDevolucion < Number(pago.monto);
      await this.prisma.pago_bancard.update({
        where: { id: pago.id },
        data: {
          estado: esParcial ? 'devuelto_parcial' : 'devuelto_total',
          monto_devuelto: (Number(pago.monto_devuelto ?? 0) + montoDevolucion) as any,
          updated_at: new Date(),
        },
      });
    }

    return { success, message: responseJson.messages?.[0]?.dsc ?? (success ? 'Devolución procesada' : 'Error en devolución') };
  }

  async rollback(shopProcessId: bigint, empresaId: string): Promise<{ success: boolean; message: string }> {
    const pago = await this.prisma.pago_bancard.findFirst({
      where: { shop_process_id: shopProcessId, empresa_id: empresaId },
    });
    if (!pago) throw new NotFoundException('Pago no encontrado');

    const privateKey = await this.configService.getPrivateKey(empresaId);
    const publicKey = await this.configService.getPublicKey(empresaId);
    const baseUrl = await this.configService.getBaseUrl(empresaId);
    const token = this.tokenService.generateRollbackToken(privateKey, shopProcessId);

    const requestBody = {
      public_key: publicKey,
      operation: { token, shop_process_id: shopProcessId.toString() },
    };

    const start = Date.now();
    const response = await fetch(`${baseUrl}/single_buy/rollback`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody),
    });
    const latencia = Date.now() - start;
    const responseJson = await response.json() as any;

    await this.logService.logOut({
      pagoId: pago.id,
      empresaId,
      endpoint: `${baseUrl}/single_buy/rollback`,
      requestBody,
      responseBody: responseJson,
      httpStatus: response.status,
      latenciaMs: latencia,
    });

    const success = responseJson.status === 'success';
    if (success) {
      await this.prisma.pago_bancard.update({
        where: { id: pago.id },
        data: { estado: 'anulado', updated_at: new Date() },
      });
    }
    return { success, message: responseJson.messages?.[0]?.dsc ?? (success ? 'Anulado' : 'Error en rollback') };
  }

  /**
   * Verifica que un webhook entrante de Bancard sea auténtico comparando el
   * `operation.token` recibido contra el token esperado
   * md5(private_key + shop_process_id + "confirm" + amount + currency), calculado
   * con el monto/moneda ALMACENADOS del pago (no los del payload) para que un
   * atacante no pueda forjar la firma manipulando el importe. Comparación
   * timing-safe. Devuelve false ante cualquier dato faltante/incorrecto.
   */
  async verifyWebhookSignature(payload: any, empresaId: string): Promise<boolean> {
    const receivedToken = payload?.operation?.token;
    if (!receivedToken || typeof receivedToken !== 'string') return false;

    const shopProcessId = BigInt(payload.operation?.shop_process_id ?? 0);
    if (!shopProcessId) return false;

    const pago = await this.prisma.pago_bancard.findFirst({
      where: { shop_process_id: shopProcessId, empresa_id: empresaId },
      select: { monto: true, moneda: true },
    });
    if (!pago) return false;

    let privateKey: string;
    try {
      privateKey = await this.configService.getPrivateKey(empresaId);
    } catch {
      return false; // sin config Bancard no se puede validar → rechazar
    }

    const expected = this.tokenService.generateConfirmationWebhookToken(
      privateKey,
      shopProcessId,
      Number(pago.monto),
      pago.moneda,
    );

    const a = Buffer.from(receivedToken, 'utf8');
    const b = Buffer.from(expected, 'utf8');
    return a.length === b.length && timingSafeEqual(a, b);
  }

  async procesarWebhook(payload: any, empresaId: string): Promise<void> {
    const shopProcessId = BigInt(payload.operation?.shop_process_id ?? 0);
    if (!shopProcessId) return;

    const pago = await this.prisma.pago_bancard.findFirst({
      where: { shop_process_id: shopProcessId, empresa_id: empresaId },
    });
    if (!pago) {
      this.logger.warn(`Webhook Bancard: pago no encontrado spid=${shopProcessId}`);
      return;
    }

    if (pago.estado === 'aprobado') return; // idempotente

    const aprobado = payload.operation?.response === 'S';
    await this.prisma.pago_bancard.update({
      where: { id: pago.id },
      data: {
        estado: aprobado ? 'aprobado' : 'rechazado',
        response_code: payload.operation?.response_code ?? null,
        response_description: payload.operation?.response_details ?? null,
        authorization_number: payload.operation?.authorization_number ?? null,
        ticket_number: payload.operation?.ticket_number ?? null,
        fecha_confirmacion: aprobado ? new Date() : null,
        updated_at: new Date(),
      },
    });
  }

  async getPagosByEmpresa(empresaId: string, page = 1, limit = 20) {
    const skip = (page - 1) * limit;
    const [data, total] = await Promise.all([
      this.prisma.pago_bancard.findMany({
        where: { empresa_id: empresaId },
        orderBy: { created_at: 'desc' },
        skip,
        take: limit,
        select: {
          id: true, shop_process_id: true, tipo_operacion: true, origen_modulo: true,
          monto: true, moneda: true, estado: true, fecha_confirmacion: true,
          response_code: true, ticket_number: true, created_at: true,
          cliente: { select: { id: true, nombre_fantasia: true } },
        },
      }),
      this.prisma.pago_bancard.count({ where: { empresa_id: empresaId } }),
    ]);
    return { data, total, page, limit };
  }

  async findPagoBySpid(shopProcessId: bigint, empresaId: string) {
    return this.prisma.pago_bancard.findFirst({
      where: { shop_process_id: shopProcessId, empresa_id: empresaId },
      select: { id: true, estado: true },
    });
  }

  private async generateShopProcessId(empresaId: string): Promise<bigint> {
    const year = new Date().getFullYear() % 100;
    const base = BigInt(year) * BigInt(1_000_000_000);

    const last = await this.prisma.pago_bancard.findFirst({
      where: { empresa_id: empresaId },
      orderBy: { created_at: 'desc' },
      select: { shop_process_id: true },
    });

    if (!last) return base + BigInt(1);

    const lastNum = last.shop_process_id % BigInt(1_000_000_000);
    const candidate = base + lastNum + BigInt(1);

    const exists = await this.prisma.pago_bancard.findFirst({
      where: { empresa_id: empresaId, shop_process_id: candidate },
    });
    if (exists) throw new ConflictException('Error generando shop_process_id, reintente');
    return candidate;
  }
}
