import { BadGatewayException, Logger, NotImplementedException } from "@nestjs/common";
import { PaymentMethod, Provider } from "@prisma/client";
import axios, { AxiosInstance } from "axios";
import { createHash } from "node:crypto";
import {
  ProviderAdapter,
  ProviderCreatePaymentInput,
  ProviderCreatePaymentResult,
  ProviderGetPaymentResult,
  ProviderPaymentStatus,
  ProviderRefundInput,
  ProviderRefundResult,
} from "../provider-adapter.interface";

/**
 * Credenciales esperadas en `MerchantProviderConfig.credentials` para Dpago:
 *   - publicToken:  string  (Token público — se usa para firmar el request)
 *   - privateToken: string  (Token privado — valida el callback / URL de respuesta)
 *   - commerceId:   number  (ID del comercio en Dpago, ej. 829)
 *   - defaultPlatformId?: string  (medio de pago por defecto si el intent no trae uno)
 *
 * A diferencia de dLocal, Dpago NO usa header de autenticación: la autenticidad
 * del request se prueba con el campo `token = SHA256(reference + amount + publicToken)`.
 */
export interface DpagoCredentials {
  publicToken: string;
  privateToken: string;
  commerceId: number;
  defaultPlatformId?: string;
}

interface DpagoConfig {
  baseUrl: string;
  mode: "sandbox" | "live";
  credentials: DpagoCredentials;
}

interface DpagoCreateResponse {
  reference: string; // ID interno de la transacción en Dpago
  redirectUrl: string | null;
  error: string | null;
  qrInformation: unknown | null;
}

/**
 * Respuesta de `POST /links` (link de pago). A diferencia de `/transactions`,
 * este endpoint NO requiere token/customer/platformId y funciona en dev.
 * El campo `link` es la URL a compartir con el cliente (vence a los 30 días).
 */
interface DpagoLinkResponse {
  id: number;
  code: string; // ej. pl_abc123...
  link: string; // https://pago.dpago.com/link?code=...
  status: string; // active | ...
  error?: string | null;
}

interface DpagoStatusResponse {
  reference: string;
  status: string; // pending | completed | refused | reversed | incomplete
  error?: string | null;
}

/** `GET /transactions/:id` — estado de una transacción. */
interface DpagoTransactionResponse {
  id: number;
  status: string; // completed | pending | refused | reversed | incomplete
  commerceReference?: string;
}

/** `GET /links/:code` — estado de un link de pago vigente. */
interface DpagoLinkStatusResponse {
  id: number;
  code: string;
  status: string; // active | ...
  paidTransactionId: number | null;
}

/** Error tipado de la API de Dpago (ej. link USED/INVALID/NOT_FOUND). */
interface DpagoApiError {
  code?: string;
  message?: string;
  details?: unknown;
}

const STATUS_MAP: Record<string, ProviderPaymentStatus> = {
  pending: "pending",
  incomplete: "processing",
  completed: "approved",
  refused: "rejected",
  reversed: "rejected",
};

export class DpagoAdapter implements ProviderAdapter {
  readonly provider: Provider = Provider.dpago;
  private readonly logger = new Logger(DpagoAdapter.name);
  private readonly http: AxiosInstance;

  constructor(private readonly config: DpagoConfig) {
    this.http = axios.create({
      baseURL: config.baseUrl,
      timeout: 30_000,
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
        "User-Agent": "NovasisPay/1.0",
      },
    });
  }

  /**
   * Dpago expone dos modalidades (ver guía de integración):
   *   - Con platformId → transacción DIRECTA (`POST /transactions`): el comercio
   *     ya sabe el medio de pago. En prod devuelve `qrInformation` (QR embebido)
   *     y/o `redirectUrl`. El pago directo sólo opera en producción.
   *   - Sin platformId → LINK DE PAGO (`POST /links`): genera una URL para
   *     compartir; el cliente elige el medio en la pasarela. Funciona en dev.
   */
  async createPayment(input: ProviderCreatePaymentInput): Promise<ProviderCreatePaymentResult> {
    // Link de pago explícito (Dpago `/links`): tiene prioridad sobre cualquier
    // platformId/defaultPlatformId — si el cliente pidió un link, damos un link.
    if (input.preferLink) {
      return this.createPaymentLink(input);
    }
    const platformId = input.platformId ?? this.config.credentials.defaultPlatformId;
    if (!platformId) {
      return this.createPaymentLink(input);
    }
    return this.createDirectTransaction(input, platformId);
  }

  /** `POST /transactions` — transacción directa con medio de pago conocido. */
  private async createDirectTransaction(
    input: ProviderCreatePaymentInput,
    platformId: string,
  ): Promise<ProviderCreatePaymentResult> {
    // `reference` debe ser único por transacción → usamos el id del intent.
    const reference = input.intentId;
    const amount = this.formatAmount(input.amount);
    const token = this.signCreate(reference, amount);

    const body = {
      amount: Number(amount),
      description: input.description ?? `Intent ${input.intentId}`,
      reference,
      // `withInvoice` (opcional): indica si Dpago debe emitir factura. Por defecto
      // enviamos false — la facturación la maneja el comercio en su propio sistema.
      withInvoice: false,
      customer: {
        email: input.customer?.email ?? undefined,
        identification: input.customer?.docNumber ?? undefined,
        name: input.customer?.name ?? undefined,
        phone: input.customer?.phone ?? undefined,
      },
      token,
      platformId: Number(platformId),
      // El panel admin guarda las credenciales como strings; Dpago espera número.
      commerceId: Number(this.config.credentials.commerceId),
      orderNumber: input.externalReference ?? reference,
    };

    try {
      const { data } = await this.http.post<DpagoCreateResponse>("/transactions", body);
      if (data.error) {
        throw new BadGatewayException(`Dpago rechazó el pago: ${data.error}`);
      }
      return {
        providerPaymentId: data.reference,
        // Al crear, Dpago devuelve el redirect y la transacción queda pendiente
        // hasta que el comprador completa el pago.
        status: "pending",
        method: input.method ?? PaymentMethod.other,
        redirectUrl: data.redirectUrl ?? null,
        qrData: data.qrInformation ?? null,
        responseCode: null,
        responseMessage: data.qrInformation ? "qr" : null,
        rawRequest: this.redactRequest(body),
        rawResponse: data,
      };
    } catch (err) {
      const detail = this.errMsg(err);
      this.logger.error(`Dpago createPayment (transactions) failed: ${detail}`);
      if (axios.isAxiosError(err)) {
        throw new BadGatewayException(`Dpago rechazó el pago: ${detail}`);
      }
      throw err;
    }
  }

  /**
   * `POST /links` — link de pago para compartir (WhatsApp, email, etc.). No usa
   * token ni customer. `isTest` sigue el modo del provider (sandbox → true).
   * Nota: la guía no documenta un GET de estado por code de link, así que la
   * confirmación de estos pagos depende del webhook (paidTransactionId).
   */
  private async createPaymentLink(
    input: ProviderCreatePaymentInput,
  ): Promise<ProviderCreatePaymentResult> {
    const body = {
      commerceId: Number(this.config.credentials.commerceId),
      amount: Number(this.formatAmount(input.amount)),
      description: input.description ?? `Intent ${input.intentId}`,
      commerceReference: input.externalReference ?? input.intentId,
      isTest: this.config.mode === "sandbox",
    };

    try {
      const { data } = await this.http.post<DpagoLinkResponse>("/links", body);
      if (data.error) {
        throw new BadGatewayException(`Dpago rechazó el link de pago: ${data.error}`);
      }
      return {
        // Guardamos el `code` del link como id del pago; el paidTransactionId real
        // llega por webhook cuando el cliente completa el pago.
        providerPaymentId: data.code ?? String(data.id),
        status: "pending",
        method: input.method ?? PaymentMethod.other,
        redirectUrl: data.link ?? null,
        qrData: null,
        responseCode: data.code ?? null,
        responseMessage: "link",
        rawRequest: body,
        rawResponse: data,
      };
    } catch (err) {
      const detail = this.errMsg(err);
      this.logger.error(`Dpago createPayment (links) failed: ${detail}`);
      if (axios.isAxiosError(err)) {
        throw new BadGatewayException(`Dpago rechazó el link de pago: ${detail}`);
      }
      throw err;
    }
  }

  async getPayment(providerPaymentId: string): Promise<ProviderGetPaymentResult> {
    // Los cobros por LINK guardan el `code` del link (pl_...). Su estado se
    // consulta distinto a una transacción directa (que guarda un id numérico).
    if (providerPaymentId.startsWith("pl_")) {
      return this.getPaymentByLink(providerPaymentId);
    }
    const { data } = await this.http.get<DpagoTransactionResponse>(
      `/transactions/${encodeURIComponent(providerPaymentId)}`,
    );
    return {
      providerPaymentId: String(data.id ?? providerPaymentId),
      status: this.mapStatus(data.status),
      responseCode: null,
      responseMessage: data.status ?? null,
      rawResponse: data,
    };
  }

  /**
   * Estado de un cobro por LINK. Dpago responde:
   *  - 200 `active` (o con `paidTransactionId`) → link vigente / con transacción.
   *  - error `USED` → link de un solo uso ya consumido = PAGADO.
   *  - error `INVALID` → link invalidado (cancelado) = rechazado.
   *  - error `NOT_FOUND` → no existe (lo tratamos como pendiente).
   * (Comportamiento observado contra la API; el webhook sigue siendo la fuente
   * autoritativa cuando esté disponible.)
   */
  private async getPaymentByLink(code: string): Promise<ProviderGetPaymentResult> {
    try {
      const { data } = await this.http.get<DpagoLinkStatusResponse>(
        `/links/${encodeURIComponent(code)}`,
      );
      if (data.paidTransactionId) {
        return this.statusFromTransaction(data.paidTransactionId, code, data);
      }
      return {
        providerPaymentId: code,
        status: data.status === "active" ? "pending" : this.mapStatus(data.status),
        responseCode: null,
        responseMessage: data.status ?? null,
        rawResponse: data,
      };
    } catch (err) {
      if (axios.isAxiosError(err) && err.response) {
        const body = (err.response.data ?? {}) as DpagoApiError;
        const map: Record<string, ProviderPaymentStatus> = {
          USED: "approved", // link single-use consumido = pagado
          INVALID: "rejected", // link invalidado/cancelado
          NOT_FOUND: "pending",
        };
        const status = body.code ? map[body.code] : undefined;
        if (status) {
          return {
            providerPaymentId: code,
            status,
            responseCode: body.code ?? null,
            responseMessage: body.message ?? null,
            rawResponse: body,
          };
        }
      }
      throw err;
    }
  }

  /** Lee el estado real de la transacción que pagó un link. */
  private async statusFromTransaction(
    transactionId: number,
    code: string,
    linkData: unknown,
  ): Promise<ProviderGetPaymentResult> {
    try {
      const { data } = await this.http.get<DpagoTransactionResponse>(
        `/transactions/${transactionId}`,
      );
      return {
        providerPaymentId: code,
        status: this.mapStatus(data.status),
        responseCode: null,
        responseMessage: data.status ?? null,
        rawResponse: { link: linkData, transaction: data },
      };
    } catch {
      // Si no podemos leer la transacción pero el link ya tiene una, está pago.
      return {
        providerPaymentId: code,
        status: "approved",
        responseCode: null,
        responseMessage: "paid",
        rawResponse: linkData,
      };
    }
  }

  async refund(_input: ProviderRefundInput): Promise<ProviderRefundResult> {
    // La API pública de Dpago no documenta refunds a la fecha de esta integración.
    throw new NotImplementedException("Dpago no soporta refunds vía API todavía");
  }

  /**
   * Callback de Dpago ("URL de respuesta"). Formato real observado:
   *   { id: <txId>, status: "completed"|..., token: "<hash>", amount, commerce, customer,
   *     platform, orderNumber, commerceReference, ... }
   * La firma viene en el body (`token`) con una fórmula NO documentada que no podemos
   * reproducir con certeza. Por eso acá NO validamos el token: devolvemos el evento
   * bien formado y el gateway **re-confirma el estado real con `getPayment(id)`**
   * (GET /transactions/:id) antes de aplicarlo. Así un tercero no puede falsear un
   * "completed" (la verdad la da la API de Dpago, no el payload entrante).
   */
  parseWebhook(rawBody: string) {
    let parsed: { id?: number | string; status?: string; token?: string };
    try {
      parsed = JSON.parse(rawBody);
    } catch {
      return null;
    }
    const id = parsed.id != null ? String(parsed.id) : null;
    if (!id || !parsed.status) return null;

    return {
      providerPaymentId: id,
      status: this.mapStatus(parsed.status),
      raw: parsed,
    };
  }

  /** `token = SHA256(reference + amount + publicToken)` (ver guía Dpago). */
  private signCreate(reference: string, amount: string): string {
    return createHash("sha256")
      .update(`${reference}${amount}${this.config.credentials.publicToken}`, "utf8")
      .digest("hex");
  }

  /**
   * Dpago espera el monto como entero (PYG no tiene decimales). Nuestro amount
   * viaja como decimal serializado (ej. "150000.0000"); lo truncamos al entero.
   * Validado contra el sandbox de Dpago (comercio 829): con este formato de
   * amount y `token = SHA256(reference + amount + publicToken)` la transacción
   * se crea OK (HTTP 201). Revisar sólo si aparecen montos con decimales (USD).
   */
  private formatAmount(amount: string): string {
    return String(Math.trunc(Number(amount)));
  }

  private mapStatus(status: string | undefined): ProviderPaymentStatus {
    if (!status) return "pending";
    return STATUS_MAP[status.toLowerCase()] ?? "processing";
  }

  /** No logueamos el token de firma en rawRequest persistido. */
  private redactRequest(body: Record<string, unknown>): Record<string, unknown> {
    return { ...body, token: "[redacted]" };
  }

  private errMsg(err: unknown): string {
    if (axios.isAxiosError(err)) {
      return `HTTP ${err.response?.status ?? "?"} ${JSON.stringify(err.response?.data ?? {})}`;
    }
    return err instanceof Error ? err.message : String(err);
  }
}
