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 DMoneyAuthResponse {
  token: string;
}

interface DMoneyInitiateRequest {
  merchantId: string;
  merchantOrderId: string;
  amount: string;
  currency: string;
  description: string;
  returnUrl: string;
  notifyUrl: string;
  payerPhone?: string;
  timestamp: string;
  signature: string;
}

interface DMoneyInitiateResponse {
  success: boolean;
  orderId: string;
  checkoutUrl?: string;
  expiresIn: number;
}

interface DMoneyQueryResponse {
  success: boolean;
  orderId: string;
  status: string;
  transactionId?: string;
  amount?: string;
  currency?: string;
  paidAt?: string;
  payerPhone?: string;
}

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

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

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

    const requestBody: DMoneyInitiateRequest = {
      merchantId: this.merchantId,
      merchantOrderId: input.merchantOrderId,
      amount,
      currency: input.currency,
      description: `EDR ${input.orderRef}`,
      returnUrl: this.returnUrl,
      notifyUrl: this.notifyUrl,
      timestamp,
      signature: this.signRequest({
        merchantId: this.merchantId,
        merchantOrderId: input.merchantOrderId,
        amount,
        timestamp,
      }),
    };

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

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

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

    return {
      providerOrderId: response.orderId,
      clientAction: response.checkoutUrl
        ? { type: "REDIRECT", url: response.checkoutUrl }
        : {
            type: "REDIRECT",
            url: `${this.baseUrl}/checkout/${response.orderId}`,
          },
      expiresAt,
      rawInitiation: {
        request: this.sanitize(requestBody),
        response,
      },
    };
  }

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

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

    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":
      case "CANCELLED":
        return ProviderPaymentStatus.FAILED;
      case "PENDING":
        return ProviderPaymentStatus.REQUIRES_ACTION;
      case "PROCESSING":
        return ProviderPaymentStatus.PROCESSING;
      default:
        return ProviderPaymentStatus.PROCESSING;
    }
  }

  private async getFabricToken(): Promise<string> {
    const response = await this.postJson<DMoneyAuthResponse>(
      `${this.baseUrl}/apiaccess/payment/gateway/payment/v1/token`,
      {
        appSecret: this.appSecret,
      },
    );

    if (!response.token) {
      throw new Error(
        `DMoney authentication failed: ${JSON.stringify(response)}`,
      );
    }

    return response.token;
  }

  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,
    token?: string,
  ): Promise<T> {
    const headers: Record<string, string> = {
      "Content-Type": "application/json",
    };
    if (token) {
      headers["Authorization"] = `Bearer ${token}`;
    }

    const config: AxiosRequestConfig = {
      headers,
      timeout: 10_000,
    };

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

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

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