import {
  BadGatewayException,
  Injectable,
  Logger,
  UnauthorizedException,
} from '@nestjs/common';
import { NovasisPayConfigService } from './novasis-pay-config.service';

interface RequestOptions {
  method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
  body?: unknown;
  timeoutMs?: number;
}

export interface CreatePaymentIntentArgs {
  /** Monto en sub-unidades (ej. PYG: 50000 = 50.000 Gs; USD: 1500 = $15.00). */
  amount: number;
  currency: string;
  country: string;
  description?: string;
  externalReference?: string;
  requestedProvider?: string | null;
  /** Customer pre-creado en el gateway. El checkout lo usa para prellenar email/nombre/doc. */
  customerId?: string;
  /** Medio de pago del provider (ej. platformId de Dpago). Requerido para cobros directos. */
  platformId?: string;
  /** Header Idempotency-Key opcional para evitar duplicados ante retries. */
  idempotencyKey?: string;
}

export interface ConfirmPaymentIntentArgs {
  /** Medio de pago del provider (ej. platformId de Dpago). Requerido por Dpago en modo directo. */
  platformId?: string;
  /** Pedir un link de pago nativo del provider (ej. Dpago /links) en vez de transacción directa. */
  paymentLink?: boolean;
  returnUrl?: string;
  cancelUrl?: string;
}

export interface ConfirmPaymentIntentResponse {
  intent: { id: string; status: string; [k: string]: unknown };
  attempt: {
    id: string;
    status: string;
    providerPaymentId: string | null;
    redirectUrl: string | null;
    qr: unknown | null;
    [k: string]: unknown;
  };
}

export interface ProviderPaymentMethod {
  platformId: string;
  name: string;
  method: string;
}

export interface PaymentIntentResponse {
  id: string;
  merchantId: string;
  status: string;
  amount: string;
  currency: string;
  country: string;
}

export interface CreateCustomerArgs {
  email?: string;
  name?: string;
  /** Enum del gateway: ruc | ci | dni | rut | cuit | cuil | passport | other. */
  docType?: string;
  docNumber?: string;
  phone?: string;
  /** ISO 3166-1 alpha-2. */
  country?: string;
}

export interface CustomerResponse {
  id: string;
  email: string | null;
  name: string | null;
  docType: string | null;
  docNumber: string | null;
  phone: string | null;
  country: string | null;
}

export interface CreateCheckoutSessionArgs {
  intentId: string;
  returnUrl?: string;
  cancelUrl?: string;
  /** Config visual del hosted checkout (ej. { qrEmbedded: true } para QR embebido). */
  uiConfig?: Record<string, unknown>;
  /** Horas de vigencia del link (1-168). Sin valor, el default del gateway (24 h). */
  expirationHours?: number;
}

export interface CheckoutSessionResponse {
  id: string;
  intentId: string;
  publicToken: string;
  url: string;
  status: string;
  expiresAt: string;
}

/**
 * Cliente HTTP del microservicio Novasis Pay.
 *
 * Reglas:
 * - Nunca persiste nada del gateway en la DB del ERP (sólo proxy / lectura puntual).
 * - El secret_key se descifra en memoria por request y no se loguea.
 * - Manejo de errores se traduce a excepciones Nest estándar para que los controllers
 *   no tengan que conocer el shape de la respuesta del gateway.
 */
@Injectable()
export class NovasisPayClient {
  private readonly logger = new Logger(NovasisPayClient.name);

  constructor(private readonly config: NovasisPayConfigService) { }

  async getProviders(empresaId?: string): Promise<unknown> {
    // El catálogo es público (no requiere auth), pero usamos el api_base_url
    // configurado para la empresa cuando existe — así un ambiente con gateway
    // de staging propio resuelve a la URL correcta sin tocar código.
    let base = 'https://api.pay.novasis.com/v1';
    if (empresaId) {
      const cfg = await this.config.getConfig(empresaId);
      if (cfg?.api_base_url) base = cfg.api_base_url;
    }
    return this.fetchJson(`${base}/providers`, {}, {});
  }

  async getMerchantMe(empresaId: string): Promise<unknown> {
    const cfg = await this.config.requireActiveConfig(empresaId);
    return this.fetchJson(
      `${cfg.apiBaseUrl}/merchants/me`,
      { Authorization: `Bearer ${cfg.secretKey}` },
      {},
    );
  }

  async createPaymentIntent(
    empresaId: string,
    args: CreatePaymentIntentArgs,
  ): Promise<PaymentIntentResponse> {
    const cfg = await this.config.requireActiveConfig(empresaId);
    const headers: Record<string, string> = {
      Authorization: `Bearer ${cfg.secretKey}`,
    };
    if (args.idempotencyKey) headers['Idempotency-Key'] = args.idempotencyKey;

    // El gateway no acepta datos de comprador inline, sólo `customerId` de un
    // Customer pre-creado. El service de cobros se encarga de garantizar que
    // exista el customer (POST /customers + cache local) antes de llamar acá.
    const body = {
      amount: args.amount,
      currency: args.currency,
      country: args.country,
      description: args.description,
      externalReference: args.externalReference,
      // El panel guarda "" cuando el medio queda en "Predeterminado"; el gateway
      // rechaza un provider vacío (400) y el checkout online se cancelaba.
      requestedProvider: args.requestedProvider?.trim() || cfg.defaultProvider || undefined,
      customerId: args.customerId,
      platformId: args.platformId?.trim() || undefined,
    };

    return (await this.fetchJson(`${cfg.apiBaseUrl}/payment-intents`, headers, {
      method: 'POST',
      body,
    })) as PaymentIntentResponse;
  }

  /**
   * Confirma un intent directamente (sin checkout hospedado). El gateway llama al
   * provider y devuelve el attempt con `redirectUrl` y/o `qr`. Se usa en cobros
   * de modo 'directo' (ej. QR directo de Dpago).
   */
  async confirmPaymentIntent(
    empresaId: string,
    intentId: string,
    args: ConfirmPaymentIntentArgs,
  ): Promise<ConfirmPaymentIntentResponse> {
    const cfg = await this.config.requireActiveConfig(empresaId);
    return (await this.fetchJson(
      `${cfg.apiBaseUrl}/payment-intents/${intentId}/confirm`,
      { Authorization: `Bearer ${cfg.secretKey}` },
      {
        method: 'POST',
        body: {
          platformId: args.platformId,
          paymentLink: args.paymentLink,
          returnUrl: args.returnUrl,
          cancelUrl: args.cancelUrl,
        },
      },
    )) as ConfirmPaymentIntentResponse;
  }

  /**
   * Lista los medios de pago (platformIds) de un provider — catálogo público del
   * gateway. Lo usa el ERP para poblar el selector de "QR directo".
   */
  async listProviderPaymentMethods(
    empresaId: string,
    provider: string,
    country = 'PY',
  ): Promise<ProviderPaymentMethod[]> {
    const cfg = await this.config.requireActiveConfig(empresaId);
    const res = (await this.fetchJson(
      `${cfg.apiBaseUrl}/providers/${encodeURIComponent(provider)}/payment-methods?country=${encodeURIComponent(country)}`,
      {},
      {},
    )) as { methods?: ProviderPaymentMethod[] };
    return res.methods ?? [];
  }

  async createCustomer(
    empresaId: string,
    args: CreateCustomerArgs,
  ): Promise<CustomerResponse> {
    const cfg = await this.config.requireActiveConfig(empresaId);
    return (await this.fetchJson(
      `${cfg.apiBaseUrl}/customers`,
      { Authorization: `Bearer ${cfg.secretKey}` },
      { method: 'POST', body: args },
    )) as CustomerResponse;
  }

  /**
   * Busca un customer existente en el gateway por email o docNumber. Devuelve
   * el primer match (UNIQUE por merchant garantiza ≤ 1). Si no hay match,
   * devuelve null. Útil para recuperar `customerId` cuando POST /customers
   * devuelve 409 (customer ya existía pero no está en el mapping local).
   *
   * IMPORTANTE: el gateway filtra los parámetros de query con AND, así que
   * `?email=..&docNumber=..` sólo matchea si AMBOS coinciden. Como el customer
   * pudo haberse creado antes sin documento (o con otro), consultamos cada
   * criterio por separado (OR real) y devolvemos el primero que aparezca.
   */
  async findCustomerByEmailOrDoc(
    empresaId: string,
    lookup: { email?: string | null; docNumber?: string | null },
  ): Promise<CustomerResponse | null> {
    const cfg = await this.config.requireActiveConfig(empresaId);
    const queryOne = async (
      key: 'email' | 'docNumber',
      value: string,
    ): Promise<CustomerResponse | null> => {
      const params = new URLSearchParams({ limit: '1' });
      params.set(key, value);
      const res = (await this.fetchJson(
        `${cfg.apiBaseUrl}/customers?${params.toString()}`,
        { Authorization: `Bearer ${cfg.secretKey}` },
        {},
      )) as { data?: CustomerResponse[] };
      return res.data?.[0] ?? null;
    };

    if (lookup.email) {
      const byEmail = await queryOne('email', lookup.email).catch(() => null);
      if (byEmail) return byEmail;
    }
    if (lookup.docNumber) {
      const byDoc = await queryOne('docNumber', lookup.docNumber).catch(() => null);
      if (byDoc) return byDoc;
    }
    return null;
  }

  /**
   * Actualiza los datos de un customer existente (PATCH). Se usa cuando el
   * customer ya existía en el gateway (409) pero le faltan datos —
   * típicamente el documento o el teléfono de un pedido anterior — para que
   * el checkout hospedado prellene todos los campos.
   */
  async updateCustomer(
    empresaId: string,
    customerId: string,
    args: Partial<CreateCustomerArgs>,
  ): Promise<CustomerResponse> {
    const cfg = await this.config.requireActiveConfig(empresaId);
    return (await this.fetchJson(
      `${cfg.apiBaseUrl}/customers/${customerId}`,
      { Authorization: `Bearer ${cfg.secretKey}` },
      { method: 'PATCH', body: args },
    )) as CustomerResponse;
  }

  /**
   * Lee el estado actual del intent en el gateway. Se usa para sincronizar
   * manualmente cobros cuyo webhook no llegó (típico en demo / dev sin túnel).
   */
  async getPaymentIntent(
    empresaId: string,
    intentId: string,
  ): Promise<PaymentIntentResponse> {
    const cfg = await this.config.requireActiveConfig(empresaId);
    return (await this.fetchJson(
      `${cfg.apiBaseUrl}/payment-intents/${intentId}`,
      { Authorization: `Bearer ${cfg.secretKey}` },
      {},
    )) as PaymentIntentResponse;
  }

  /**
   * Le pide al gateway que pull-ee el estado del provider (dLocal etc.) y se
   * sincronice. Resuelve el caso típico de dev/demo donde el webhook del
   * provider hacia el gateway no llega.
   */
  async syncPaymentIntent(
    empresaId: string,
    intentId: string,
  ): Promise<PaymentIntentResponse> {
    const cfg = await this.config.requireActiveConfig(empresaId);
    return (await this.fetchJson(
      `${cfg.apiBaseUrl}/payment-intents/${intentId}/sync`,
      { Authorization: `Bearer ${cfg.secretKey}` },
      { method: 'POST' },
    )) as PaymentIntentResponse;
  }

  /**
   * Registra en el gateway el endpoint de webhook del ERP (para recibir en tiempo
   * real los eventos de pago). Devuelve el `secret` (whsec_…) que hay que guardar
   * como `webhook_secret` para verificar la firma de los webhooks entrantes.
   */
  async registerWebhookEndpoint(
    empresaId: string,
    url: string,
    events: string[] = [
      'payment_intent.succeeded',
      'payment_intent.failed',
      'payment_intent.expired',
      'payment_intent.processing',
    ],
  ): Promise<{ id: string; secret: string }> {
    const cfg = await this.config.requireActiveConfig(empresaId);
    const res = (await this.fetchJson(
      `${cfg.apiBaseUrl}/webhook-endpoints`,
      { Authorization: `Bearer ${cfg.secretKey}` },
      // El gateway espera el campo `subscribedEvents` (no `events`).
      { method: 'POST', body: { url, subscribedEvents: events } },
    )) as { id: string; secret: string };
    return res;
  }

  async createCheckoutSession(
    empresaId: string,
    args: CreateCheckoutSessionArgs,
  ): Promise<CheckoutSessionResponse> {
    const cfg = await this.config.requireActiveConfig(empresaId);
    return (await this.fetchJson(
      `${cfg.apiBaseUrl}/checkout-sessions`,
      { Authorization: `Bearer ${cfg.secretKey}` },
      {
        method: 'POST',
        body: {
          intentId: args.intentId,
          returnUrl: args.returnUrl,
          cancelUrl: args.cancelUrl,
          uiConfig: args.uiConfig,
          expirationHours: args.expirationHours,
        },
      },
    )) as CheckoutSessionResponse;
  }

  async testConnection(empresaId: string): Promise<{
    ok: boolean;
    merchant?: { id?: string; name?: string };
    providers?: Array<{ id: string; displayName: string; available: boolean }>;
    error?: string;
  }> {
    try {
      const [merchant, providersRes] = await Promise.all([
        this.getMerchantMe(empresaId),
        this.getProviders(empresaId),
      ]);
      const m = merchant as { id?: string; name?: string };
      const p = providersRes as {
        providers?: Array<{ id: string; displayName: string; available: boolean }>;
      };
      return {
        ok: true,
        merchant: { id: m.id, name: m.name },
        providers: p.providers ?? [],
      };
    } catch (err) {
      const e = err as Error;
      this.logger.warn(
        `Novasis Pay test-connection falló para empresa ${empresaId}: ${e.message}`,
      );
      return { ok: false, error: e.message };
    }
  }

  private async fetchJson(
    url: string,
    extraHeaders: Record<string, string>,
    opts: RequestOptions,
  ): Promise<unknown> {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? 12_000);
    try {
      const res = await fetch(url, {
        method: opts.method ?? 'GET',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
          ...extraHeaders,
        },
        body: opts.body ? JSON.stringify(opts.body) : undefined,
        signal: controller.signal,
      });

      if (res.status === 401 || res.status === 403) {
        this.logger.error(
          `Gateway ${res.status} [${opts.method ?? 'GET'} ${url}]: credenciales rechazadas`,
        );
        throw new UnauthorizedException(
          'Credenciales de Novasis Pay rechazadas por el gateway',
        );
      }
      if (!res.ok) {
        const text = await res.text().catch(() => '');
        this.logger.error(
          `Gateway ${res.status} [${opts.method ?? 'GET'} ${url}]: ${text.slice(0, 500)}`,
        );
        throw new BadGatewayException(
          `Novasis Pay respondió ${res.status}: ${text.slice(0, 200)}`,
        );
      }
      return await res.json();
    } catch (err) {
      if ((err as { name?: string }).name === 'AbortError') {
        this.logger.error(
          `Gateway TIMEOUT (${opts.timeoutMs ?? 12_000}ms) [${opts.method ?? 'GET'} ${url}]`,
        );
        throw new BadGatewayException(
          `Timeout al comunicarse con Novasis Pay (${opts.timeoutMs ?? 12_000}ms) [${opts.method ?? 'GET'} ${url}]`,
        );
      }
      // Falla de conexión (DNS/ECONNREFUSED/TLS): `fetch` tira un TypeError genérico
      // "fetch failed". Lo convertimos en un 502 accionable que nombra el servicio y la URL.
      if (err instanceof TypeError) {
        const causa = (err as { cause?: { code?: string } }).cause?.code;
        this.logger.error(
          `Gateway UNREACHABLE [${opts.method ?? 'GET'} ${url}]${causa ? ` (${causa})` : ''}`,
        );
        throw new BadGatewayException(
          `No se pudo conectar con Novasis Pay [${opts.method ?? 'GET'} ${url}]${causa ? ` (${causa})` : ''}`,
        );
      }
      // Errores ya logueados arriba (401/4xx/5xx) se relanzan sin duplicar traza.
      throw err;
    } finally {
      clearTimeout(timeout);
    }
  }
}
