import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { Provider } from '../../client/invoice-manager';
import {
  CreateInvoiceData,
  Invoice,
  InvoiceID,
  InvoiceStats,
  DataContainer,
  InvoiceUpdate,
} from '../../models/invoice';
import {
  CancelInvoiceRequestData,
  CancelInvoiceResponseData,
  CreateInvoiceRequestData,
  CreateInvoiceResponseData,
  GetInvoiceInfoResponseData,
  GetInvoiceStatsResponseData,
  InvoiceResponse,
} from './types';
import axiosRetry from 'axios-retry';
import { createHmac } from 'crypto';

type Settings =
  | {
      url: string;
      apiKey: string;
      merchantId: number;
    }
  | { client: AxiosInstance };

/** HttpProvider provides methods to communicate with Merchant API via HTTP. */
export class HttpProvider implements Provider {
  private readonly _apiKey: string;
  private readonly _client: axios.AxiosInstance;

  constructor(settings: Settings) {
    // if client is provided then use it
    if ('client' in settings) {
      this._client = settings.client;

      // Accessing the Authorization header
      const headers = settings.client.defaults.headers;
      this._apiKey = String(headers['Authorization']);
      if (!this._apiKey) {
        console.error('Authorization header not found');
      }

      return;
    }

    this._apiKey = settings.apiKey;
    this._client = axios.create({
      baseURL: new URL(`/m/${settings.merchantId}/`, settings.url).href,
      headers: {
        Authorization: settings.apiKey,
      },
    });

    axiosRetry(axios, {
      retries: 3,
    });
  }

  async createInvoice(
    data: CreateInvoiceData
  ): Promise<DataContainer<Invoice>> {
    let resp: axios.AxiosResponse<CreateInvoiceResponseData>;

    try {
      resp = await this._sendWithAuth<CreateInvoiceResponseData>(
        'POST',
        `/invoice`,
        data
      );
    } catch (e) {
      return this._invalidData(e);
    }

    if (!this._isSuccess(resp)) {
      return this._invalidData();
    }

    return {
      data: this._parseInvoiceResponse(resp.data.data),
      isValid: true,
    };
  }

  async cancelInvoice(invoiceId: InvoiceID): Promise<DataContainer<Invoice>> {
    let resp: axios.AxiosResponse<CancelInvoiceResponseData>;
    try {
      resp = await this._sendWithAuth<CancelInvoiceResponseData>(
        'PATCH',
        `/invoice/cancel`,
        {
          invoiceId,
        }
      );
    } catch (e) {
      return this._invalidData(e);
    }

    if (!this._isSuccess(resp)) {
      return this._invalidData();
    }

    return {
      data: this._parseInvoiceResponse(resp.data.data),
      isValid: true,
    };
  }

  async getInvoiceInfo(invoiceId: InvoiceID): Promise<DataContainer<Invoice>> {
    let resp: axios.AxiosResponse<GetInvoiceInfoResponseData>;
    try {
      resp = await this._sendWithAuth<GetInvoiceInfoResponseData>(
        'GET',
        `/invoice/info?id=${invoiceId}`
      );
    } catch (e) {
      return this._invalidData(e);
    }

    if (!this._isSuccess(resp)) {
      return this._invalidData();
    }

    return {
      data: this._parseInvoiceResponse(resp.data.data),
      isValid: true,
    };
  }

  async getInvoiceStats(): Promise<DataContainer<InvoiceStats>> {
    let resp: axios.AxiosResponse<GetInvoiceStatsResponseData>;
    try {
      resp = await this._sendWithAuth<GetInvoiceStatsResponseData>(
        'GET',
        `/invoice/stats`
      );
    } catch (e) {
      return this._invalidData(e);
    }

    if (!this._isSuccess(resp)) {
      return this._invalidData();
    }

    return {
      data: {
        ...resp.data.data,
      },
      isValid: true,
    };
  }

  isUpdateValid(update: InvoiceUpdate): boolean {
    const { data } = update;
    const { sign, ...others } = data;
    const response = Object.entries(others)
      .sort()
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      .map(([_key, value]) => value)
      .join(';');
    const hmac = createHmac('sha256', this._apiKey);
    hmac.update(response);
    const signReceived = hmac.digest('hex');
    return sign === signReceived;
  }

  private _sendWithAuth<Res>(
    method: axios.Method,
    path: string,
    data?: CreateInvoiceRequestData | CancelInvoiceRequestData | null
  ): Promise<axios.AxiosResponse<Res>> {
    switch (method) {
      case 'POST':
        return this._client.post<Res>(path, data);
      case 'GET':
        return this._client.get<Res>(path);
      case 'PATCH':
        return this._client.patch<Res>(path, data);
    }
    throw new Error(`unsupported method: ${method}`);
  }

  private _invalidData<T>(e?: unknown): DataContainer<T> {
    return {
      data: null,
      isValid: false,
      error: e,
    };
  }

  private _parseInvoiceResponse(invoiceResponse: InvoiceResponse): Invoice {
    return {
      ...invoiceResponse,
      id: Number(invoiceResponse.id),
      amount: BigInt(invoiceResponse.amount),
      order_id: BigInt(invoiceResponse.order_id),
      createdAt: invoiceResponse.createdAt
        ? Date.parse(invoiceResponse.createdAt)
        : undefined,
      updatedAt: invoiceResponse.updatedAt
        ? Date.parse(invoiceResponse.updatedAt)
        : undefined,
      user_from_id: invoiceResponse.user_from_id
        ? BigInt(invoiceResponse.user_from_id)
        : undefined,
      user_to_id: invoiceResponse.user_to_id
        ? BigInt(invoiceResponse.user_to_id)
        : undefined,
      wallet_from_id: invoiceResponse.wallet_from_id
        ? Number(invoiceResponse.wallet_from_id)
        : undefined,
      wallet_to_id: invoiceResponse.wallet_to_id
        ? Number(invoiceResponse.wallet_to_id)
        : undefined,
    };
  }

  private _isSuccess(res: AxiosResponse): boolean {
    return Math.floor(res.status / 100) === 2 && !!res.data;
  }
}
