import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { HttpService } from "@nestjs/axios";
import {
  PaymentProvider,
  ProviderInitiationInput,
  ProviderInitiationResult,
  ProviderStatus,
  ProviderPaymentStatus,
  ProviderMethod,
} from "@edr/types";
import { AxiosError, AxiosRequestConfig } from "axios";
import { firstValueFrom } from "rxjs";
import * as crypto from "node:crypto";
import * as https from "node:https";
import {
  WaafiGetTranInfoRequest,
  WaafiGetTranInfoResponse,
  WaafiHppPurchaseRequest,
  WaafiHppPurchaseResponse,
} from "./waafi.types";

const WAAFI_HTTP_TIMEOUT_MS = 10_000;
const WAAFI_SUCCESS_CODE = "2001";
/** Waafi cancels an unprocessed HPP session after ~5 minutes (RCS_HPP_USERACTION_TIMEOUT). */
const WAAFI_HPP_SESSION_MS = 5 * 60_000;

@Injectable()
export class WaafiProvider implements PaymentProvider {
  readonly method = ProviderMethod.WAAFI;
  private readonly logger = new Logger(WaafiProvider.name);
  private readonly httpsAgent: https.Agent;

  constructor(
    private readonly config: ConfigService,
    private readonly http: HttpService,
  ) {
    const insecure = this.config.get<boolean>("waafi.insecureTls");
    if (insecure) {
      this.logger.warn(
        "WAAFI_INSECURE_TLS=true — TLS verification disabled for Waafi calls. DEV ONLY.",
      );
    }
    this.httpsAgent = new https.Agent({ rejectUnauthorized: !insecure });
  }

  async initiate(
    input: ProviderInitiationInput,
  ): Promise<ProviderInitiationResult> {
    const requestBody = this.buildPurchaseRequest(input);
    const response = await this.postJson<WaafiHppPurchaseResponse>(
      `${this.baseUrl}/asm`,
      requestBody,
    );

    if (response.responseCode !== WAAFI_SUCCESS_CODE) {
      throw new Error(
        `Waafi HPP_PURCHASE failed: responseCode=${response.responseCode} errorCode=${response.errorCode} msg=${response.responseMsg}`,
      );
    }

    const checkoutUrl =
      response.params?.hppUrl ?? response.params?.directPaymentLink;
    const orderId = response.params?.orderId;
    if (!checkoutUrl || !orderId) {
      throw new Error(
        `Waafi HPP_PURCHASE succeeded but returned no hppUrl/orderId: ${JSON.stringify(response)}`,
      );
    }

    return {
      providerOrderId: orderId,
      clientAction: { type: "REDIRECT", url: checkoutUrl },
      expiresAt: new Date(Date.now() + WAAFI_HPP_SESSION_MS),
      rawInitiation: {
        request: this.sanitize(requestBody),
        response,
      },
    };
  }

  async queryStatus(merchantOrderId: string): Promise<ProviderStatus> {
    const requestBody = this.buildGetTranInfoRequest(merchantOrderId);
    const response = await this.postJson<WaafiGetTranInfoResponse>(
      `${this.baseUrl}/asm`,
      requestBody,
    );

    // Waafi returns transaction info (params.status) ONLY when responseCode is 2001. For an
    // unpaid or not-yet-existing transaction it returns an error envelope (e.g. 5001 / E10206
    // "Failed to get transaction info") with no status. Treat that as still-pending (PROCESSING),
    // never terminal — so the intent keeps waiting for the webhook / its expiry rather than being
    // wrongly resolved off a "no info" response.
    if (response.responseCode !== WAAFI_SUCCESS_CODE) {
      this.logger.debug(
        `Waafi HPP_GETTRANINFO ${merchantOrderId}: ${response.responseCode}/${response.errorCode} ${response.responseMsg} — treating as pending`,
      );
      return {
        status: ProviderPaymentStatus.PROCESSING,
        rawResponse: response as unknown as Record<string, unknown>,
      };
    }

    const rawState = response.params?.status ?? response.params?.tranStatusDesc;
    const transactionId = response.params?.transactionId;
    const mapped = this.mapStatus(rawState);

    return {
      status: mapped,
      providerTxnId: transactionId,
      failureCode:
        mapped === ProviderPaymentStatus.FAILED && rawState
          ? rawState
          : undefined,
      rawResponse: response as unknown as Record<string, unknown>,
    };
  }

  /** Map a webhook `payment.status` to the shared status enum. */
  mapWebhookStatus(status: string | undefined): ProviderPaymentStatus {
    return this.mapStatus(status);
  }

  /**
   * Verify an HMAC-SHA256 webhook signature.
   *
   * Signing string is `{timestamp}.{eventId}.{rawBody}` over the *raw* request body bytes — the
   * caller must pass the unparsed body string. Returns false (never throws) on any mismatch so
   * callers can treat verification as a boolean gate.
   */
  verifyWebhookSignature(
    rawBody: string,
    signature: string | undefined,
    timestamp: string | undefined,
    eventId: string | undefined,
  ): boolean {
    if (!this.webhookSecret) {
      this.logger.error(
        "WAAFI_WEBHOOK_SECRET not configured; rejecting all webhooks",
      );
      return false;
    }
    if (!signature || !timestamp || !eventId) {
      this.logger.warn(
        "Waafi webhook missing signature/timestamp/event-id headers",
      );
      return false;
    }

    const signingString = `${timestamp}.${eventId}.${rawBody}`;
    const expected = crypto
      .createHmac("sha256", this.webhookSecret)
      .update(signingString)
      .digest("hex");

    const provided = Buffer.from(signature, "utf8");
    const computed = Buffer.from(expected, "utf8");
    if (provided.length !== computed.length) return false;
    return crypto.timingSafeEqual(provided, computed);
  }

  private mapStatus(raw: string | undefined): ProviderPaymentStatus {
    switch (raw?.toUpperCase()) {
      case "APPROVED":
      case "SUCCESS":
        return ProviderPaymentStatus.SUCCEEDED;
      case "CANCELED":
      case "CANCELLED":
        return ProviderPaymentStatus.CANCELLED;
      case "DECLINED":
      case "FAILED":
      case "EXPIRED":
      case "TIMEOUT":
        return ProviderPaymentStatus.FAILED;
      case "PENDING":
      case "INITIATED":
        return ProviderPaymentStatus.REQUIRES_ACTION;
      default:
        return ProviderPaymentStatus.PROCESSING;
    }
  }

  private buildPurchaseRequest(
    input: ProviderInitiationInput,
  ): WaafiHppPurchaseRequest {
    return {
      schemaVersion: "1.0",
      requestId: crypto.randomUUID(),
      timestamp: this.timestamp(),
      channelName: "WEB",
      serviceName: "HPP_PURCHASE",
      serviceParams: {
        merchantUid: this.merchantUid,
        storeId: this.storeId,
        hppKey: this.hppKey,
        paymentMethod: this.paymentMethod,
        // Browser bounce-back is per-transaction (each calling app has its own UI), so the
        // caller-supplied URLs win; the static config is only a fallback. UX-only — the
        // webhook remains the single source of truth for payment state.
        hppSuccessCallbackUrl: input.returnUrl ?? this.successUrl,
        hppFailureCallbackUrl: input.failureUrl ?? this.failureUrl,
        hppRespDataFormat: this.respDataFormat,
        // MWALLET_ACCOUNT requires the payer phone up front; omit if the caller did not supply it
        // and let the hosted page collect it. See docs/waffi open question on payer-phone sourcing.
        ...(input.payerAccount
          ? { payerInfo: { subscriptionId: input.payerAccount } }
          : {}),
        transactionInfo: {
          referenceId: input.merchantOrderId,
          amount: this.toAmount(input.amountMinor),
          // Waafi has no ETB; `waafi.currency` overrides the booking currency when set.
          currency: this.currency || input.currency,
          description: `EDR ${input.orderRef}`,
        },
      },
    };
  }

  private buildGetTranInfoRequest(
    merchantOrderId: string,
  ): WaafiGetTranInfoRequest {
    return {
      schemaVersion: "1.0",
      requestId: crypto.randomUUID(),
      timestamp: this.timestamp(),
      channelName: "WEB",
      serviceName: "HPP_GETTRANINFO",
      serviceParams: {
        merchantUid: this.merchantUid,
        storeId: this.storeId,
        hppKey: this.hppKey,
        referenceId: merchantOrderId,
      },
    };
  }

  /** Convert integer minor units to a 2-decimal major amount (truncated, never rounded up). */
  private toAmount(amountMinor: number): number {
    return Math.trunc(amountMinor);
  }

  private timestamp(): string {
    return Math.round(Date.now() / 1000).toString();
  }

  private async postJson<T>(url: string, body: unknown): Promise<T> {
    const config: AxiosRequestConfig = {
      headers: { "Content-Type": "application/json" },
      timeout: WAAFI_HTTP_TIMEOUT_MS,
      httpsAgent: this.httpsAgent,
    };

    const started = Date.now();
    try {
      const res = await firstValueFrom(this.http.post<T>(url, body, config));
      this.logger.debug(
        `Waafi POST ${url} status=${res.status} latency=${Date.now() - started}ms`,
      );
      return res.data;
    } catch (err) {
      if (err instanceof AxiosError) {
        this.logger.error(
          `Waafi POST ${url} failed: status=${err.response?.status} body=${JSON.stringify(err.response?.data)} code=${err.code} message=${err.message}`,
        );
      } else {
        this.logger.error(
          `Waafi POST ${url} threw: ${err instanceof Error ? err.message : err}`,
        );
      }
      throw err;
    }
  }

  private sanitize(body: WaafiHppPurchaseRequest): Record<string, unknown> {
    return {
      ...body,
      serviceParams: { ...body.serviceParams, hppKey: "***REDACTED***" },
    };
  }

  private get baseUrl(): string {
    return (
      this.config.get<string>("waafi.baseUrl") ?? "https://sandbox.waafipay.net"
    );
  }
  private get merchantUid(): string {
    return this.config.get<string>("waafi.merchantUid") ?? "";
  }
  private get storeId(): string {
    return this.config.get<string>("waafi.storeId") ?? "";
  }
  private get hppKey(): string {
    return this.config.get<string>("waafi.hppKey") ?? "";
  }
  private get webhookSecret(): string {
    return this.config.get<string>("waafi.webhookSecret") ?? "";
  }
  private get paymentMethod(): string {
    return this.config.get<string>("waafi.paymentMethod") ?? "MWALLET_ACCOUNT";
  }
  private get currency(): string {
    return this.config.get<string>("waafi.currency") ?? "";
  }
  private get successUrl(): string {
    return this.config.get<string>("waafi.successUrl") ?? "";
  }
  private get failureUrl(): string {
    return this.config.get<string>("waafi.failureUrl") ?? "";
  }
  private get respDataFormat(): number {
    return this.config.get<number>("waafi.respDataFormat") ?? 1;
  }
}
