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 CbeBirrInitiateRequest {
  merchantId: string;
  merchantOrderId: string;
  amount: string;
  currency: string;
  description: string;
  returnUrl: string;
  notifyUrl: string;
  timestamp: string;
  signature: string;
}

interface CbeBirrInitiateResponse {
  success: boolean;
  orderId: string;
  paymentUrl: string;
  expiresIn: number;
}

interface CbeBirrQueryResponse {
  success: boolean;
  orderId: string;
  status: string;
  transactionId?: string;
  amount?: string;
  paidAt?: string;
}

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

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

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

    const requestBody: CbeBirrInitiateRequest = {
      merchantId: this.merchantId,
      merchantOrderId: input.merchantOrderId,
      amount,
      currency: input.currency,
      description: `EDR ${input.orderRef}`,
      // Per-transaction browser return target (each calling app has its own UI); config is fallback.
      returnUrl: input.returnUrl ?? this.returnUrl,
      notifyUrl: this.notifyUrl,
      timestamp,
      signature: this.signRequest({
        merchantId: this.merchantId,
        merchantOrderId: input.merchantOrderId,
        amount,
        timestamp,
      }),
    };

    const response = await this.postJson<CbeBirrInitiateResponse>(
      `${this.baseUrl}/api/v1/payment/initiate`,
      requestBody,
    );

    if (!response.success || !response.orderId) {
      throw new Error(`CBE Birr initiate failed: ${JSON.stringify(response)}`);
    }

    const expiresAt = new Date(Date.now() + response.expiresIn * 1000);

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

  async queryStatus(merchantOrderId: string): Promise<ProviderStatus> {
    const timestamp = new Date().toISOString();
    const signature = this.signRequest({
      merchantId: this.merchantId,
      merchantOrderId,
      timestamp,
    });

    const response = await this.postJson<CbeBirrQueryResponse>(
      `${this.baseUrl}/api/v1/payment/query`,
      {
        merchantId: this.merchantId,
        merchantOrderId,
        timestamp,
        signature,
      },
    );

    const mapped = this.mapStatus(response.status);

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

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

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

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

  private mapStatus(status: string): ProviderPaymentStatus {
    switch (status?.toUpperCase()) {
      case "SUCCESS":
      case "COMPLETED":
        return ProviderPaymentStatus.SUCCEEDED;
      case "FAILED":
      case "REJECTED":
      case "EXPIRED":
        return ProviderPaymentStatus.FAILED;
      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("&");

    return crypto
      .createHmac("sha256", this.secretKey)
      .update(signString)
      .digest("hex");
  }

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

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

  private sanitize(body: CbeBirrInitiateRequest): Record<string, unknown> {
    const { signature: _signature, ...rest } = body;
    return rest;
  }

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