import {
  CONTINGENCY,
  NOTIFICATION_TYPE,
  ORDER_TYPE,
  SUBSCRIPTION_MODE,
  TIME_IN_FORCE,
} from "../constants";
import { Balance, Commission, OrderRequest, Report } from "../models";
import { fromSnakeCaseToCamelCase } from "../paramStyleConverter";
import { AuthClient } from "./authClient";

const reportsKey = "reports";
const balanceKey = "balance";
/**
 * TradingClient connects via websocket to cryptomarket to enable the user to manage orders. uses SHA256 as auth method and authenticates on connection.
 */
export class TradingClient extends AuthClient {
  /**
   * Creates a new spot trading websocket client. It connects and authenticates with the client with the method  {@link connect()}.
   * @param apiKey public API key
   * @param apiSecret secret API key
   * @param window Maximum difference between the send of the request and the moment of request processing in milliseconds.
   * @param requestTimeoutMs Timeout time for requests to the server. No timeout by default
   */
  constructor(apiKey: string, apiSecret: string, window?: number, requestTimeoutMs?: number) {
    super(
      "wss://api.exchange.cryptomkt.com/api/3/ws/trading",
      apiKey,
      apiSecret,
      window,
      requestTimeoutMs,
      {
        // reports
        spot_subscribe: { key: reportsKey, type: NOTIFICATION_TYPE.COMMAND },
        spot_unsubscribe: { key: reportsKey, type: NOTIFICATION_TYPE.COMMAND },
        spot_order: { key: reportsKey, type: NOTIFICATION_TYPE.UPDATE },
        spot_orders: { key: reportsKey, type: NOTIFICATION_TYPE.SNAPSHOT },
        spot_balance_subscribe: { key: balanceKey, type: NOTIFICATION_TYPE.COMMAND },
        spot_balance_unsubscribe: { key: balanceKey, type: NOTIFICATION_TYPE.COMMAND },
        spot_balance: { key: balanceKey, type: NOTIFICATION_TYPE.SNAPSHOT },
      }
    );
  }

  /**
   * Get all active spot orders
   *
   * Orders without executions are deleted after 24 hours
   *
   * https://api.exchange.cryptomkt.com/#get-active-spot-orders
   *
   * @return A promise that resolves with all the spot orders
   *
   */
  async getActiveSpotOrders(): Promise<Report[]> {
    const reports = await this.makeRequest<Report[]>({ method: "spot_get_orders" });
    return fromSnakeCaseToCamelCase(reports)
  }

  /**
   * Creates a new spot order
   *
   * For fee, for price accuracy and quantity, and for order status information see the api docs at
   * https://api.exchange.cryptomkt.com/#create-new-spot-order
   *
   * https://api.exchange.cryptomkt.com/#place-new-spot-order
   *
   * @param {string} params.symbol Trading symbol
   * @param {string} params.side Either 'buy' or 'sell'
   * @param {string} params.quantity Order quantity
   * @param {string} [params.clientOrderId] Optional. If given must be unique within the trading day, including all active orders. If not given, is generated by the server
   * @param {ORDER_TYPE} [params.type] Optional. 'limit', 'market', 'stopLimit', 'stopMarket', 'takeProfitLimit' or 'takeProfitMarket'. Default is 'limit'
   * @param {string} [params.price] Optional. Required for 'limit' and 'stopLimit'. limit price of the order
   * @param {string} [params.stopPrice] Optional. Required for 'stopLimit' and 'stopMarket' orders. stop price of the order
   * @param {TIME_IN_FORCE} [params.timeInForce] Optional. 'GTC', 'IOC', 'FOK', 'Day', 'GTD'. Default to 'GTC'
   * @param {string} [params.expireTime] Optional. Required for orders with timeInForce = GDT
   * @param {boolean} [params.strictValidate] Optional. If False, the server rounds half down for tickerSize and quantityIncrement. Example of ETHBTC: tickSize = '0.000001', then price '0.046016' is valid, '0.0460165' is invalid
   * @param {boolean} [params.postOnly] Optional. If True, your postOnly order causes a match with a pre-existing order as a taker, then the order will be cancelled
   * @param {string} [params.takeRate] Optional. Liquidity taker fee, a fraction of order volume, such as 0.001 (for 0.1% fee). Can only increase the fee. Used for fee markup.
   * @param {string} [params.makeRate] Optional. Liquidity provider fee, a fraction of order volume, such as 0.001 (for 0.1% fee). Can only increase the fee. Used for fee markup.
   * @return A promise that resolves with a report of the new order
   */
  async createSpotOrder(params: {
    symbol: string;
    side: string;
    quantity: string;
    clientOrderId?: string;
    type?: ORDER_TYPE;
    price?: string;
    stopPrice?: string;
    timeInForce?: TIME_IN_FORCE;
    expireTime?: string;
    strictValidate?: boolean;
    postOnly?: boolean;
    takeRate?: string;
    makeRate?: string;
  }): Promise<Report> {
    const report = await this.makeRequest<Report>({
      method: "spot_new_order",
      params,
    });
    return fromSnakeCaseToCamelCase(report)
  }

  /**
   * creates a list of spot orders
   *
   * Types or contingency:
   *
   * - CONTINGENCY.ALL_OR_NONE (CONTINGENCY.AON)
   * - CONTINGENCY.ONE_CANCEL_OTHER (CONTINGENCY.OCO)
   * - CONTINGENCY.ONE_TRIGGER_ONE_CANCEL_OTHER (CONTINGENCY.OTOCO)
   *
   * Restriction in the number of orders:
   *
   * - An AON list must have 2 or 3 orders
   * - An OCO list must have 2 or 3 orders
   * - An OTOCO must have 3 or 4 orders
   *
   * Symbol restrictions:
   *
   * - For an AON order list, the symbol code of orders must be unique for each order in the list.
   * - For an OCO order list, there are no symbol code restrictions.
   * - For an OTOCO order list, the symbol code of orders must be the same for all orders in the list (placing orders in different order books is not supported).
   *
   * ORDER_TYPE restrictions:
   * - For an AON order list, orders must be ORDER_TYPE.LIMIT or ORDER_TYPE.Market
   * - For an OCO order list, orders must be ORDER_TYPE.LIMIT, ORDER_TYPE.STOP_LIMIT, ORDER_TYPE.STOP_MARKET, ORDER_TYPE.TAKE_PROFIT_LIMIT or ORDER_TYPE.TAKE_PROFIT_MARKET.
   * - An OCO order list cannot include more than one limit order (the same
   * applies to secondary orders in an OTOCO order list).
   * - For an OTOCO order list, the first order must be ORDER_TYPE.LIMIT, ORDER_TYPE.MARKET, ORDER_TYPE.STOP_LIMIT, ORDER_TYPE.STOP_MARKET, ORDER_TYPE.TAKE_PROFIT_LIMIT or ORDER_TYPE.TAKE_PROFIT_MARKET.
   * - For an OTOCO order list, the secondary orders have the same restrictions as an OCO order
   * - Default is ORDER_TYPE.Limit
   *
   * https://api.exchange.cryptomkt.com/#create-new-spot-order-list-2
   *
   * @param {string} params.orderListId Order list identifier. If omitted, it will be generated by the system upon order list creation. Must be equal to clientOrderId of the first order in the request.
   * @param {string} params.contingencyType Order list type.
   * @param {OrderRequest[]} params.orders Orders in the list.
   * @return A promise that resolves with a list all reports created
   */
  async createNewSpotOrderList(params: {
    orderListId: string;
    contingencyType: CONTINGENCY;
    orders: OrderRequest[];
  }): Promise<Report[]> {
    const reportList = await this.makeListRequest<Report>({
      method: "spot_new_order_list",
      params,
      responseCount: params.orders.length,
    });
    return fromSnakeCaseToCamelCase(reportList)
  }

  /**
   * cancels a spot order
   *
   * https://api.exchange.cryptomkt.com/#cancel-spot-order-2
   *
   * @param {string} clientOrderId the client order id of the order to cancel
   * @return A promise that resolves with a report of the canceled order
   */
  async cancelSpotOrder(clientOrderId: string): Promise<Report> {
    const report = await this.makeRequest<Report>({
      method: "spot_cancel_order",
      params: { clientOrderId },
    });
    return fromSnakeCaseToCamelCase(report)
  }

  /**
   * changes the parameters of an existing order, quantity or price
   *
   * https://api.exchange.cryptomkt.com/#cancel-replace-spot-order
   *
   * @param {string} params.clientOrderId the client order id of the order to change
   * @param {string} params.newClientOrderId the new client order id for the modified order. must be unique within the trading day
   * @param {string} params.quantity new order quantity
   * @param {string} params.price new order price
   * @param {string} [params.stopPrice]  Required if order type is stopLimit, stopMarket, takeProfitLimit, or takeProfitMarket. Order price
   * @param {boolean} [params.strictValidate]  price and quantity will be checked for the incrementation with tick size and quantity step. See symbol's tickSize and quantityIncrement
   * @return A promise that resolves with a report of the modified order
   */
  async replaceSpotOrder(params: {
    clientOrderId: string;
    newClientOrderId: string;
    quantity: string;
    price: string;
    stop_price?: string;
    strictValidate?: Boolean;
  }): Promise<Report> {
    const report = await this.makeRequest<Report>({
      method: "spot_replace_order",
      params,
    });
    return fromSnakeCaseToCamelCase(report)
  }

  /**
   * cancel all active spot orders and returns the ones that could not be canceled
   *
   * https://api.exchange.cryptomkt.com/#cancel-spot-orders
   *
   * @return A promise that resolves with a list of report of the canceled orders
   */
  async cancelSpotOrders(): Promise<Report[]> {
    const reports = await this.makeRequest<Report[]>({ method: "spot_cancel_orders" });
    return fromSnakeCaseToCamelCase(reports)
  }

  /**
   * Get the user's spot trading balance for all currencies with balance
   *
   * https://api.exchange.cryptomkt.com/#get-spot-trading-balances
   *
   * @return A promise that resolves with a list of spot trading balances
   */
  async getSpotTradingBalances(): Promise<Balance[]> {
    return this.makeRequest<Balance[]>({ method: "spot_balances" });
  }

  /**
   * Get the user spot trading balance of a currency
   *
   * https://api.exchange.cryptomkt.com/#get-spot-trading-balance-2
   *
   * @param {string} currency The currency code to query the balance
   * @return A promise that resolves with the spot trading balance of a currency
   */
  async getSpotTradingBalance(currency: string): Promise<Balance> {
    return this.makeRequest<Balance>({ method: "spot_balance", params: { currency } });
  }

  /**
   * alias of {@link getSpotTradingBalance}
   */
  getSpotTradingBalanceOfCurrency = this.getSpotTradingBalance;

  /**
   * Get the personal trading fee rates for all symbols
   *
   * https://api.exchange.cryptomkt.com/#get-spot-fees
   *
   * @return A promise that resolves with a list of commission rates
   */
  async getSpotFees(): Promise<Commission[]> {
    const commissions = await this.makeRequest<Commission[]>({ method: "spot_fees" });
    return fromSnakeCaseToCamelCase(commissions)
  }

  /**
   * Get the personal trading fee rate of a symbol
   *
   * https://api.exchange.cryptomkt.com/#get-spot-fee
   *
   * @param {string} symbol The symbol of the commission rate
   * @return A promise that resolves with the commission rate of a symbol
   */
  async getSpotFee(symbol: string): Promise<Commission> {
    const commission = await this.makeRequest<Commission>({ method: "spot_fee", params: { symbol } });
    return fromSnakeCaseToCamelCase(commission)
  }

  /**
   * alias of {@link getSpotFee}
   */
  getSpotFeeOfSymbol = this.getSpotFee;
  /**
   * alias of {@link getSpotFee}
   */
  getSpotFeeBySymbol = this.getSpotFee;

  ///////////////////
  // subscriptions //
  ///////////////////

  /**
   * subscribe to a feed of execution reports of the user's orders
   *
   * the first notification is a snapshot of the current orders, further
   * notifications are updates of the user orders
   *
   * https://api.exchange.cryptomkt.com/#subscribe-to-reports
   *
   * @param {function} callback a function that recieves a list of reports, and the type of notification (either SNAPSHOT or UPDATE)
   * @return {Promise<Boolean>} A Promise of the subscription result. True if subscribed
   */
  async subscribeToReports(
    callback: (notification: Report[], type: NOTIFICATION_TYPE) => any
  ): Promise<Boolean> {
    return (
      (await this.sendSubscription({
        method: "spot_subscribe",
        callback: (notificationRaw: any, type: NOTIFICATION_TYPE) => {
          const notification = fromSnakeCaseToCamelCase(notificationRaw)
          if (type === NOTIFICATION_TYPE.SNAPSHOT) {
            callback(notification as Report[], type);
          } else {
            callback([notification as Report], type);
          }
        },
      })) as {
        result: boolean;
      }
    ).result;
  }

  /**
   * stop recieveing the report feed subscription
   *
   * https://api.exchange.cryptomkt.com/#subscribe-to-reports
   *
   * @return {Promise<Boolean>} A Promise of the unsubscription result. True if unsubscribed
   */
  async unsubscribeToReports(): Promise<Boolean> {
    return this.sendUnsubscription({ method: "spot_unsubscribe" });
  }

  /**
   * subscribe to a feed of the user's spot balances
   * 
   * only non-zero values are present
   * 
   * https://api.exchange.cryptomkt.com/#subscribe-to-spot-balances
   * 
   * @param {function} callback A function that recieves notifications with a list of balances
   * @param {string} mode Either 'updates' or 'batches'. Update messages arrive after an update. Batch messages arrive at equal intervals after a first update
   * @return {Promise<Boolean>} A Promise of the subscription result. True if subscribed
   */
  async subscribeToSpotBalance(
    callback: (notification: Balance[]) => any,
    mode: SUBSCRIPTION_MODE,
  ): Promise<Boolean> {
    return (
      (await this.sendSubscription({
        method: "spot_balance_subscribe",
        callback: (notification: any, type) => {
          callback(notification as Balance[]);
        },
        params: { mode },
      })) as {
        result: boolean;
      }
    ).result;
  }


  /**
   * stop recieveing the feed of balances
   * 
   * https://api.exchange.cryptomkt.com/#subscribe-to-spot-balances
   *
   * @return {Promise<Boolean>} A Promise of the unsubscription result. True if unsubscribed
   */
  unsubscribeToSpotBalance(): Promise<Boolean> {
    return this.sendUnsubscription({
      method: "spot_balance_unsubscribe",
      params: {
        mode: SUBSCRIPTION_MODE.UPDATES
      }
    });
  }
}
