import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { PaymentIntentStatus, PaymentMethodType } from '@prisma/client';
import { AxiosError, AxiosRequestConfig } from 'axios';
import { firstValueFrom } from 'rxjs';
import * as https from 'node:https';
import {
  PaymentProvider,
  ProviderInitiationInput,
  ProviderInitiationResult,
  ProviderStatus,
} from '../payments.types';
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 = PaymentMethodType.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,
            prepayId,
            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,
      },
    );

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

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

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

  mapWebhookTradeStatus(tradeStatus: string | undefined): PaymentIntentStatus {
    switch (tradeStatus) {
      case 'Completed':
        return PaymentIntentStatus.SUCCEEDED;
      case 'Failure':
      case 'Expired':
        return PaymentIntentStatus.FAILED;
      case 'Paying':
      case 'Pending':
        return PaymentIntentStatus.PROCESSING;
      default:
        return PaymentIntentStatus.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 / 100);
    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 ${input.bookingRef}`,
        total_amount: totalAmount,
        trans_currency: input.currency,
        timeout_express: this.timeoutExpress,
      },
    };
    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 this.config.get<string>('telebirr.privateKey') ?? ''; }
  private get publicKey(): string { return this.config.get<string>('telebirr.publicKey') ?? ''; }
}
