"use strict"

class Bitfinex extends Exchange {
	static meta = {
		aliases: [
			"BITFINEX",
		],
		endpoint: "https://api.bitfinex.com",
		fields: {
			public: {
				label: "Key ID",
			},
			private: {
				label: "Secret",
			},
		},
		name: "Bitfinex",
		permissions: {
			origins: [
				"https://api.bitfinex.com/*"
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"bitfi1",
				"bitfi2",
			],
		},
		version: "v1",
		website: "https://www.bitfinex.com/?refcode=iU8fp-wd",
		desc: "Bitfinex is the longest-running and most liquid major cryptocurrency exchange.",
		margin: true,
		spot: true,
		testSymbol: "BTCUSD",
		defaultSymbol: "USD",
		multiAsset: "USDM",
	}
	constructor(meta = Bitfinex.meta) {
		super(meta)
	}

	async req(method, resource, params = {}) {
		resource = "/" + this.meta.version + resource

		const headers = {"Content-Type": "application/json;charset=utf-8"}
		if (method !== 'GET') {
			const credentials = this.getCredentials("private")
			params.request = resource
			params.nonce = this.getNonce().toString()
			params = JSON.stringify(params)
			const payload = btoa(params)
			headers["X-BFX-APIKEY"] = credentials.public
			headers["X-BFX-PAYLOAD"] = payload
			headers["X-BFX-SIGNATURE"] = CryptoJS.HmacSHA384(payload, credentials.private).toString(CryptoJS.enc.Hex)
		} else this.hasAccess()

		return await request.call(this, method, this.meta.endpoint + resource, params, headers)
	}

	async account(currency, isMargin) {
		let balances = {}
		const isTest = this.isTest()
		const response = await this.req('POST', "/balances")
		response?.length && response.forEach(balance => {
			const item = balance.currency && balance.currency.toUpperCase().replace('TEST','')
			const amount = Number(balance.amount)
			const suffix = !isMargin && balance.type === "trading" ? marginSuffix : ''

			if ((!isMargin || balance.type === "trading") && (amount || item === currency)) {
				balances[item + suffix] = {
					available: Number(balance.available),
					balance: amount
				}
			}
		})
		if (isMargin) {
			const info = await this.req('POST', "/margin_infos")
			if (info && info[0]) {
				const available = info[0].tradable_balance / info[0].leverage
				const balance = Number(info[0].margin_balance)
				balances[this.meta.multiAsset] = {available, balance}
			}
		}
		isTest && currency && (balances[currency] = balances[currency] || {
			available: 0,
			balance: 0
		})
		return balances
	}

	async symbolInfoImpl(cache) {
		const response = await this.req('GET', "/symbols_details")
		response?.forEach(info => {
			info.symbol = info.pair.toUpperCase()
			let pair = info.symbol.split(':')
			if (pair.length < 2) {
				pair[1] = pair[0].slice(-3)
				pair[0] = pair[0].slice(0, -3)
			}
			info.base = pair[0].replace('TEST','')
			info.quote = pair[1].replace('TEST','')
			info.symbol = pair[0]+pair[1]
			info.leverage = 100 / info.initial_margin
			info.future = info.margin && info.leverage > 10
			info.factor = info.future ? 1 : 1 - 1 / (info.leverage * info.leverage)
			info.type = info.future ? "Futures" : info.margin ? "Margin" : "Spot"
			info.minimum_order_size = Number(info.minimum_order_size)
			info.maximum_order_size = Number(info.maximum_order_size)
			info.quantity_precision = info.quantity_precision || decimals(info.minimum_order_size)
			cache[this.sym(info.symbol)] = info
		})
	}

	async symbolTickerImpl(pair) {
		const ticker = await this.req('GET', "/pubticker/"+pair)
		return ticker && {
			ask: Number(ticker.ask),
			bid: Number(ticker.bid),
			mid: Number(ticker.mid),
			last: Number(ticker.last_price),
		}
	}

	static fixPrecision(value, precision = 5) {
		value = Number(value).toString()
		let decimals = value.replace('.', '').replace(/0/g, ' ').trim().length
		if (decimals > precision) {
			value = value.slice(0, precision - decimals)
			if (value.slice(-1) === '.') {
				value = value.slice(0, -1)
			}
		}
		return value
	}

	async convert(available, market, price) {
		try {
			if (["EUR", "GBP", "JPY"].includes(market.quote)) {
				const toBTC = await this.symbolTicker(false, "btcusd")
				available /= toBTC.mid

				if (market.base !== "BTC") {
					const toQuote = await this.symbolTicker(false, "btc"+market.quote.toLowerCase())
					available *= toQuote.mid
					available /= price
				}
			} else {
				if (market.quote !== "USD" && market.quote !== "USDT") {
					const toQuoteInv = await this.symbolTicker(false, market.quote.toLowerCase()+"usd")
					available /= toQuoteInv.mid
				}
				available /= price
			}
		} catch(ex) {
			this.warn(`couldn't convert balance from USD to ${market.quote} – please use absolute quantity!`)
		}
		return available
	}

	async trade(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.pair
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.pair)) || (await this.symbolTicker(cmd.up, market.pair))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.symbol} is not available!`)
		}
		this.checkSlip(cmd, ohlc, market.pair, ticker)
		if (cmd.up) {
			this.updatePrice(market.pair, ticker)
			if (!cmd.b && !cmd.ub) {
				return ticker
			}
		}
		if (cmd.isMargin && !market.margin) {
			throw new ReferenceError(`Market ${market.symbol} does not support margin trading!`)
		}

		const currency = cmd.currency = cmd.isMargin && !market.future ? this.meta.multiAsset : cmd.isBid || market.future ? market.quote : market.base
		let balance = {available: 0, balance: 0}
		if ((cmd.y !== "possize" && cmd.q.wasPercent()) || cmd.ub) {
			balance = cmd.bc && !cmd.ub && this.getBalance(currency, cmd.isMargin)
			const balances = !balance && (await this.account(currency, cmd.isMargin))
			balance = balance || balances[currency]
			this.checkBalance(cmd, balance, currency)
			if (!cmd.bc || cmd.ub) {
				this.updateBalance(balances, currency, cmd.isMargin)
				if (cmd.ub) {
					return Object.assign(ticker, balance)
				}
			}
		}

		const first = ticker[cmd.pr] || this.ohlc(cmd, ohlc, 'pr') || ((cmd.isBid && cmd.t !== "market") || (cmd.isAsk && cmd.t === "market") ? ticker.bid : ticker.ask)
		let price = Bitfinex.fixPrecision(cmd.fp ? cmd.fp.resolve(8) : cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(8), market.price_precision)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated)")
		}

		let cf = 1.0
		let leverage = cmd._lev = cmd.l * (this.ohlc(cmd, ohlc, 'lr') || 1) || (market.future ? 10 : market.leverage)
		if (leverage > market.leverage) {
			cmd.l && (cf = market.leverage / leverage)
			leverage = cmd._lev = market.leverage
			this.warn(`leverage limited to ${leverage}x by instrument!`)
		}

		let available = ohlc[cmd.y] || cmd.y === "equity" ? balance.balance : balance.available
		if (cmd.y !== "possize") {
			if (cmd.isMargin) {
				available *= cmd.lc || leverage
				available = market.future ? available / price : (await this.convert(available * market.factor, market, price))
			} else if (cmd.isBid) {
				available /= price
			}
		}

		const hasStop = cmd.sl || cmd.so || cmd.tp
		const isOCO = (cmd.sl || cmd.so) && cmd.tp
		let order = {
			symbol: market.pair,
			side: cmd.isBid ? "buy" : "sell",
			amount: cmd.q._().div(cmd.u === "currency" && !cmd.q.wasPercent() ? price : 1)
				.reference(available).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc)).minmax(cmd.minq, cmd.maxq).resolve(8).toString(),
			price: price,
			type: (cmd.isMargin ? "" : "exchange ") + (cmd.ts ? "trailing-stop" : hasStop && !isOCO ? (cmd.t !== "market" ? "stop limit" : "stop") : ({fok: "fill-or-kill", ioc: "immediate-or-cancel", market: "market"})[cmd.t] || "limit"),
			aff_code: this.prefixID("iU8fp-wd"),
			exchange: "bitfinex"
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.amount = ohlc.left.toString()
		}

		if (market.minimum_order_size && order.amount < market.minimum_order_size) {
			this.warn(`order quantity below instrument minimum of ${market.minimum_order_size} – use minq= to set minimum!`)
		} else if (market.maximum_order_size && order.amount > market.maximum_order_size) {
			this.warn(`order quantity above instrument maximum of ${market.maximum_order_size} – use maxq= to set maximum!`)
		} else if (order.amount <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated)")
		}
		if (cmd.t === "post") order.is_postonly = true
		if (cmd.h) order.is_hidden = true
		if (cmd.l) {
			if (market.future) {
				order.lev = leverage.toString()
				!cmd.d && this.setBalanceRaw(market.symbol, leverage)
			} else {
				this.warn('this market does not support setting custom leverage!')
			}
		}
		if (cmd.ts) {
			// cmd.t !== "market" && cmd.has('p') => warn!
			order.price = Bitfinex.fixPrecision(cmd.ts._().reference(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || first).abs().resolve(8), market.price_precision)
		} else if (hasStop) {
			let sl = null, tp = null
			if (cmd.sl || cmd.so) {
				sl = Bitfinex.fixPrecision((cmd.sl || cmd.so)._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(8), market.price_precision)
			}
			if (cmd.tp) {
				tp = Bitfinex.fixPrecision(cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(8), market.price_precision)
			}
			if (isOCO) {
				order.ocoorder = true
				// cmd.t !== "market" && cmd.has('p') => warn!
				order[order.side+"_price_oco"] = sl
				order.price = tp
			} else {
				if (cmd.t !== "market") order[order.side+"_stoplimit_price"] = order.price
				order.price = sl || tp
			}
		}

		this.info(`placing ${currency} ${cmd.isMargin?market.type:'Spot'} order${cmd.isMargin?` @ ${toPrecision(leverage,2)}x ${!market.future?` (${market.factor*100}%)`:''} margin`:''}: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		this.checkOrder(order, 184859057, 8)
		const result = await this.req('POST', "/order/new", order)
		cmd.id && result?.id && this.cacheId(result.id, cmd.id)
		return result
	}

	async ordersCancel(cmd, ohlc) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.pair
		let orders = await this.req('POST', "/orders")
		if (!orders || !orders.filter) {
			this.msgOrderErr()
			return false
		}
		orders = orders.filter(order => !order.is_cancelled && order.symbol === market.pair)
		if (!orders.length) {
			this.msgOrdersNone()
			return false
		}

		const ids = (cmd.id || "").split(',')
		const match = cmd.cr ? false : true
		const tickers = {}
		await this.filterOrderStart(cmd, ohlc, tickers, orders)
		const totalOrders = orders.length
		orders = orders.filter(order => {
			if ((cmd.isBid && order.side !== "buy") || (cmd.isAsk && order.side !== "sell")) {
				return false
			}
			if (cmd.isMargin && order.type.includes("exchange")) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if (cmd.t === "market" && order.type !== "market") {
				return !match
			}
			if (cmd.ts && order.type !== "trailing-stop") {
				return !match
			}
			if ((cmd.sl || cmd.tp || cmd.so) && order.type !== "stop") {
				return !match
			}
			if (cmd.id && !this.checkId(order.id, ids)) {
				return !match
			}
			if (!this.filterOrder(cmd, ohlc, tickers, order)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(orders.length).getMax() < orders.length) {
			const sort = {
				"newest": 	["timestamp", true],
				"oldest": 	["timestamp", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["original_amount", true],
				"smallest": ["original_amount", false]
			}
			cmd.cmo === 'random' ? shuffle(orders) : sortByIndex(orders, sort[cmd.cmo][0], sort[cmd.cmo][1])
			orders = orders.slice(0, cmd.cm.resolve(0))
		}

		this.msgOrders(cmd, orders, totalOrders)
		if (!orders.length || cmd.ch || cmd.d) {
			orders.length && this.msgDisabled(cmd)
			return orders
		}

		const orderIds = orders.map(order => order.id)
		const multi = await this.req('POST', "/order/cancel/multi", {order_ids: orderIds})
		if (multi && (multi.result === "Orders cancelled" || multi.result === `Submitting ${orderIds.length} order cancellations.`)) {
			orderIds.forEach(id => this.removeId(id))
			return orders
		}
		return false
	}

	async positionsClose(cmd, pos, i, ohlc = {}, ticker, market) {
		ticker = (cmd.pc && !cmd.up && this.getPrice(market.pair)) || ticker || (await this.symbolTicker(cmd.up, market.pair))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.symbol} is not available!`)
		}
		ticker.pos = pos.base
		this.checkSlip(cmd, ohlc, market.pair, ticker)
		cmd.up && this.updatePrice(market.pair, ticker)

		const first = ticker[cmd.sl || cmd.tp || cmd.so || cmd.ts ? 'pos' : cmd.pr] || this.ohlc(cmd, ohlc, 'pr') || ((cmd.isBid && cmd.t !== "market") || (cmd.isAsk && cmd.t === "market") ? ticker.bid : ticker.ask)
		let price = Bitfinex.fixPrecision(cmd.fp ? cmd.fp.resolve(8) : cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(8), market.price_precision)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated)")
		}

		const leverage = cmd.l || pos.leverage || 1
		const hasStop = cmd.sl || cmd.so || cmd.tp
		const isOCO = (cmd.sl || cmd.so) && cmd.tp
		let order = {
			symbol: pos.pair,
			side: pos.side === "long" ? "sell" : "buy",
			amount: cmd.q._().div(!cmd.q.wasPercent() ? (cmd.u === "currency" ? price / (cmd.lc || leverage) : 1 / (cmd.lc || leverage)) : 1)
				.reference(pos.amount).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc, pos)).minmax(cmd.minq, cmd.maxq).resolve(8).toString(),
			price: price,
			type: cmd.ts ? "trailing-stop" : hasStop && !isOCO ? (cmd.t !== "market" ? "stop limit" : "stop") : ({fok: "fill-or-kill", ioc: "immediate-or-cancel", market: "market"})[cmd.t] || "limit",
			aff_code: this.prefixID("iU8fp-wd"),
			exchange: "bitfinex"
		}
		if (cmd.t === "post") order.is_postonly = true
		if (cmd.h) order.is_hidden = true
		if (cmd.l !== undefined) order.lev = cmd.l.toString()

		if (cmd.ts) {
			// cmd.t !== "market" && cmd.has('p') => warn!
			order.price = Bitfinex.fixPrecision(cmd.ts._().reference(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || first).abs().resolve(8), market.price_precision)
		} else if (hasStop) {
			let sl = null, tp = null
			if (cmd.sl || cmd.so) {
				sl = Bitfinex.fixPrecision((cmd.sl || cmd.so)._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(8), market.price_precision)
			}
			if (cmd.tp) {
				tp = Bitfinex.fixPrecision(cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(8), market.price_precision)
			}
			if (isOCO) {
				order.ocoorder = true
				// cmd.t !== "market" && cmd.has('p') => warn!
				order[order.side+"_price_oco"] = sl
				order.price = tp
			} else {
				if (cmd.t !== "market") order[order.side+"_stoplimit_price"] = order.price
				order.price = sl || tp
			}
		}
		this.checkOrder(order, 184859057, 8)
		return order
	}

	async positionsCloseAll(cmd, ohlc = {}, positions) {
		const symbols = await this.symbolInfo()
		if (!positions) {
			const sym = cmd.s !== '*' && cmd.s
			const market = sym && symbols[sym]
			if (sym && !market) {
				throw new Error(`Unknown market symbol: ${sym} – Use the 'Symbols' button in the exchange settings or alert editor to browse for valid symbols!`)
			}
			positions = await this.req('POST', "/positions")
			if (!positions || !positions.filter) {
				this.msgPosErr()
				return false
			}
			positions = positions.filter(pos => pos.status === "ACTIVE" && (!market || pos.symbol === market.pair))
			if (!positions.length) {
				this.msgPosNone()
				return false
			}
		}

		const tickers = {}
		for (const pos of positions) {
			pos._sym = pos.symbol
			pos.symbol = this.sym(pos._sym)
			if (!tickers[pos._sym]) {
				tickers[pos._sym] = ((cmd.up || cmd.minpl || cmd.maxpl || !cmd.s || cmd.s === '*') && (await this.symbolTicker(cmd.up, pos._sym))) || (cmd.pc && this.getPrice(pos._sym))
			}
		}

		cmd.isMargin = true
		const match = cmd.cr ? false : true
		await this.filterPosStart(cmd, ohlc, tickers, positions)
		const totalPos = positions.length
		positions = positions.filter(pos => {
			if ((cmd.isBid && pos.amount < 0) || (cmd.isAsk && pos.amount > 0)) {
				return false
			}
			const market = symbols[pos.symbol] || {}
			pos.side = pos.amount < 0 ? 'short' : 'long'
			pos.amount = Math.abs(pos.amount)
			pos.currency = cmd.currency = (market.future ? market.quote : this.meta.multiAsset) + marginSuffix
			pos.leverage = market.future ? (this.getBalanceRaw()[market.symbol] || 10) : market.leverage
			pos._sizeUSD = pos.amount * pos.base / pos.leverage * market.factor
			pos._sizeCoin = pos.amount / pos.leverage * market.factor
			pos.pnl = pos.pl / pos._sizeUSD / (market.future ? pos.leverage : 1)
			pos.mark = Bitfinex.fixPrecision(pos.base * (1 + (pos.side === 'long' ? pos.pnl : -pos.pnl)), market.price_precision+1)
			if (!this.filterPos(cmd, ohlc, tickers, pos, total => total * pos.leverage * market.factor / pos.base, 8, true)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(positions.length).getMax() < positions.length) {
			if (["newest", "oldest"].includes(cmd.cmo)) {
				this.warn(`Close Maximum Order option "${cmd.cmo}" is currently not supported!`)
			} else {
				const sort = {
					"highest": 	["base", true],
					"lowest": 	["base", false],
					"biggest": 	["amount", true],
					"smallest": ["amount", false]
				}
				cmd.cmo === 'random' ? shuffle(positions) : sortByIndex(positions, sort[cmd.cmo][0], sort[cmd.cmo][1])
				positions = positions.slice(0, cmd.cm.resolve(0))
			}
		}
		if (!positions.length || cmd.ch) {
			this.msgPos(cmd, positions, totalPos)
			return positions
		}

		let closeOrders = [], i = 0
		for (const pos of positions) {
			const order = await this.positionsClose(cmd, pos, i++, ohlc, tickers[pos._sym], symbols[pos.symbol])
			if (order) {
				closeOrders.push(order)
				//Object.assign(closeOrders.last(), Object.without(pos, ["quantity", "size", "side", "take_profit", "stop_loss", "trailing_stop"]))
				// for cmd.ch ???
			}
		}

		!cmd.ch && this.info("placing Close Position orders: "+stringify(closeOrders))
		if (cmd.ch || cmd.d) {
			this.msgDisabled(cmd)
			return closeOrders
		}
		const result = await this.req('POST', "/order/new/multi", {orders: closeOrders})
		cmd.id && result?.order_ids && result.order_ids.forEach(order => this.cacheId(order.id, cmd.id))
		return result
	}
}

Broker.add(new Bitfinex())
