"use strict"

class BitMEXTestnet extends Exchange {
	static meta = {
		aliases: [
			"BITMEXTESTNET",
			"BITMEX-TESTNET",
		],
		endpoint: "https://testnet.bitmex.com",
		fields: {
			public: {
				label: "ID",
			},
			private: {
				label: "Secret",
			}
		},
		name: "BitMEX Testnet",
		patterns: [],
		permissions: {
			origins: [
				"https://testnet.bitmex.com/api/*"
			],
			permissions: [
				"webRequest",
				"webRequestBlocking",
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		},
		version: "v1",
		recvWindow: 30 * 1000,
		website: "https://www.bitmex.com/app/register/oqpJQC",
		desc: "Trade Bitcoin and other cryptocurrencies with up to 100x leverage. Fast execution, low fees, Bitcoin futures and swaps: available only on BitMEX.",
		defaultSymbol: "XBT",
		testSymbol: "XBTUSD",
		margin: true
	}
	constructor() {
		super(BitMEXTestnet.meta)
	}

	*req(method, resource, params, sign = true) {
		resource = "/api/" + this.meta.version + resource
		params = serialize(params).replace(/%20/g, '+')
		if (method === 'GET' && params) {
			resource += '?' + params
			params = ''
		}

		const expires = Math.ceil(this.getTimestamp() / 1000) + this.meta.recvWindow / 1000
		const payload = method + resource + expires + params
		let headers = {}
		if (sign) {
			const credentials = this.getCredentials("private")
			let sha = new jsSHA("SHA-256", "TEXT")
			sha.setHMACKey(credentials.private, "TEXT")
			sha.update(payload)
			headers = {
				"api-expires": expires,
				"api-key": credentials.public,
				"api-signature": sha.getHMAC("HEX")
			}
		} else this.hasAccess()

		this.removeOrigin()
		return yield request.bind(this, method, this.meta.endpoint + resource, params, headers)
	}

	*time() {
		const data = yield request.bind(this, 'GET+', this.meta.endpoint + "/api/v1/trade", {symbol: '.BVOL24H', columns: 'price', count: 1, reverse: true}, null)
		return data._headers.get('x-ratelimit-reset') * 1000
	}

	*account(currency) {
		// /wallet/assets
		const data = yield* this.req('GET', "/user/margin", {currency: "all"})
		let balances = {}

		if (!data || !data.forEach) {
			this.info("unable to get account info!")
			return balances
		}
		data.forEach(item => {
			const currency = item.currency.toUpperCase()
			const f = currency === 'USDT' ? 1000000 : 100000000
			balances[currency] = {
				available: item.availableMargin / f,
				balance: item.marginBalance / f
			}
		})
		return balances
	}

	*symbolInfo(symbol, noPrice) {
		const norm = info => {
			info.type = info.typ === 'FFCCSX' ? 'Futures' : info.typ === 'IFXXXP' ? 'Spot' : 'Swap' /* FFWCSX */
			info.currency = info.settlCurrency && info.settlCurrency.toUpperCase() || this.meta.defaultSymbol
			const f = info.currency === 'USDT' ? 1000000 : 100000000
			info.multiplier = Math.abs(Number(info.multiplier) || f) / f
			info.precision = decimals(info.tickSize) + (!info.tickSize.toString().endsWith('1') ? info.tickSize % 1 : 0)
			info.lotSize = Number(info.lotSize) || 0
			info.lotSize = info.lotSize <= 1 ? 0 : -info.lotSize
			info.takerFee = Number(info.takerFee) || 0
			info.maxLeverage = 1 / Number(info.initMargin)
			return info
		}

		if (!symbol || noPrice) {
			const symbolCache = this.getSymbolCache()
			if (symbolCache._refresh) {
				const response = yield* this.req('GET', "/instrument/active", null, false)
				response.forEach(info => symbolCache[info.symbol] = norm(info))
				this.updatedSymbolCache()
			}
			return symbol ? symbolCache[symbol] : symbolCache
		}

		let info = yield* this.req('GET', "/instrument", {symbol: symbol})
		if (!info || !info.length) {
			throw new Error(`Unknown market symbol: ${symbol} – Use the 'Symbols' button in the exchange settings or alert editor to browse for valid symbols!`)
		}
		info = norm(info.shift())
		return {
			ask: Number(info.askPrice),
			mid: Number(info.midPrice),
			bid: Number(info.bidPrice),
			last: Number(info.lastPrice),
			mark: Number(info.markPrice),
			currency: info.currency,
			multiplier: info.multiplier,
			precision: info.precision,
			lotSize: info.lotSize,
			takerFee: info.takerFee,
			maxLeverage: info.maxLeverage,
		}
	}

	*symbolTickerImpl(symbol) {
		return yield* this.symbolInfo(symbol)
	}

	static contracts(ticker, balance, leverage, price, applyFee) {
		if (applyFee) {
			balance -= balance * ticker.takerFee * leverage * 2
		}
		return price < 1 || ticker.multiplier < 1 || ticker.currency === 'USDT' ? 
				Math.floor(balance * leverage / price / ticker.multiplier) : 
				Math.floor(balance * leverage * price * ticker.multiplier)
	}

	*trade(cmd, ohlc = {}) {
		const symbol = cmd.s
		const ticker = (cmd.pc && !cmd.up && this.getPrice(symbol)) || (yield* this.symbolInfo(symbol))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${symbol} is not available!`)
		}
		this.checkSlip(cmd, ohlc, symbol, ticker)
		if (cmd.up) {
			this.updatePrice(symbol, ticker)
			if (!cmd.b && !cmd.ub) {
				return ticker
			}
		}

		const currency = cmd.currency = ticker.currency
		let balance = {available: 0, balance: 0}
		if ((cmd.y !== "possize" && cmd.q.wasPercent()) || cmd.ub) {
			balance = cmd.bc && !cmd.ub && this.getBalance(currency)
			const balances = !balance && (yield* this.account())
			balance = balance || balances[currency]
			this.checkBalance(cmd, balance, currency)
			if (!cmd.bc || cmd.ub) {
				this.updateBalance(balances, currency)
				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 = (cmd.fp || cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp)).resolve(ticker.precision)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated, use minp= to set minimum)")
		}

		const available = cmd.y === "equity" ? balance.balance : balance.available
		const cross = cmd.l === 0
		const leverage = cross ? 100 : cmd.l * (this.ohlc(cmd, ohlc, 'lr') || 1) || (yield* this.getLeverage(symbol)) || 100
		cmd._lev = cross ? 0 : leverage
		const contracts = ohlc[cmd.y] || BitMEXTestnet.contracts(ticker, available, cmd.lc || leverage, price, true)

		let order = {
			symbol: symbol,
			side: cmd.isBid ? "Buy" : "Sell",
			ordType: cmd.t === "market" ? "Market" : "Limit",
			timeInForce: ({market: "ImmediateOrCancel", fok: "FillOrKill", ioc: "ImmediateOrCancel", day: "Day"})[cmd.t] || "GoodTillCancel",
			orderQty: cmd.q._().mul(cmd.u === "currency" && !cmd.q.wasPercent() ? BitMEXTestnet.contracts(ticker, 1, cmd.lc || leverage, price, true) : 1)
				.reference(contracts).mul(this.ohlc(cmd, ohlc, 'qr') || 1).minmax(cmd.minq, cmd.maxq).resolve(ticker.lotSize || 0)
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.orderQty = ohlc.left.toFixed(Math.abs(ticker.lotSize || 0))
		}
		if (cmd.t !== "market") {
			order.price = price
		}
		if (cmd.h) {
			order.displayQty = cmd.h._().reference(contracts).resolve(ticker.lotSize || 0)
		}
		/*if (ticker.min && order.orderQty < ticker.min * leverage) {
			this.warn(`order quantity below instrument minimum of ${ticker.min} – use minq= to set minimum!`)
		} else */if (order.orderQty <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated, use minq= to set minimum)")
		}

		let execInst = []
		if (cmd.t === "post") {
			execInst.push("ParticipateDoNotInitiate")
		}

		if (cmd.sl || cmd.so) {
			order.ordType = cmd.t === "market" ? "Stop" : "StopLimit"
			const param = cmd.sl && 'slref' || 'soref'
			const stop = this.ohlc(cmd, ohlc, param, ticker, price) || first
			if (cmd.ts && !cmd.ts.compare(1)) {
				order.pegPriceType = "TrailingStopPeg"
				order.pegOffsetValue = (cmd.sl || cmd.so)._().reference(stop).resolve(ticker.precision)
			} else {
				order.stopPx = (cmd.sl || cmd.so)._().relative(stop).resolve(ticker.precision)
			}
		} else if (cmd.tp) {
			order.ordType = cmd.t === "market" ? "MarketIfTouched" : "LimitIfTouched"
			const stop = this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first
			if (cmd.ts && !cmd.ts.compare(1)) {
				order.pegPriceType = "TrailingStopPeg"
				order.pegOffsetValue = cmd.tp._().reference(stop).resolve(ticker.precision)
			} else {
				order.stopPx = cmd.tp._().relative(stop).resolve(ticker.precision)
			}
		} else if (cmd.ts) {
			order.ordType = cmd.t === "market" ? "Stop" : "StopLimit"
			order.pegPriceType = "TrailingStopPeg"
			order.pegOffsetValue = cmd.ts._().reference(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || first).resolve(ticker.precision)
		}
		if (cmd.sl || cmd.tp || cmd.ts) {
			execInst.push(ucfirst(cmd.sr) + "Price")
			if (cmd.r && cmd.q.wasPercent(true)) {
				delete order.orderQty
			}
		}
		if (cmd.r) execInst.push(order.orderQty ? "ReduceOnly" : "Close")
		if (execInst.length) order.execInst = execInst.join(",")
		if (cmd.id) order.clOrdID = this.uniqueID(cmd.id)

		this.info(`placing ${currency} order @ ${cross?'cross':leverage.toFixed(2).replace('.00','')+'x'}: ${stringify(order)}`)
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}

		try {
			if (cmd.l !== undefined) {
				this.info(`setting contract leverage to ${cross?'cross':leverage+'x'}...`)
				yield* this.req('POST', "/position/leverage", {symbol: symbol, leverage: cross ? 0 : leverage})
			}
		} catch(ex) {
			const err = (ex.error && ex.error.message) || ex.message || ex
			this.warn(`couldn't set leverage, please set manually! (Error: ${err})`)
		}
		const result = yield* this.req('POST', "/order", order)
		if (cmd.t === 'post' && cmd.ifr && (result.ordStatus === 'Rejected' || result.ordStatus === 'Canceled')) {
			throw new Error(errRejected)
		}
		return result
	}

	*ordersCancel(cmd, ohlc = {}) {
		const symbol = cmd.s
		let filter = {}
		if (cmd.b) filter.side = cmd.isBid ? "Buy" : "Sell"
		if (cmd.fp) {
			const ticker = yield* this.symbolInfo(symbol, true)
			if (!ticker) {
				throw new ReferenceError(`Ticker ${symbol} is not available!`)
			}
			filter.price = cmd.fp.resolve(ticker.precision)
		}

		if (!cmd.ch && !cmd.d && !cmd.tp && !cmd.sl && !cmd.so && !cmd.ts && !cmd.has('t') && cmd.r === undefined && !cmd.id && cmd.cm.wasPercent(true) && !cmd.gt && !cmd.gte && !cmd.lt && !cmd.lte) {
			this.info(`canceling all ${symbol} orders in bulk!`)
			const result = yield* this.req('DELETE', "/order/all", {
				filter: JSON.stringify(filter),
				symbol: symbol
			})
			if (!result || !result.filter) {
				this.msgOrderErr()
				return false
			} else if (!result.length) {
				this.msgOrdersNone()
				return false
			}
			return result
		}
		//filter.open = true // $$$ broken in testnet?

		let orders = yield* this.req('GET', "/order", {
			filter: JSON.stringify(filter),
			symbol: symbol
		})
		if (!orders || !orders.filter) {
			this.msgOrderErr()
			return false
		}
		orders = orders.filter(order => order.ordStatus !== "Canceled" && order.ordStatus !== "Rejected" && order.ordStatus !== "Filled")
		if (!orders.length) {
			this.msgOrdersNone()
			return false
		}

		const ids = (cmd.id || "").split(',')
		const hasType = cmd.has('t') && (cmd.t === "limit" || cmd.t === "market")
		const match = cmd.cr ? false : true
		const tickers = {}
		yield* this.filterOrderStart(cmd, ohlc, tickers, orders)
		const totalOrders = orders.length
		orders = orders.filter(order => {
			if ((cmd.tp && !order.ordType.endsWith("IfTouched")) || ((cmd.sl || cmd.so) && !order.ordType.startsWith("Stop")) || (cmd.ts && !order.pegPriceType)) {
				return !match
			}
			if (hasType && ((cmd.t === "limit" && !order.ordType.includes("Limit")) || (cmd.t === "market" && order.ordType.includes("Limit")))) {
				return !match
			}
			if (cmd.r !== undefined && !cmd.r != !order.execInst.includes("ReduceOnly")) {
				return !match
			}
			if ((cmd.t === "open" && Number(order.stopPx)) || (cmd.t === "close" && !Number(order.stopPx))) {
				return !match
			}
			if (cmd.id && (!order.clOrdID || !order.clOrdID.startsWithAny(ids, true))) {
				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": 	["orderQty", true],
				"smallest": ["orderQty", 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.orderID)
		const result = yield* this.req('DELETE', "/order", {orderID: orderIds.join(',')})
		let i = 0
		result.forEach(order => order.leavesQty = orders[i++].leavesQty)
		return result
	}

	*positionsClose(cmd, pos, i, ohlc = {}, ticker) {
		ticker.pos = pos.avgEntryPrice
		ticker.even = pos.breakEvenPrice
		ticker.liq = pos.liquidationPrice
		this.checkSlip(cmd, ohlc, pos.symbol, ticker)
		cmd.up && this.updatePrice(pos.symbol, ticker)

		const first = ticker[cmd.sl || cmd.tp || 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 = (cmd.fp || cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp)).resolve(ticker.precision)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated, use minp= to set minimum)")
		}

		let order = {
			symbol: pos.symbol,
			ordType: cmd.t === "market" ? "Market" : "Limit",
			timeInForce: ({market: "ImmediateOrCancel", fok: "FillOrKill", ioc: "ImmediateOrCancel", day: "Day"})[cmd.t] || "GoodTillCancel",
			orderQty: cmd.q._().mul(cmd.u === "currency" && !cmd.q.wasPercent() ? BitMEXTestnet.contracts(ticker, 1, cmd.lc || pos.leverage, price, true) : 1)
				.reference(pos.quantity).mul(this.ohlc(cmd, ohlc, 'qr') || 1).minmax(cmd.minq, cmd.maxq).resolve(ticker.lotSize || 0) * (pos.currentQty > 0 ? -1 : 1)
		}
		if (cmd.t !== "market") order.price = price
		if (cmd.h) {
			order.displayQty = cmd.h._().reference(pos.quantity).resolve(ticker.lotSize || 0)
		} else if (cmd.q.wasPercent(true)) {
			order.side = order.orderQty > 0 ? "Buy" : "Sell"
			delete order.orderQty
		}

		let execInst = []
		if (cmd.t === "post") execInst.push("ParticipateDoNotInitiate")
		if (cmd.r !== false) execInst.push(order.orderQty ? "ReduceOnly" : "Close")

		if (cmd.sl) {
			order.ordType = cmd.t === "market" ? "Stop" : "StopLimit"
			const stop = this.ohlc(cmd, ohlc, 'slref', ticker, price) || first
			if (cmd.ts && !cmd.ts.compare(1)) {
				order.pegPriceType = "TrailingStopPeg"
				order.pegOffsetValue = cmd.sl._().reference(stop).resolve(ticker.precision)
			} else {
				order.stopPx = cmd.sl._().relative(stop).resolve(ticker.precision)
			}
		} else if (cmd.tp) {
			order.ordType = cmd.t === "market" ? "MarketIfTouched" : "LimitIfTouched"
			const stop = this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first
			if (cmd.ts && !cmd.ts.compare(1)) {
				order.pegPriceType = "TrailingStopPeg"
				order.pegOffsetValue = cmd.tp._().reference(stop).resolve(ticker.precision)
			} else {
				order.stopPx = cmd.tp._().relative(stop).resolve(ticker.precision)
			}
		} else if (cmd.ts) {
			order.ordType = cmd.t === "market" ? "Stop" : "StopLimit"
			order.pegPriceType = "TrailingStopPeg"
			order.pegOffsetValue = cmd.ts._().reference(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || first).resolve(ticker.precision)
		}
		if (cmd.sl || cmd.tp || cmd.ts) {
			execInst.push(ucfirst(cmd.sr) + "Price")
		}
		if (execInst.length) order.execInst = execInst.join(",")
		if (cmd.id) order.clOrdID = this.uniqueID(cmd.id, i)

		!cmd.ch && this.info(`placing Close Position order @ ${pos.leverage.toFixed(2).replace(".00", "")}x: ${stringify(order)}`)
		if (cmd.ch || cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		const result = yield* this.req('POST', "/order", order)
		if (cmd.t === 'post' && cmd.ifr && (result.ordStatus === 'Rejected' || result.ordStatus === 'Canceled')) {
			throw new Error(errRejected)
		}
		return result
	}

	*getLeverage(symbol) {
		let data = null
		try {
			const result = yield* this.req('GET', "/position", {filter: JSON.stringify({symbol: symbol}), count: 1})
			data = result[0]
		} catch (ex) {}
		return data && data.leverage
	}

	*positionsCloseAll(cmd, ohlc, positions) {
		if (!positions) {
			let filter = {isOpen: true}
			if (cmd.s && cmd.s !== '*') filter.symbol = cmd.s
			if (cmd.has('l')) filter.leverage = cmd.l === 0 ? 100 : cmd.l
			positions = yield* this.req('GET', "/position", {filter: JSON.stringify(filter)})
			if (!positions || !positions.filter) {
				this.msgPosErr()
				return false
			}
			positions = positions.filter(pos => pos.currentQty != 0)
			if (!positions.length) {
				this.msgPosNone()
				return false
			}
		}

		const tickers = {}
		for (const pos of positions) {
			const ticker = tickers[pos.symbol] = (cmd.pc && !cmd.up && this.getPrice(pos.symbol)) || (yield* this.symbolInfo(pos.symbol, cmd.ch && !cmd.up))
			if (!ticker) {
				throw new ReferenceError(`Ticker ${pos.symbol} is not available!`)
			}
		}

		const match = cmd.cr ? false : true
		yield* this.filterPosStart(cmd, ohlc, tickers, positions)
		const totalPos = positions.length
		positions = positions.filter(pos => {
			if ((cmd.isBid && pos.currentQty < 0) || (cmd.isAsk && pos.currentQty > 0)) {
				return false
			}
			const ticker = tickers[pos.symbol]
			pos.currency = cmd.currency = ticker.currency
			pos.side = pos.currentQty < 0 ? 'short' : 'long'
			pos.quantity = Math.abs(pos.currentQty)
			pos.pnl = pos.unrealisedPnlPcnt
			pos.roe = pos.unrealisedRoePcnt
			const f = pos.currency === 'USDT' ? 1000000 : 100000000
			pos.unrealisedPnl /= f
			pos._sizeCoin = pos.posMargin / f
			pos._sizeUSD = Math.abs(pos.homeNotional * pos.markPrice / pos.leverage)
			if (!this.filterPos(cmd, ohlc, tickers, pos, balance => BitMEXTestnet.contracts(ticker, balance, pos.leverage, pos.avgEntryPrice, true))) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(positions.length).getMax() < positions.length) {
			const sort = {
				"newest": 	["openingTimestamp", true],
				"oldest": 	["openingTimestamp", false],
				"highest": 	["avgEntryPrice", true],
				"lowest": 	["avgEntryPrice", false],
				"biggest": 	["quantity", true],
				"smallest": ["quantity", 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 = yield* this.positionsClose(cmd, pos, i++, ohlc, tickers[pos.symbol])
			if (order) {
				closeOrders.push(order)
				Object.assign(closeOrders.last(), Object.without(pos, ["quantity", "side"]))
			}
		}
		return closeOrders
	}
}

Broker.add(new BitMEXTestnet())
