/**
 * Clip Service
 *
 * Integración con Clip para pagos TPV
 * Basado en: michangarrito INT-005
 *
 * Features:
 * - Crear pagos con tarjeta
 * - Generar links de pago
 * - Procesar reembolsos
 * - Manejar webhooks
 * - Multi-tenant con credenciales por tenant
 * - Retry con backoff exponencial
 *
 * Comisión: 3.6% + IVA por transacción
 */

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

// DTOs
export interface CreateClipPaymentDto {
  amount: number;
  currency?: string;
  description?: string;
  customerEmail?: string;
  customerName?: string;
  customerPhone?: string;
  referenceType?: string;
  referenceId?: string;
  metadata?: Record<string, any>;
}

export interface RefundClipPaymentDto {
  paymentId: string;
  amount?: number;
  reason?: string;
}

export interface CreateClipLinkDto {
  amount: number;
  description: string;
  expiresInMinutes?: number;
  referenceType?: string;
  referenceId?: string;
}

export interface ClipCredentials {
  apiKey: string;
  secretKey: string;
  merchantId: string;
}

export interface ClipConfig {
  defaultCurrency?: string;
  webhookSecret?: string;
}

// Interfaces para respuestas de API Clip
interface ClipPaymentResponse {
  id: string;
  status: string;
  card?: {
    last_four: string;
    brand: string;
    type: string;
  };
}

interface ClipPaymentLinkResponse {
  id: string;
  url: string;
}

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

// Constantes
const CLIP_API_BASE = 'https://api.clip.mx';
const MAX_RETRIES = 5;
const RETRY_DELAYS = [1000, 2000, 4000, 8000, 16000];

// Clip fee: 3.6% + IVA
const CLIP_FEE_RATE = 0.036;
const IVA_RATE = 0.16;

export class ClipService {
  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 Clip para un tenant
   */
  async getCredentials(tenantId: string): Promise<{
    credentials: ClipCredentials;
    config: ClipConfig;
    configId: string;
  }> {
    const terminalConfig = await this.configRepository.findOne({
      where: {
        tenantId,
        provider: 'clip',
        isActive: true,
      },
    });

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

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

    return {
      credentials: terminalConfig.credentials as ClipCredentials,
      config: terminalConfig.config as ClipConfig,
      configId: terminalConfig.id,
    };
  }

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

    // Calcular comisiones
    const feeAmount = dto.amount * CLIP_FEE_RATE * (1 + IVA_RATE);
    const netAmount = dto.amount - feeAmount;

    // Crear registro local
    const payment = this.paymentRepository.create({
      tenantId,
      configId,
      provider: 'clip',
      amount: dto.amount,
      currency: dto.currency || config.defaultCurrency || 'MXN',
      status: 'pending',
      paymentMethod: 'card',
      customerEmail: dto.customerEmail,
      customerName: dto.customerName,
      customerPhone: dto.customerPhone,
      description: dto.description,
      referenceType: dto.referenceType,
      referenceId: dto.referenceId ? dto.referenceId : undefined,
      feeAmount,
      feeDetails: {
        rate: CLIP_FEE_RATE,
        iva: IVA_RATE,
        calculated: feeAmount,
      },
      netAmount,
      metadata: dto.metadata || {},
      createdBy,
    });

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

    try {
      // Crear pago en Clip
      const clipPayment = await this.executeWithRetry(async () => {
        const response = await fetch(`${CLIP_API_BASE}/v1/payments`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${credentials.apiKey}`,
            'Content-Type': 'application/json',
            'X-Clip-Merchant-Id': credentials.merchantId,
            'X-Idempotency-Key': savedPayment.id,
          },
          body: JSON.stringify({
            amount: dto.amount,
            currency: dto.currency || 'MXN',
            description: dto.description,
            customer: {
              email: dto.customerEmail,
              name: dto.customerName,
              phone: dto.customerPhone,
            },
            metadata: {
              tenant_id: tenantId,
              internal_id: savedPayment.id,
              ...dto.metadata,
            },
          }),
        });

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

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

      // Actualizar registro local
      savedPayment.externalId = clipPayment.id;
      savedPayment.externalStatus = clipPayment.status;
      savedPayment.status = this.mapClipStatus(clipPayment.status);
      savedPayment.providerResponse = clipPayment;
      savedPayment.processedAt = new Date();

      if (clipPayment.card) {
        savedPayment.cardLastFour = clipPayment.card.last_four;
        savedPayment.cardBrand = clipPayment.card.brand;
        savedPayment.cardType = clipPayment.card.type;
      }

      return this.paymentRepository.save(savedPayment);
    } catch (error: any) {
      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');
    }

    // Sincronizar si es necesario
    if (payment.externalId && !['approved', 'rejected'].includes(payment.status)) {
      await this.syncPaymentStatus(tenantId, payment);
    }

    return payment;
  }

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

    try {
      const response = await fetch(`${CLIP_API_BASE}/v1/payments/${payment.externalId}`, {
        headers: {
          'Authorization': `Bearer ${credentials.apiKey}`,
          'X-Clip-Merchant-Id': credentials.merchantId,
        },
      });

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

    return payment;
  }

  /**
   * Procesar reembolso
   */
  async refundPayment(
    tenantId: string,
    dto: RefundClipPaymentDto
  ): 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 clipRefund = await this.executeWithRetry(async () => {
      const response = await fetch(
        `${CLIP_API_BASE}/v1/payments/${payment.externalId}/refund`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${credentials.apiKey}`,
            'X-Clip-Merchant-Id': credentials.merchantId,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            amount: refundAmount,
            reason: dto.reason,
          }),
        }
      );

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

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

    // 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: CreateClipLinkDto,
    createdBy?: string
  ): Promise<{ url: string; id: string }> {
    const { credentials } = await this.getCredentials(tenantId);

    const paymentLink = await this.executeWithRetry(async () => {
      const response = await fetch(`${CLIP_API_BASE}/v1/payment-links`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${credentials.apiKey}`,
          'X-Clip-Merchant-Id': credentials.merchantId,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          amount: dto.amount,
          description: dto.description,
          expires_in: dto.expiresInMinutes || 1440, // Default 24 horas
          metadata: {
            tenant_id: tenantId,
            reference_type: dto.referenceType,
            reference_id: dto.referenceId,
          },
        }),
      });

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

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

    return {
      url: paymentLink.url,
      id: paymentLink.id,
    };
  }

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

    if (config?.config?.webhookSecret && headers['x-clip-signature']) {
      const isValid = this.verifyWebhookSignature(
        JSON.stringify(data),
        headers['x-clip-signature'],
        config.config.webhookSecret as string
      );

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

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

    await this.webhookRepository.save(event);

    // Procesar evento
    try {
      switch (eventType) {
        case 'payment.succeeded':
          await this.handlePaymentSucceeded(tenantId, data);
          break;
        case 'payment.failed':
          await this.handlePaymentFailed(tenantId, data);
          break;
        case 'refund.succeeded':
          await this.handleRefundSucceeded(tenantId, data);
          break;
      }

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

    await this.webhookRepository.save(event);
  }

  /**
   * Procesar pago exitoso
   */
  private async handlePaymentSucceeded(tenantId: string, data: any): Promise<void> {
    const payment = await this.paymentRepository.findOne({
      where: [
        { externalId: data.payment_id, tenantId },
        { id: data.metadata?.internal_id, tenantId },
      ],
    });

    if (payment) {
      payment.status = 'approved';
      payment.externalStatus = 'succeeded';
      payment.processedAt = new Date();

      if (data.card) {
        payment.cardLastFour = data.card.last_four;
        payment.cardBrand = data.card.brand;
      }

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

  /**
   * Procesar pago fallido
   */
  private async handlePaymentFailed(tenantId: string, data: any): Promise<void> {
    const payment = await this.paymentRepository.findOne({
      where: [
        { externalId: data.payment_id, tenantId },
        { id: data.metadata?.internal_id, tenantId },
      ],
    });

    if (payment) {
      payment.status = 'rejected';
      payment.externalStatus = 'failed';
      payment.errorCode = data.error?.code;
      payment.errorMessage = data.error?.message;
      await this.paymentRepository.save(payment);
    }
  }

  /**
   * Procesar reembolso exitoso
   */
  private async handleRefundSucceeded(tenantId: string, data: any): Promise<void> {
    const payment = await this.paymentRepository.findOne({
      where: { externalId: data.payment_id, tenantId },
    });

    if (payment) {
      payment.refundedAmount = Number(payment.refundedAmount || 0) + data.amount;
      payment.refundedAt = new Date();

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

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

  /**
   * Verificar firma de webhook
   */
  private verifyWebhookSignature(
    payload: string,
    signature: string,
    secret: string
  ): boolean {
    try {
      const expected = createHmac('sha256', secret).update(payload, 'utf8').digest('hex');
      return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    } catch {
      return false;
    }
  }

  /**
   * Mapear estado de Clip a estado interno
   */
  private mapClipStatus(
    clipStatus: string
  ): 'pending' | 'processing' | 'approved' | 'rejected' | 'refunded' | 'cancelled' {
    const statusMap: Record<string, any> = {
      pending: 'pending',
      processing: 'processing',
      succeeded: 'approved',
      approved: 'approved',
      failed: 'rejected',
      declined: 'rejected',
      cancelled: 'cancelled',
      refunded: 'refunded',
    };

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

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

      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 Clip
 */
export class ClipError extends Error {
  constructor(
    message: string,
    public status: number,
    public response?: any
  ) {
    super(message);
    this.name = 'ClipError';
  }
}
