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 { CacBankAuth } from "./cac-bank.auth";
import type {
  CacConfirmResult,
  CacGetPaymentByReferenceRequest,
  CacPaymentByReferenceResponse,
  CacPaymentConfirmRequest,
  CacPaymentConfirmResponse,
  CacPaymentInitiateRequest,
  CacPaymentInitiateResponse,
} from "./cac-bank.types";

@Injectable()
export class CacBankProvider implements PaymentProvider {
  readonly method = ProviderMethod.CAC_BANK;
  private readonly logger = new Logger(CacBankProvider.name);
  private auth: CacBankAuth | null = null;

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

  async initiate(
    input: ProviderInitiationInput,
  ): Promise<ProviderInitiationResult> {
    if (!input.payerAccount) {
      throw new Error("CAC Bank requires payerAccount (customer mobile number)");
    }

    const requestBody: CacPaymentInitiateRequest = {
      app_key: this.appKey,
      api_key: this.apiKey,
      customer_mobile: input.payerAccount,
      currency: input.currency || this.defaultCurrency,
      desc: `EDR ${input.orderRef}`.slice(0, 500),
      vender_ref: input.merchantOrderId,
      amount: this.toMajorAmount(input.amountMinor, input.currency),
      company_services_id: this.companyServicesId,
    };

    const response = await this.postJson<CacPaymentInitiateResponse>(
      "/paymentapi/PaymentInitiateRequest",
      requestBody,
    );

    if (response.paymentRequestId == null) {
      throw new Error(
        `CAC Bank initiate failed: ${JSON.stringify(response)}`,
      );
    }

    const providerOrderId = String(response.paymentRequestId);
    const expiresAt = new Date(Date.now() + this.otpExpiryMs);

    return {
      providerOrderId,
      clientAction: {
        type: "COLLECT_OTP",
        providerOrderId,
        message: "Enter the OTP sent to your phone",
      },
      expiresAt,
      rawInitiation: {
        request: this.sanitizeKeys(requestBody),
        response,
        venderRef: input.merchantOrderId,
      },
    };
  }

  async confirmPayment(
    paymentRequestId: string,
    otp: string,
  ): Promise<CacConfirmResult> {
    const requestBody: CacPaymentConfirmRequest = {
      app_key: this.appKey,
      api_key: this.apiKey,
      payment_request_id: Number(paymentRequestId),
      otp,
    };

    try {
      const response = await this.postJson<CacPaymentConfirmResponse>(
        "/paymentapi/PaymentConfirmationRequest",
        requestBody,
      );

      if (response.confirmReference == null && !response.reference) {
        return {
          status: "FAILED",
          failureCode: "CONFIRM_REJECTED",
          failureMessage: response.description ?? "Confirmation rejected",
          rawResponse: response as unknown as Record<string, unknown>,
        };
      }

      return {
        status: "SUCCEEDED",
        providerTxnId: String(
          response.confirmReference ?? response.reference,
        ),
        reference: response.reference,
        rawResponse: response as unknown as Record<string, unknown>,
      };
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return {
        status: "FAILED",
        failureCode: "CONFIRM_ERROR",
        failureMessage: message,
        rawResponse: {},
      };
    }
  }

  async queryStatus(
    merchantOrderId: string,
    reference?: string,
  ): Promise<ProviderStatus> {
    const lookupRef = reference ?? merchantOrderId;
    const requestBody: CacGetPaymentByReferenceRequest = {
      app_key: this.appKey,
      api_key: this.apiKey,
      reference: lookupRef,
    };

    try {
      const response = await this.postJson<CacPaymentByReferenceResponse>(
        "/paymentapi/GetPaymentByReferenceRequest",
        requestBody,
      );

      if (response.transactionNo != null && response.transactionDate) {
        return {
          status: ProviderPaymentStatus.SUCCEEDED,
          providerTxnId: String(response.transactionNo),
          rawResponse: response as unknown as Record<string, unknown>,
        };
      }

      return {
        status: ProviderPaymentStatus.PROCESSING,
        rawResponse: response as unknown as Record<string, unknown>,
      };
    } catch (err) {
      if (err instanceof AxiosError && err.response?.status === 404) {
        return {
          status: ProviderPaymentStatus.PROCESSING,
          rawResponse: { notFound: true, reference: lookupRef },
        };
      }
      throw err;
    }
  }

  private async postJson<T>(path: string, body: unknown): Promise<T> {
    const token = await this.getAuth().getAccessToken();
    const url = `${this.baseUrl}${path}`;
    const config: AxiosRequestConfig = {
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
      timeout: 10_000,
    };

    const started = Date.now();
    try {
      const res = await firstValueFrom(this.http.post<T>(url, body, config));
      this.logger.debug(
        `CAC Bank POST ${path} status=${res.status} latency=${Date.now() - started}ms`,
      );
      return res.data;
    } catch (err) {
      if (err instanceof AxiosError && err.response?.status === 401) {
        this.getAuth().invalidate();
        const retryToken = await this.getAuth().getAccessToken();
        const retryConfig: AxiosRequestConfig = {
          ...config,
          headers: {
            ...config.headers,
            Authorization: `Bearer ${retryToken}`,
          },
        };
        const res = await firstValueFrom(
          this.http.post<T>(url, body, retryConfig),
        );
        return res.data;
      }

      if (err instanceof AxiosError) {
        this.logger.error(
          `CAC Bank POST ${path} failed: status=${err.response?.status} body=${JSON.stringify(err.response?.data)}`,
        );
      } else {
        this.logger.error(
          `CAC Bank POST ${path} threw: ${err instanceof Error ? err.message : err}`,
        );
      }
      throw err;
    }
  }

  private getAuth(): CacBankAuth {
    if (!this.auth) {
      this.auth = new CacBankAuth(this.http, {
        baseUrl: this.baseUrl,
        username: this.username,
        password: this.password,
        tokenTtlMs: this.tokenTtlMs,
      });
    }
    return this.auth;
  }

  /** DJF has no fractional units — amountMinor is the major amount. */
  private toMajorAmount(amountMinor: number, currency: string): number {
    if (currency.toUpperCase() === "DJF") {
      return amountMinor;
    }
    return amountMinor / 100;
  }

  private sanitizeKeys(
    body: CacPaymentInitiateRequest | CacPaymentConfirmRequest,
  ): Record<string, unknown> {
    const { app_key: _appKey, api_key: _apiKey, ...rest } = body;
    return rest;
  }

  private get baseUrl(): string {
    return (this.config.get<string>("cac.baseUrl") ?? "").replace(/\/$/, "");
  }
  private get username(): string {
    return this.config.get<string>("cac.username") ?? "";
  }
  private get password(): string {
    return this.config.get<string>("cac.password") ?? "";
  }
  private get appKey(): string {
    return this.config.get<string>("cac.appKey") ?? "";
  }
  private get apiKey(): string {
    return this.config.get<string>("cac.apiKey") ?? "";
  }
  private get companyServicesId(): number {
    return this.config.get<number>("cac.companyServicesId") ?? 0;
  }
  private get defaultCurrency(): string {
    return this.config.get<string>("cac.currency") ?? "DJF";
  }
  private get tokenTtlMs(): number {
    return this.config.get<number>("cac.tokenTtlMs") ?? 23 * 60 * 60 * 1000;
  }
  private get otpExpiryMs(): number {
    return this.config.get<number>("cac.otpExpiryMs") ?? 10 * 60 * 1000;
  }
}
