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 https from "node:https";
import {
  createNonceStr,
  createTimestamp,
  signRequestObject,
  verifyRequestObject,
} from "./telebirr.crypto";
import {
  CreateOrderRequest,
  CreateOrderResponse,
  FabricTokenResponse,
  QueryOrderResponse,
} from "./telebirr.types";

const TELEBIRR_HTTP_TIMEOUT_MS = 10_000;

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

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

  async initiate(
    input: ProviderInitiationInput,
  ): Promise<ProviderInitiationResult> {
    const fabricToken = await this.applyFabricToken();
    const requestBody = this.buildCreateOrderRequest(input);
    const response = await this.requestCreateOrder(fabricToken, requestBody);

    const prepayId = response.biz_content?.prepay_id;
    if (!prepayId) {
      throw new Error(
        `Telebirr createOrder returned no prepay_id: ${JSON.stringify(response)}`,
      );
    }

    const expiresAt = this.computeExpiresAt(
      requestBody.biz_content.timeout_express,
    );
    const platform = input.platform ?? "web";
    const clientAction =
      platform === "mobile"
        ? {
            type: "LAUNCH_APP" as const,
            appId: this.merchantAppId,
            receiveCode: response.biz_content?.receiveCode,
            shortCode: this.merchantCode,
          }
        : { type: "REDIRECT" as const, url: this.buildCheckoutUrl(prepayId) };

    return {
      providerOrderId: prepayId,
      clientAction,
      expiresAt,
      rawInitiation: {
        request: this.sanitize(requestBody),
        response,
      },
    };
  }

  async queryStatus(merchantOrderId: string): Promise<ProviderStatus> {
    const fabricToken = await this.applyFabricToken();
    const requestBody = this.buildQueryOrderRequest(merchantOrderId);
    const response = await this.postJson<QueryOrderResponse>(
      `${this.baseUrl}/payment/v1/merchant/queryOrder`,
      requestBody,
      {
        "Content-Type": "application/json",
        "X-APP-Key": this.fabricAppId,
        Authorization: fabricToken,
      },
    );

    this.logger.log(response);

    // const tradeStatus = response.biz_content?.trade_status;
    const orderStatus = response.biz_content?.order_status;
    const providerTxnId =
      response.biz_content?.trans_id ?? response.biz_content?.payment_order_id;
    const mapped = this.mapTradeStatus(orderStatus);

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

  mapTradeStatus(tradeStatus: string | undefined): ProviderPaymentStatus {
    switch (tradeStatus) {
      case "PAY_SUCCESS":
        return ProviderPaymentStatus.SUCCEEDED;
      case "PAY_FAILED":
      case "ORDER_CLOSED":
        return ProviderPaymentStatus.FAILED;
      case "WAIT_PAY":
        return ProviderPaymentStatus.REQUIRES_ACTION;
      case "PAYING":
        return ProviderPaymentStatus.PROCESSING;
      default:
        return ProviderPaymentStatus.PROCESSING;
    }
  }

  mapWebhookTradeStatus(
    tradeStatus: string | undefined,
  ): ProviderPaymentStatus {
    switch (tradeStatus?.toUpperCase()) {
      case "COMPLETED":
        return ProviderPaymentStatus.SUCCEEDED;
      case "FAILURE":
        return ProviderPaymentStatus.FAILED;
      case "PAYING":
      case "PENDING":
      case "EXPIRED":
        return ProviderPaymentStatus.PROCESSING;
      default:
        return ProviderPaymentStatus.PROCESSING;
    }
  }

  verifyWebhookSignature(payload: Record<string, unknown>): boolean {
    if (!this.publicKey) {
      this.logger.error(
        "TELEBIRR_PUBLIC_KEY not configured; rejecting all webhooks",
      );
      return false;
    }
    return verifyRequestObject(payload, this.publicKey);
  }

  private async applyFabricToken(): Promise<string> {
    const response = await this.postJson<FabricTokenResponse>(
      `${this.baseUrl}/payment/v1/token`,
      { appSecret: this.appSecret },
      {
        "Content-Type": "application/json",
        "X-APP-Key": this.fabricAppId,
      },
    );
    if (!response?.token) {
      throw new Error(
        `Telebirr token request failed: ${JSON.stringify(response)}`,
      );
    }
    return response.token;
  }

  private async requestCreateOrder(
    fabricToken: string,
    body: CreateOrderRequest,
  ): Promise<CreateOrderResponse> {
    return this.postJson<CreateOrderResponse>(
      `${this.baseUrl}/payment/v1/inapp/createOrder`,
      body,
      {
        "Content-Type": "application/json",
        "X-APP-Key": this.fabricAppId,
        Authorization: fabricToken,
      },
    );
  }

  private buildCreateOrderRequest(
    input: ProviderInitiationInput,
  ): CreateOrderRequest {
    const totalAmount = String(input.amountMinor);
    const req = {
      timestamp: createTimestamp(),
      nonce_str: createNonceStr(),
      method: "payment.preorder" as const,
      version: "1.0" as const,
      biz_content: {
        notify_url: this.notifyUrl,
        appid: this.merchantAppId,
        merch_code: this.merchantCode,
        merch_order_id: input.merchantOrderId,
        trade_type: "Checkout" as const,
        title: `EDR booking payment`,
        total_amount: totalAmount,
        trans_currency: input.currency,
        timeout_express: this.timeoutExpress,
        ...(input.redirectUrl ? { redirect_url: input.redirectUrl } : {}),
      },
    };
    const sign = signRequestObject(
      req as unknown as Record<string, unknown>,
      this.privateKey,
    );
    return { ...req, sign, sign_type: "SHA256WithRSA" };
  }

  private buildQueryOrderRequest(
    merchantOrderId: string,
  ): Record<string, unknown> {
    const req = {
      timestamp: createTimestamp(),
      nonce_str: createNonceStr(),
      method: "payment.queryorder",
      version: "1.0",
      biz_content: {
        appid: this.merchantAppId,
        merch_code: this.merchantCode,
        merch_order_id: merchantOrderId,
      },
    };
    const sign = signRequestObject(
      req as Record<string, unknown>,
      this.privateKey,
    );
    return { ...req, sign, sign_type: "SHA256WithRSA" };
  }

  private buildCheckoutUrl(prepayId: string): string {
    const map: Record<string, string> = {
      appid: this.merchantAppId,
      merch_code: this.merchantCode,
      nonce_str: createNonceStr(),
      prepay_id: prepayId,
      timestamp: createTimestamp(),
    };
    const sign = signRequestObject(map, this.privateKey);
    const rawRequest = [
      `appid=${map.appid}`,
      `merch_code=${map.merch_code}`,
      `nonce_str=${map.nonce_str}`,
      `prepay_id=${map.prepay_id}`,
      `timestamp=${map.timestamp}`,
      "sign_type=SHA256WithRSA",
      `sign=${sign}`,
      "version=1.0",
      "trade_type=Checkout",
    ].join("&");
    return `${this.webBaseUrl}${rawRequest}`;
  }

  private computeExpiresAt(timeoutExpress: string): Date {
    const match = /^(\d+)([smhd])$/.exec(timeoutExpress);
    const minutes = match
      ? this.toMinutes(parseInt(match[1], 10), match[2])
      : 15;
    return new Date(Date.now() + minutes * 60_000);
  }

  private toMinutes(n: number, unit: string): number {
    switch (unit) {
      case "s":
        return Math.max(1, Math.round(n / 60));
      case "m":
        return n;
      case "h":
        return n * 60;
      case "d":
        return n * 60 * 24;
      default:
        return 15;
    }
  }

  private async postJson<T>(
    url: string,
    body: unknown,
    headers: Record<string, string>,
  ): Promise<T> {
    const config: AxiosRequestConfig = {
      headers,
      timeout: TELEBIRR_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(
        `Telebirr POST ${url} status=${res.status} latency=${Date.now() - started}ms`,
      );
      return res.data;
    } catch (err) {
      if (err instanceof AxiosError) {
        this.logger.error(
          `Telebirr POST ${url} failed: status=${err.response?.status} body=${JSON.stringify(err.response?.data)} code=${err.code} message=${err.message}`,
        );
      } else {
        this.logger.error(
          `Telebirr POST ${url} threw: ${err instanceof Error ? err.message : err}`,
        );
      }
      throw err;
    }
  }

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

  private get baseUrl(): string {
    return this.config.get<string>("telebirr.baseUrl") ?? "";
  }
  private get webBaseUrl(): string {
    return this.config.get<string>("telebirr.webBaseUrl") ?? "";
  }
  private get fabricAppId(): string {
    return this.config.get<string>("telebirr.fabricAppId") ?? "";
  }
  private get appSecret(): string {
    return this.config.get<string>("telebirr.appSecret") ?? "";
  }
  private get merchantAppId(): string {
    return this.config.get<string>("telebirr.merchantAppId") ?? "";
  }
  private get merchantCode(): string {
    return this.config.get<string>("telebirr.merchantCode") ?? "";
  }
  private get notifyUrl(): string {
    return this.config.get<string>("telebirr.notifyUrl") ?? "";
  }
  private get timeoutExpress(): string {
    return this.config.get<string>("telebirr.timeoutExpress") ?? "15m";
  }
  private get privateKey(): string {
    return `-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/ZcoOng1sJZ4CegopQVCw3HYqqVRLEudgT+dDpS8fRVy7zBgqZunju2VRCQuHeWs7yWgc9QGd4/8kRSLY+jlvKNeZ60yWcqEY+eKyQMmcjOz2Sn41fcVNgF+HV3DGiV4b23B6BCMjnpEFIb9d99/TsjsFSc7gCPgfl2yWDxE/Y1B2tVE6op2qd63YsMVFQGdre/CQYvFJENpQaBLMq4hHyBDgluUXlF0uA1X7UM0ZjbFC6ZIB/Hn1+pl5Ua8dKYrkVaecolmJT/s7c/+/1JeN+ja8luBoONsoODt2mTeVJHLF9Y3oh5rI+IY8HukIZJ1U6O7/JcjH3aRJTZagXUS9AgMBAAECggEBALBIBx8JcWFfEDZFwuAWeUQ7+VX3mVx/770kOuNx24HYt718D/HV0avfKETHqOfA7AQnz42EF1Yd7Rux1ZO0e3unSVRJhMO4linT1XjJ9ScMISAColWQHk3wY4va/FLPqG7N4L1w3BBtdjIc0A2zRGLNcFDBlxl/CVDHfcqD3CXdLukm/friX6TvnrbTyfAFicYgu0+UtDvfxTL3pRL3u3WTkDvnFK5YXhoazLctNOFrNiiIpCW6dJ7WRYRXuXhz7C0rENHyBtJ0zura1WD5oDbRZ8ON4v1KV4QofWiTFXJpbDgZdEeJJmFmt5HIi+Ny3P5n31WwZpRMHGeHrV23//0CgYEA+2/gYjYWOW3JgMDLX7r8fGPTo1ljkOUHuH98H/a/lE3wnnKKx+2ngRNZX4RfvNG4LLeWTz9plxR2RAqqOTbX8fj/NA/sS4mru9zvzMY1925FcX3WsWKBgKlLryl0vPScq4ejMLSCmypGz4VgLMYZqT4NYIkU2Lo1G1MiDoLy0CcCgYEAwt77exynUhM7AlyjhAA2wSINXLKsdFFF1u976x9kVhOfmbAutfMJPEQWb2WXaOJQMvMpgg2rU5aVsyEcuHsRH/2zatrxrGqLqgxaiqPz4ELINIh1iYK/hdRpr1vATHoebOv1wt8/9qxITNKtQTgQbqYci3KV1lPsOrBAB5S57nsCgYAvw+cagS/jpQmcngOEoh8I+mXgKEET64517DIGWHe4kr3dO+FFbc5eZPCbhqgxVJ3qUM4LK/7BJq/46RXBXLvVSfohR80Z5INtYuFjQ1xJLveeQcuhUxdK+95W3kdBBi8lHtVPkVsmYvekwK+ukcuaLSGZbzE4otcn47kajKHYDQKBgDbQyIbJ+ZsRw8CXVHu2H7DWJlIUBIS3s+CQ/xeVfgDkhjmSIKGX2to0AOeW+S9MseiTE/L8a1wY+MUppE2UeK26DLUbH24zjlPoI7PqCJjl0DFOzVlACSXZKV1lfsNEeriC61/EstZtgezyOkAlSCIH4fGr6tAeTU349Bnt0RtvAoGBAObgxjeH6JGpdLz1BbMj8xUHuYQkbxNeIPhH29CySn0vfhwg9VxAtIoOhvZeCfnsCRTj9OZjepCeUqDiDSoFznglrKhfeKUndHjvg+9kiae92iI6qJudPCHMNwP8wMSphkxUqnXFR3lr9A765GA980818UWZdrhrjLKtIIZdh+X1\n-----END PRIVATE KEY-----`
  }
  private get publicKey(): string {
    return this.config.get<string>("telebirr.publicKey") ?? "";
  }
}
