/**
 * MercadoPago Service
 *
 * Integración con MercadoPago para pagos TPV
 * Basado en: michangarrito INT-004
 *
 * Features:
 * - Crear pagos con tarjeta
 * - Generar QR de pago
 * - Generar links de pago
 * - Procesar reembolsos
 * - Manejar webhooks
 * - Multi-tenant con credenciales por tenant
 * - Retry con backoff exponencial
 */

import { Repository, DataSource } from 'typeorm';
import { createHmac, timingSafeEqual } from 'crypto';
import {
  TenantTerminalConfig,
  TerminalPayment,
  TerminalWebhookEvent,
} from '../entities';

// DTOs
export interface CreatePaymentDto {
  amount: number;
  currency?: string;
  description?: string;
  paymentMethod?: 'card' | 'qr' | 'link';
  customerEmail?: string;
  customerName?: string;
  referenceType?: string;
  referenceId?: string;
  metadata?: Record<string, any>;
}

export interface RefundPaymentDto {
  paymentId: string;
  amount?: number; // Partial refund
  reason?: string;
}

export interface CreatePaymentLinkDto {
  amount: number;
  title: string;
  description?: string;
  expiresAt?: Date;
  referenceType?: string;
  referenceId?: string;
}

export interface MercadoPagoCredentials {
  accessToken: string;
  publicKey: string;
  collectorId?: string;
}

export interface MercadoPagoConfig {
  statementDescriptor?: string;
  notificationUrl?: string;
  externalReference?: string;
}

// Interfaces para respuestas de API MercadoPago
interface MercadoPagoPaymentResponse {
  id: number;
  status: string;
  fee_details?: Array<{ amount: number; type: string }>;
  card?: {
    last_four_digits: string;
    payment_method?: { name: string };
    cardholder?: { identification?: { type: string } };
  };
  external_reference?: string;
}

interface MercadoPagoPreferenceResponse {
  id: string;
  init_point: string;
}

interface MercadoPagoErrorResponse {
  message?: string;
  code?: string;
}

// Constantes
const MP_API_BASE = 'https://api.mercadopago.com';
const MAX_RETRIES = 5;
const RETRY_DELAYS = [1000, 2000, 4000, 8000, 16000]; // Backoff exponencial

export class MercadoPagoService {
  private configRepository: Repository<TenantTerminalConfig>;
  private paymentRepository: Repository<TerminalPayment>;
  private webhookRepository: Repository<TerminalWebhookEvent>;

  constructor(private dataSource: DataSource) {
    this.configRepository = dataSource.getRepository(TenantTerminalConfig);
    this.paymentRepository = dataSource.getRepository(TerminalPayment);
    this.webhookRepository = dataSource.getRepository(TerminalWebhookEvent);
  }

  /**
   * Obtener credenciales de MercadoPago para un tenant
   */
  async getCredentials(tenantId: string): Promise<{
    credentials: MercadoPagoCredentials;
    config: MercadoPagoConfig;
    configId: string;
  }> {
    const terminalConfig = await this.configRepository.findOne({
      where: {
        tenantId,
        provider: 'mercadopago',
        isActive: true,
      },
    });

    if (!terminalConfig) {
      throw new Error('MercadoPago not configured for this tenant');
    }

    if (!terminalConfig.isVerified) {
      throw new Error('MercadoPago credentials not verified');
    }

    return {
      credentials: terminalConfig.credentials as MercadoPagoCredentials,
      config: terminalConfig.config as MercadoPagoConfig,
      configId: terminalConfig.id,
    };
  }

  /**
   * Crear un pago
   */
  async createPayment(
    tenantId: string,
    dto: CreatePaymentDto,
    createdBy?: string
  ): Promise<TerminalPayment> {
    const { credentials, config, configId } = await this.getCredentials(tenantId);

    // Crear registro local primero
    const payment = this.paymentRepository.create({
      tenantId,
      configId,
      provider: 'mercadopago',
      amount: dto.amount,
      currency: dto.currency || 'MXN',
      status: 'pending',
      paymentMethod: dto.paymentMethod || 'card',
      customerEmail: dto.customerEmail,
      customerName: dto.customerName,
      description: dto.description,
      statementDescriptor: config.statementDescriptor,
      referenceType: dto.referenceType,
      referenceId: dto.referenceId ? dto.referenceId : undefined,
      metadata: dto.metadata || {},
      createdBy,
    });

    const savedPayment = await this.paymentRepository.save(payment);

    try {
      // Crear pago en MercadoPago con retry
      const mpPayment = await this.executeWithRetry(async () => {
        const response = await fetch(`${MP_API_BASE}/v1/payments`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${credentials.accessToken}`,
            'Content-Type': 'application/json',
            'X-Idempotency-Key': savedPayment.id,
          },
          body: JSON.stringify({
            transaction_amount: dto.amount,
            currency_id: dto.currency || 'MXN',
            description: dto.description,
            payment_method_id: 'card', // Se determinará por el checkout
            payer: {
              email: dto.customerEmail,
            },
            statement_descriptor: config.statementDescriptor,
            external_reference: savedPayment.id,
            notification_url: config.notificationUrl,
            metadata: {
              tenant_id: tenantId,
              internal_id: savedPayment.id,
              ...dto.metadata,
            },
          }),
        });

        if (!response.ok) {
          const error = await response.json() as MercadoPagoErrorResponse;
          throw new MercadoPagoError(error.message || 'Payment failed', response.status, error);
        }

        return response.json() as Promise<MercadoPagoPaymentResponse>;
      });

      // Actualizar registro local
      savedPayment.externalId = mpPayment.id?.toString() ?? null;
      savedPayment.externalStatus = mpPayment.status;
      savedPayment.status = this.mapMPStatus(mpPayment.status);
      savedPayment.providerResponse = mpPayment;
      savedPayment.processedAt = new Date();

      if (mpPayment.fee_details && mpPayment.fee_details.length > 0) {
        const totalFee = mpPayment.fee_details.reduce(
          (sum: number, fee: { amount: number }) => sum + fee.amount,
          0
        );
        savedPayment.feeAmount = totalFee;
        savedPayment.feeDetails = mpPayment.fee_details as unknown as Record<string, any>;
        savedPayment.netAmount = dto.amount - totalFee;
      }

      if (mpPayment.card) {
        savedPayment.cardLastFour = mpPayment.card.last_four_digits ?? null;
        savedPayment.cardBrand = mpPayment.card.payment_method?.name ?? null;
        savedPayment.cardType = mpPayment.card.cardholder?.identification?.type ?? null;
      }

      return this.paymentRepository.save(savedPayment);
    } catch (error: any) {
      // Guardar error
      savedPayment.status = 'rejected';
      savedPayment.errorCode = error.code || 'unknown';
      savedPayment.errorMessage = error.message;
      savedPayment.providerResponse = error.response;
      await this.paymentRepository.save(savedPayment);
      throw error;
    }
  }

  /**
   * Consultar estado de un pago
   */
  async getPayment(tenantId: string, paymentId: string): Promise<TerminalPayment> {
    const payment = await this.paymentRepository.findOne({
      where: { id: paymentId, tenantId },
    });

    if (!payment) {
      throw new Error('Payment not found');
    }

    // Si tiene external_id, sincronizar con MercadoPago
    if (payment.externalId && payment.status !== 'approved' && payment.status !== 'rejected') {
      await this.syncPaymentStatus(tenantId, payment);
    }

    return payment;
  }

  /**
   * Sincronizar estado de pago con MercadoPago
   */
  private async syncPaymentStatus(
    tenantId: string,
    payment: TerminalPayment
  ): Promise<TerminalPayment> {
    const { credentials } = await this.getCredentials(tenantId);

    try {
      const response = await fetch(`${MP_API_BASE}/v1/payments/${payment.externalId}`, {
        headers: {
          'Authorization': `Bearer ${credentials.accessToken}`,
        },
      });

      if (response.ok) {
        const mpPayment = await response.json() as MercadoPagoPaymentResponse;
        payment.externalStatus = mpPayment.status;
        payment.status = this.mapMPStatus(mpPayment.status);
        payment.providerResponse = mpPayment;
        await this.paymentRepository.save(payment);
      }
    } catch {
      // Silenciar errores de sincronización
    }

    return payment;
  }

  /**
   * Procesar reembolso
   */
  async refundPayment(
    tenantId: string,
    dto: RefundPaymentDto
  ): Promise<TerminalPayment> {
    const payment = await this.paymentRepository.findOne({
      where: { id: dto.paymentId, tenantId },
    });

    if (!payment) {
      throw new Error('Payment not found');
    }

    if (payment.status !== 'approved') {
      throw new Error('Cannot refund a payment that is not approved');
    }

    if (!payment.externalId) {
      throw new Error('Payment has no external reference');
    }

    const { credentials } = await this.getCredentials(tenantId);
    const refundAmount = dto.amount || Number(payment.amount);

    const mpRefund = await this.executeWithRetry(async () => {
      const response = await fetch(
        `${MP_API_BASE}/v1/payments/${payment.externalId}/refunds`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${credentials.accessToken}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            amount: refundAmount,
          }),
        }
      );

      if (!response.ok) {
        const error = await response.json() as MercadoPagoErrorResponse;
        throw new MercadoPagoError(error.message || 'Refund failed', response.status, error);
      }

      return response.json() as Promise<MercadoPagoPaymentResponse>;
    });

    // Actualizar pago
    payment.refundedAmount = Number(payment.refundedAmount || 0) + refundAmount;
    payment.refundReason = dto.reason ?? null;
    payment.refundedAt = new Date();

    if (payment.refundedAmount >= Number(payment.amount)) {
      payment.status = 'refunded';
    } else {
      payment.status = 'partially_refunded';
    }

    return this.paymentRepository.save(payment);
  }

  /**
   * Crear link de pago
   */
  async createPaymentLink(
    tenantId: string,
    dto: CreatePaymentLinkDto,
    createdBy?: string
  ): Promise<{ url: string; id: string }> {
    const { credentials, config } = await this.getCredentials(tenantId);

    const preference = await this.executeWithRetry(async () => {
      const response = await fetch(`${MP_API_BASE}/checkout/preferences`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${credentials.accessToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          items: [
            {
              title: dto.title,
              description: dto.description,
              quantity: 1,
              currency_id: 'MXN',
              unit_price: dto.amount,
            },
          ],
          back_urls: {
            success: config.notificationUrl,
            failure: config.notificationUrl,
            pending: config.notificationUrl,
          },
          notification_url: config.notificationUrl,
          expires: dto.expiresAt ? true : false,
          expiration_date_to: dto.expiresAt?.toISOString(),
          external_reference: dto.referenceId || undefined,
          metadata: {
            tenant_id: tenantId,
            reference_type: dto.referenceType,
            reference_id: dto.referenceId,
          },
        }),
      });

      if (!response.ok) {
        const error = await response.json() as MercadoPagoErrorResponse;
        throw new MercadoPagoError(
          error.message || 'Failed to create payment link',
          response.status,
          error
        );
      }

      return response.json() as Promise<MercadoPagoPreferenceResponse>;
    });

    return {
      url: preference.init_point,
      id: preference.id,
    };
  }

  /**
   * Manejar webhook de MercadoPago
   */
  async handleWebhook(
    tenantId: string,
    eventType: string,
    data: any,
    headers: Record<string, string>
  ): Promise<void> {
    // Verificar firma si está configurada
    const config = await this.configRepository.findOne({
      where: { tenantId, provider: 'mercadopago', isActive: true },
    });

    if (config?.webhookSecret && headers['x-signature']) {
      const isValid = this.verifyWebhookSignature(
        headers['x-signature'],
        headers['x-request-id'],
        data.id?.toString(),
        config.webhookSecret
      );

      if (!isValid) {
        throw new Error('Invalid webhook signature');
      }
    }

    // Guardar evento
    const event = this.webhookRepository.create({
      tenantId,
      provider: 'mercadopago',
      eventType,
      eventId: data.id?.toString(),
      externalId: data.data?.id?.toString(),
      payload: data,
      headers,
      signatureValid: true,
      idempotencyKey: `${data.id}-${eventType}`,
    });

    await this.webhookRepository.save(event);

    // Procesar evento
    try {
      switch (eventType) {
        case 'payment':
          await this.handlePaymentWebhook(tenantId, data.data?.id);
          break;
        case 'refund':
          await this.handleRefundWebhook(tenantId, data.data?.id);
          break;
      }

      event.processed = true;
      event.processedAt = new Date();
    } catch (error: any) {
      event.processingError = error.message;
      event.retryCount += 1;
    }

    await this.webhookRepository.save(event);
  }

  /**
   * Procesar webhook de pago
   */
  private async handlePaymentWebhook(tenantId: string, mpPaymentId: string): Promise<void> {
    const { credentials } = await this.getCredentials(tenantId);

    // Obtener detalles del pago
    const response = await fetch(`${MP_API_BASE}/v1/payments/${mpPaymentId}`, {
      headers: {
        'Authorization': `Bearer ${credentials.accessToken}`,
      },
    });

    if (!response.ok) return;

    const mpPayment = await response.json() as MercadoPagoPaymentResponse;

    // Buscar pago local por external_reference o external_id
    let payment = await this.paymentRepository.findOne({
      where: [
        { externalId: mpPaymentId.toString(), tenantId },
        { id: mpPayment.external_reference, tenantId },
      ],
    });

    if (payment) {
      payment.externalId = mpPaymentId.toString();
      payment.externalStatus = mpPayment.status;
      payment.status = this.mapMPStatus(mpPayment.status);
      payment.providerResponse = mpPayment;
      payment.processedAt = new Date();

      if (mpPayment.card) {
        payment.cardLastFour = mpPayment.card.last_four_digits ?? null;
        payment.cardBrand = mpPayment.card.payment_method?.name ?? null;
      }

      await this.paymentRepository.save(payment);
    }
  }

  /**
   * Procesar webhook de reembolso
   */
  private async handleRefundWebhook(tenantId: string, refundId: string): Promise<void> {
    // Implementación similar a handlePaymentWebhook
  }

  /**
   * Verificar firma de webhook MercadoPago
   */
  private verifyWebhookSignature(
    xSignature: string,
    xRequestId: string,
    dataId: string,
    secret: string
  ): boolean {
    try {
      const parts = xSignature.split(',').reduce((acc, part) => {
        const [key, value] = part.split('=');
        acc[key.trim()] = value.trim();
        return acc;
      }, {} as Record<string, string>);

      const ts = parts['ts'];
      const hash = parts['v1'];

      const manifest = `id:${dataId};request-id:${xRequestId};ts:${ts};`;
      const expected = createHmac('sha256', secret).update(manifest).digest('hex');

      return timingSafeEqual(Buffer.from(hash), Buffer.from(expected));
    } catch {
      return false;
    }
  }

  /**
   * Mapear estado de MercadoPago a estado interno
   */
  private mapMPStatus(
    mpStatus: string
  ): 'pending' | 'processing' | 'approved' | 'rejected' | 'refunded' | 'cancelled' {
    const statusMap: Record<string, any> = {
      pending: 'pending',
      in_process: 'processing',
      approved: 'approved',
      authorized: 'approved',
      rejected: 'rejected',
      cancelled: 'cancelled',
      refunded: 'refunded',
      charged_back: 'charged_back',
    };

    return statusMap[mpStatus] || 'pending';
  }

  /**
   * Ejecutar operación con retry y backoff exponencial
   */
  private async executeWithRetry<T>(fn: () => Promise<T>, attempt = 0): Promise<T> {
    try {
      return await fn();
    } catch (error: any) {
      if (attempt >= MAX_RETRIES) {
        throw error;
      }

      // Solo reintentar en errores de rate limit o errores de servidor
      if (error.status === 429 || error.status >= 500) {
        const delay = RETRY_DELAYS[attempt] || RETRY_DELAYS[RETRY_DELAYS.length - 1];
        await new Promise((resolve) => setTimeout(resolve, delay));
        return this.executeWithRetry(fn, attempt + 1);
      }

      throw error;
    }
  }
}

/**
 * Error personalizado para MercadoPago
 */
export class MercadoPagoError extends Error {
  constructor(
    message: string,
    public status: number,
    public response?: any
  ) {
    super(message);
    this.name = 'MercadoPagoError';
  }
}
