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";

interface EBirrInitiateRequest {
  merchantCode: string;
  orderNo: string;
  amount: number;
  currency: string;
  subject: string;
  body: string;
  notifyUrl: string;
  returnUrl: string;
  timestamp: number;
  sign: string;
}

interface EBirrInitiateResponse {
  code: string;
  message: string;
  data?: {
    orderNo: string;
    payUrl: string;
    expireTime: number;
  };
}

interface EBirrQueryResponse {
  code: string;
  message: string;
  data?: {
    orderNo: string;
    tradeStatus: string;
    tradeNo?: string;
    totalAmount?: number;
    payTime?: number;
  };
}

@Injectable()
export class EBirrProvider implements PaymentProvider {
  readonly method = ProviderMethod.EBIRR;
  private readonly logger = new Logger(EBirrProvider.name);

  constructor(
    private readonly config: ConfigService,
    private readonly http: HttpService,
  ) {}

  async initiate(
    input: ProviderInitiationInput,
  ): Promise<ProviderInitiationResult> {
    const amount = input.amountMinor / 100;
    const timestamp = Date.now();

    const requestBody: EBirrInitiateRequest = {
      merchantCode: this.merchantCode,
      orderNo: input.merchantOrderId,
      amount,
      currency: input.currency,
      subject: `EDR Ticket`,
      body: `Order ${input.orderRef}`,
      notifyUrl: this.notifyUrl,
      // Per-transaction browser return target (each calling app has its own UI); config is fallback.
      returnUrl: input.returnUrl ?? this.returnUrl,
      timestamp,
      sign: this.signRequest({
        merchantCode: this.merchantCode,
        orderNo: input.merchantOrderId,
        amount,
        timestamp,
      }),
    };

    const response = await this.postJson<EBirrInitiateResponse>(
      `${this.baseUrl}/gateway/api/pay/create`,
      requestBody,
    );

    if (response.code !== "0000" || !response.data?.orderNo) {
      throw new Error(`eBirr initiate failed: ${response.message}`);
    }

    const expiresAt = new Date(response.data.expireTime);

    return {
      providerOrderId: response.data.orderNo,
      clientAction: { type: "REDIRECT", url: response.data.payUrl },
      expiresAt,
      rawInitiation: {
        request: this.sanitize(requestBody),
        response,
      },
    };
  }

  async queryStatus(merchantOrderId: string): Promise<ProviderStatus> {
    const timestamp = Date.now();
    const requestBody = {
      merchantCode: this.merchantCode,
      orderNo: merchantOrderId,
      timestamp,
      sign: this.signRequest({
        merchantCode: this.merchantCode,
        orderNo: merchantOrderId,
        timestamp,
      }),
    };

    const response = await this.postJson<EBirrQueryResponse>(
      `${this.baseUrl}/gateway/api/pay/query`,
      requestBody,
    );

    if (response.code !== "0000" || !response.data) {
      throw new Error(`eBirr query failed: ${response.message}`);
    }

    const mapped = this.mapStatus(response.data.tradeStatus);

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

  verifyWebhookSignature(payload: Record<string, unknown>): boolean {
    const { sign, ...data } = payload;
    if (!sign || typeof sign !== "string") return false;

    const expectedSign = this.signRequest(data);
    return crypto.timingSafeEqual(Buffer.from(sign), Buffer.from(expectedSign));
  }

  mapWebhookStatus(tradeStatus: string): ProviderPaymentStatus {
    return this.mapStatus(tradeStatus);
  }

  private mapStatus(tradeStatus: string): ProviderPaymentStatus {
    switch (tradeStatus?.toUpperCase()) {
      case "TRADE_SUCCESS":
      case "SUCCESS":
        return ProviderPaymentStatus.SUCCEEDED;
      case "TRADE_CLOSED":
      case "TRADE_FAILED":
      case "FAILED":
        return ProviderPaymentStatus.FAILED;
      case "WAIT_BUYER_PAY":
      case "PENDING":
        return ProviderPaymentStatus.REQUIRES_ACTION;
      case "PROCESSING":
        return ProviderPaymentStatus.PROCESSING;
      default:
        return ProviderPaymentStatus.PROCESSING;
    }
  }

  private signRequest(data: Record<string, unknown>): string {
    const sortedKeys = Object.keys(data).sort();
    const signString =
      sortedKeys.map((key) => `${key}=${data[key]}`).join("&") +
      `&key=${this.secretKey}`;

    return crypto
      .createHash("md5")
      .update(signString)
      .digest("hex")
      .toUpperCase();
  }

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

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

  private sanitize(body: EBirrInitiateRequest): Record<string, unknown> {
    const { sign: _sign, ...rest } = body;
    return rest;
  }

  private get baseUrl(): string {
    return this.config.get<string>("ebirr.baseUrl") ?? "";
  }
  private get merchantCode(): string {
    return this.config.get<string>("ebirr.merchantCode") ?? "";
  }
  private get secretKey(): string {
    return this.config.get<string>("ebirr.secretKey") ?? "";
  }
  private get notifyUrl(): string {
    return this.config.get<string>("ebirr.notifyUrl") ?? "";
  }
  private get returnUrl(): string {
    return this.config.get<string>("ebirr.returnUrl") ?? "";
  }
}
