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

/**
 * Credenciales esperadas en `MerchantProviderConfig.credentials` para dLocal Go:
 *   - apiKey: string       (API Key del dashboard dLocal Go)
 *   - secretKey: string    (Secret Key del dashboard dLocal Go — auth + firma webhooks)
 *
 * Auth: `Authorization: Bearer <apiKey>:<secretKey>` (formato dLocal Go).
 */
export interface DlocalCredentials {
  apiKey: string;
  secretKey: string;
}

interface DlocalConfig {
  baseUrl: string;
  mode: "sandbox" | "live";
  credentials: DlocalCredentials;
}

interface DlocalPaymentResponse {
  id: string;
  status: string;
  status_code?: string;
  status_detail?: string;
  redirect_url?: string;
  payment_method_type?: string;
}

const STATUS_MAP: Record<string, ProviderPaymentStatus> = {
  PENDING: "pending",
  AUTHORIZED: "processing",
  VERIFIED: "processing",
  PAID: "approved",
  REJECTED: "rejected",
  CANCELLED: "rejected",
  EXPIRED: "expired",
  REFUNDED: "approved",
};

const METHOD_MAP: Record<string, PaymentMethod> = {
  CARD: PaymentMethod.card,
  TICKET: PaymentMethod.cash_voucher,
  BANK_TRANSFER: PaymentMethod.bank_transfer,
  PIX: PaymentMethod.pix,
  WALLET: PaymentMethod.wallet,
};

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

  constructor(private readonly config: DlocalConfig) {
    this.http = axios.create({
      baseURL: config.baseUrl,
      timeout: 30_000,
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
        Authorization: `Bearer ${config.credentials.apiKey}:${config.credentials.secretKey}`,
        "User-Agent": "NovasisPay/1.0",
      },
    });
  }

  async createPayment(input: ProviderCreatePaymentInput): Promise<ProviderCreatePaymentResult> {
    const body = {
      amount: Number(input.amount),
      currency: input.currency,
      country: input.country,
      order_id: input.intentId,
      description: input.description ?? `Intent ${input.intentId}`,
      success_url: input.returnUrl,
      back_url: input.cancelUrl,
      notification_url: input.notificationUrl,
      payer: input.customer ? this.buildPayer(input.customer, input.country) : undefined,
    };

    try {
      const { data } = await this.http.post<DlocalPaymentResponse>("/v1/payments", body);
      return {
        providerPaymentId: data.id,
        status: this.mapStatus(data.status),
        method: this.mapMethod(data.payment_method_type),
        redirectUrl: data.redirect_url ?? null,
        responseCode: data.status_code ?? null,
        responseMessage: data.status_detail ?? null,
        rawRequest: body,
        rawResponse: data,
      };
    } catch (err) {
      const detail = this.errMsg(err);
      this.logger.error(`dLocal createPayment failed: ${detail}`);
      // Reempaquetamos el AxiosError genérico con el detalle real de dLocal para
      // que el caller (y los logs de ExceptionsHandler) muestren el motivo.
      if (axios.isAxiosError(err)) {
        throw new BadGatewayException(`dLocal rechazó el pago: ${detail}`);
      }
      throw err;
    }
  }

  async getPayment(providerPaymentId: string): Promise<ProviderGetPaymentResult> {
    const { data } = await this.http.get<DlocalPaymentResponse>(
      `/v1/payments/${providerPaymentId}`,
    );
    return {
      providerPaymentId: data.id,
      status: this.mapStatus(data.status),
      responseCode: data.status_code ?? null,
      responseMessage: data.status_detail ?? null,
      rawResponse: data,
    };
  }

  async refund(input: ProviderRefundInput): Promise<ProviderRefundResult> {
    const body = {
      payment_id: input.providerPaymentId,
      amount: Number(input.amount),
      notes: input.reason ?? undefined,
    };
    const { data } = await this.http.post<{ id: string; status: string; status_detail?: string }>(
      "/v1/refunds",
      body,
    );
    return {
      providerRefundId: data.id,
      status: this.mapRefundStatus(data.status),
      responseMessage: data.status_detail ?? null,
      rawRequest: body,
      rawResponse: data,
    };
  }

  parseWebhook(rawBody: string, headers: Record<string, string>) {
    const signature = headers["x-dlocalgo-signature"] ?? headers["X-Dlocalgo-Signature"];
    if (!signature) return null;
    const expected = createHmac("sha256", this.config.credentials.secretKey)
      .update(rawBody, "utf8")
      .digest("hex");
    const a = Buffer.from(expected, "hex");
    const b = Buffer.from(signature, "hex");
    if (a.length !== b.length || !timingSafeEqual(a, b)) return null;

    try {
      const parsed = JSON.parse(rawBody) as DlocalPaymentResponse;
      return {
        providerPaymentId: parsed.id,
        status: this.mapStatus(parsed.status),
        raw: parsed,
      };
    } catch {
      return null;
    }
  }

  /**
   * Construye el objeto `payer` que dLocal Go espera.
   *
   * dLocal exige que `document` y `document_type` viajen juntos. Si nuestro
   * customer trae documento pero no tipo, inferimos el tipo por país (heurística
   * conservadora: solo se setea cuando hay un default obvio para ese country).
   * Si no podemos inferirlo, omitimos ambos campos — dLocal los pedirá en su
   * propio checkout.
   */
  private buildPayer(customer: NonNullable<ProviderCreatePaymentInput["customer"]>, country: string) {
    const rawDocType = customer.docType ?? this.defaultDocTypeForCountry(country, customer.docNumber);
    // dLocal exige el documentType en MAYÚSCULAS (RUC, CI, DNI, etc.).
    // Nuestro enum interno de Prisma vive en minúsculas, así que normalizamos acá.
    const docType = rawDocType ? rawDocType.toUpperCase() : undefined;
    const includeDoc = !!customer.docNumber && !!docType;
    return {
      name: customer.name ?? undefined,
      email: customer.email ?? undefined,
      phone: customer.phone ?? undefined,
      ...(includeDoc ? { document: customer.docNumber, document_type: docType } : {}),
    };
  }

  private defaultDocTypeForCountry(country: string, docNumber: string | null | undefined): string | undefined {
    if (!docNumber) return undefined;
    const c = country.toUpperCase();
    // RUCs paraguayos suelen incluir guion (`-`); el resto son CI.
    if (c === "PY") return docNumber.includes("-") ? "RUC" : "CI";
    return undefined;
  }

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

  private mapMethod(type: string | undefined): PaymentMethod {
    if (!type) return PaymentMethod.other;
    return METHOD_MAP[type.toUpperCase()] ?? PaymentMethod.other;
  }

  private mapRefundStatus(status: string): "pending" | "approved" | "rejected" | "error" {
    const s = status.toUpperCase();
    if (s === "PAID" || s === "APPROVED" || s === "SUCCESS") return "approved";
    if (s === "REJECTED" || s === "FAILED" || s === "CANCELLED") return "rejected";
    if (s === "PENDING" || s === "PROCESSING") return "pending";
    return "error";
  }

  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);
  }
}
