"use strict"

class DeribitTestnet extends Exchange {
	static meta = {
		aliases: [
			"DERIBITTESTNET",
			"DERIBIT-TESTNET"
		],
		endpoint: "https://test.deribit.com",
		fields: {
			public: {
				label: "Client Id",
			},
			private: {
				label: "Client Secret",
			},
		},
		name: "Deribit Testnet",
		permissions: {
			origins: [
				"https://test.deribit.com/api/*"
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		}, d: true,
		version: "v2",
		recvWindow: 60 * 1000,
		website: "https://test.deribit.com/reg-559.6685",
		desc: "Trade European Style Options: 10x leverage. Trade Bitcoin Futures: 50x leverage. The most advanced derivatives trading platform on for Bitcoin available today!",
		margin: true,
		testSymbol: "BTC-PERPETUAL"
	}
	constructor(meta = DeribitTestnet.meta) {
		super(meta)
	}

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

		if (resource.includes("private")) {
			const credentials = this.getCredentials("private")
			const timestamp = this.getTimestamp()
			const nonce = this.getNonce()
			const payload = timestamp + "\n" + nonce + "\n" + method.toUpperCase() + "\n" +
				resource + (params ? "?" + serialize(params) : "") + "\n\n"
			headers["Authorization"] = this.meta.d ? "Basic "+btoa(credentials.public+':'+credentials.private) : 
				`deri-hmac-sha256 id=${credentials.public},ts=${timestamp},sig=${CryptoJS.HmacSHA256(payload, credentials.private).toString(CryptoJS.enc.Hex)},nonce=${nonce}`
		} else this.hasAccess()

		const response = await request.call(this, method, this.meta.endpoint + resource, params, headers)
		if (!response) {
			throw new Error("Unknown connection error with API endpoint!")
		}
		if (response.error || typeof response.result === 'undefined') {
			throw new Error((response.message || "Unknown").replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) + ` (Error Code ${response.error})`)
		}
		return response.result
	}

	async time() {
		return (await request.call(this, 'GET', this.meta.endpoint + "/api/" + this.meta.version + "/public/get_time", null, null)).result
	}

	async account(currency = "BTC") {
		let balances = {}
		const data = await this.req('GET', "/private/get_account_summary", {currency: currency})

		if (data?.currency) {
			balances[data.currency] = {
				available: data.available_withdrawal_funds || data.available_funds,
				balance: data.equity
			}
		}
		return balances
	}

	async symbolInfoImpl(cache) {
		const currencies = await this.req('GET', "/public/get_currencies")
		for (const currency of currencies) {
			const response = await this.req('GET', "/public/get_instruments", {currency: currency.currency})
			response?.forEach(info => {
				info.pricePrecision = decimals(info.tick_size) + info.tick_size % 1
				info.quantityPrecision = info.contract_size > 1 ? -info.contract_size : 
					(decimals(info.contract_size) || decimals(info.min_trade_amount)) + (info.contract_size && !info.contract_size.toString().endsWith('1') ? info.contract_size % 1 : 0)
				info.contract_size = info.kind === "future" ? 1 : info.contract_size
				info.kind = ucfirsts(info.kind || "")
				cache[this.sym(info.instrument_name)] = info
			})
		}
	}

	async symbolTickerImpl(symbol) {
		const ticker = await this.req('GET', "/public/ticker", {instrument_name: symbol})

		return ticker && {
			ask: Number(ticker.best_ask_price),
			bid: Number(ticker.best_bid_price),
			mid: (Number(ticker.best_ask_price) + Number(ticker.best_bid_price)) / 2,
			mark: Number(ticker.mark_price)
		}
	}

	async trade(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.instrument_name
		const currency = cmd.currency = market.settlement_currency || market.base_currency
		const ticker = (cmd.pc && !cmd.up && this.getPrice(cmd._sym)) || (await this.symbolTicker(cmd.up, cmd._sym))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${cmd._sym} is not available!`)
		}
		this.checkSlip(cmd, ohlc, cmd._sym, ticker)
		if (cmd.up) {
			this.updatePrice(cmd._sym, ticker)
			if (!cmd.b && !cmd.ub) {
				return ticker
			}
		}

		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 && (await this.account(currency))
			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(market.pricePrecision)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated, use minp= to set minimum)")
		}

		const leverage = cmd._lev = cmd.l * (this.ohlc(cmd, ohlc, 'lr') || 1) || 1
		const available = cmd.y === "equity" ? balance.balance : balance.available
		const factor = currency !== market.base_currency ? 1 / price : price
		const contracts = ohlc[cmd.y] || available * factor * (cmd.lc || leverage) / market.contract_size

		let order = {
			direction: cmd.isBid ? "buy" : "sell",
			amount: cmd.q._().mul(cmd.u === "currency" && !cmd.q.wasPercent() ? factor * (cmd.lc || leverage) / market.contract_size : 1)
				.reference(contracts).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc)).minmax(cmd.minq, cmd.maxq).resolve(market.quantityPrecision),
			instrument_name: cmd._sym,
			type: cmd.t === "market" ? "market" : "limit",
			time_in_force: ({fok: "fill_or_kill", ioc: "immediate_or_cancel"})[cmd.t] || "good_til_cancelled"
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.amount = new NumberObject(ohlc.left).resolve(market.quantityPrecision)
		}
		if (market.min_trade_amount && order.amount < market.min_trade_amount) {
			this.warn(`order quantity below instrument minimum of ${market.min_trade_amount} – use minq= to set minimum!`)
		} else if (order.amount <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated, use minq= to set minimum)")
		}
		if (cmd.t !== "market") order.price = price
		if (cmd.h) order.max_show = cmd.h._().reference(contracts).resolve(0)
		if (cmd.t === "post") order.post_only = order.reject_post_only = true
		if (cmd.r) order.reduce_only = true

		if (cmd.tp) {
			order.type = "take_" + order.type
			order.trigger_price = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pricePrecision)
			order.trigger = cmd.sr + "_price"
		} else if (cmd.sl) {
			order.type = "stop_" + order.type
			order.trigger_price = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.pricePrecision)
			order.trigger = cmd.sr + "_price"
		} else if (cmd.ts) {
			order.type = "trailing_stop"
			order.trigger_offset = cmd.ts._().reference(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || first).resolve(market.pricePrecision)
			order.trigger = cmd.sr + "_price"
		}
		if (cmd.id) order.label = this.uniqueID(cmd.id)
		if (cmd.l !== undefined) {
			!cmd.d && this.setBalanceRaw(cmd._sym, leverage)
			this.info("does not offer isolated margin – leverage parameter only used for calculation and position check!")
		}

		this.info(`placing ${currency} order: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}

		const result = await this.req('GET', "/private/"+order.direction, order)
		return result.order
	}

	async ordersCancel(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.instrument_name
		if (!cmd.ch && !cmd.d && !cmd.b && !cmd.fp && !cmd.id && cmd.cm.wasPercent(true) && !cmd.gt && !cmd.gte && !cmd.lt && !cmd.lte) {
			const type = cmd.ts ? "trailing_stop" : cmd.sl ? "stop" : cmd.tp ? "take" : (cmd.so || cmd.t === "close") ? "trigger_all" : (cmd.has('t') && cmd.t === "limit") ? "limit" : "all"
			this.info(`canceling ${type} ${cmd._sym} orders in bulk!`)
			return await this.req('GET', "/private/cancel_all_by_instrument", {instrument_name: cmd._sym, type: type, detailed: true})
		}

		let orders = await this.req('GET', "/private/get_open_orders_by_instrument", {instrument_name: cmd._sym, type: "all"})
		if (!orders || !orders.filter) {
			this.msgOrderErr()
			return false
		} else 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.direction !== 'buy') || (cmd.isAsk && order.direction !== 'sell')) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(order.price === 'market_price' ? 0 : order.price)) {
				return !match
			}
			if ((cmd.t === "open" && order.order_type.includes("stop")) || ((cmd.t === "close" || cmd.sl || cmd.tp) && !order.order_type.includes("stop"))) {
				return !match
			}
			if (cmd.r !== undefined && !cmd.r != !order.reduce_only) {
				return !match
			}
			if (cmd.id && (!order.label || !order.label.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": 	["creation_timestamp", true],
				"oldest": 	["creation_timestamp", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["amount", true],
				"smallest": ["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 (cmd.ch || cmd.d) {
			orders.length && this.msgDisabled(cmd)
			return orders
		}

		let cancelOrders = []
		for (const order of orders) {
			const result = await this.req('GET', "/private/cancel", {order_id: order.order_id})
			if (result?.order_id) {
				cancelOrders.push(result)
			}
		}
		return cancelOrders
	}

	async positionsClose(cmd, pos, i, ohlc = {}, market) {
		cmd._sym = market.instrument_name
		const ticker = (cmd.pc && !cmd.up && this.getPrice(cmd._sym)) || (await this.symbolTicker(cmd.up, cmd._sym))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${cmd._sym} is not available!`)
		}
		ticker.pos = pos.average_price
		ticker.liq = pos.estimated_liquidation_price
		this.checkSlip(cmd, ohlc, cmd._sym, ticker)
		cmd.up && this.updatePrice(cmd._sym, 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(market.pricePrecision)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated, use minp= to set minimum)")
		}

		const leverage = cmd.l || pos.leverage || 1
		const factor = pos.currency !== market.base_currency ? 1 / price : price
		const contracts = pos.quantity

		let order = {
			direction: pos.direction === 'buy' ? 'sell' : 'buy',
			amount: cmd.q._().mul(cmd.u === "currency" && !cmd.q.wasPercent() ? factor * (cmd.lc || leverage) / market.contract_size : 1)
				.reference(contracts).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc, pos)).minmax(cmd.minq, cmd.maxq).resolve(market.quantityPrecision),
			instrument_name: cmd._sym,
			type: cmd.t === "market" ? "market" : "limit",
			time_in_force: ({fok: "fill_or_kill", ioc: "immediate_or_cancel"})[cmd.t] || "good_til_cancelled",
			reduce_only: cmd.r === undefined ? true : cmd.r
		}
		if (cmd.t !== "market") {
			order.price = price
		}
		if (cmd.h) order.max_show = cmd.h._().reference(contracts).resolve(0)
		if (cmd.t === "post") order.post_only = order.reject_post_only = true

		if (cmd.tp) {
			order.type = "take_" + order.type
			order.trigger_price = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pricePrecision)
			order.trigger = cmd.sr + "_price"
		} else if (cmd.sl) {
			order.type = "stop_" + order.type
			order.trigger_price = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.pricePrecision)
			order.trigger = cmd.sr + "_price"
		} else if (cmd.ts) {
			order.type = "trailing_stop"
			order.trigger_offset = cmd.ts._().reference(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || first).resolve(market.pricePrecision)
			order.trigger = cmd.sr + "_price"
		} else if (!cmd.b && !cmd.ch && !cmd.h && cmd.t !== "post" && order.amount >= pos.quantity) {
			order = {
				instrument_name: cmd._sym,
				type: cmd.t === "market" ? "market" : "limit"
			}
			if (cmd.t !== "market") order.price = price
			this.info("calling Close Position API function: "+stringify(order))
			if (cmd.d) {
				this.msgDisabled(cmd)
				return order
			}
			const result = await this.req('GET', "/private/close_position", order)
			return result.order
		}
		if (cmd.id) order.label = this.uniqueID(cmd.id, i)

		!cmd.ch && this.info("placing Close Position order: "+stringify(order))
		if (cmd.ch || cmd.d) {
			this.msgDisabled(cmd)
			return order
		}

		const result = await this.req('GET', "/private/"+order.direction, order)
		return result.order
	}

	async positionsCloseAll(cmd, ohlc, positions) {
		const symbols = await this.symbolInfo()
		if (!positions) {
			const isAll = !cmd.s || cmd.s === '*'
			const market = !isAll && symbols[this.sym(cmd.s)] || {}
			const currencies = !isAll ? [{currency: market.settlement_currency || market.base_currency}] : (await this.req('GET', "/public/get_currencies"))
			positions = []
			for (const currency of currencies) {
				const response = await this.req('GET', "/private/get_positions", {currency: currency.currency})
				if (!response || !response.filter) {
					this.msgPosErr()
					return false
				}
				positions.push(...response)
			}

			positions = positions.filter(pos => (isAll || pos.instrument_name === market.instrument_name) && pos.size && pos.direction !== 'zero')
			if (!positions.length) {
				this.msgPosNone()
				return false
			}
		}

		const match = cmd.cr ? false : true
		const tickers = {}
		await this.filterPosStart(cmd, ohlc, tickers, positions)
		const totalPos = positions.length
		positions = positions.filter(pos => {
			if ((cmd.isBid && pos.direction !== 'buy') || (cmd.isAsk && pos.direction !== 'sell')) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(pos.average_price)) {
				return !match
			}
			const market = symbols[this.sym(pos.instrument_name)]
			cmd._sym = market.symbol
			pos.currency = cmd.currency = market.settlement_currency || market.base_currency
			pos.maxLeverage = pos.leverage
			pos.leverage = this.getBalanceRaw()[pos.instrument_name] || 1
			pos.quantity = Math.abs(pos.currency !== market.base_currency ? pos.size_currency : pos.size)
			pos._sizeCoin = pos.initial_margin
			pos._sizeUSD = pos.initial_margin * pos.average_price
			pos.pnl = pos.floating_profit_loss / pos.initial_margin / pos.leverage
			const factor = pos.currency !== market.base_currency ? 1 / pos.average_price : pos.average_price
			if (!this.filterPos(cmd, ohlc, tickers, pos, balance => balance * pos.leverage * factor / market.contract_size, market.quantityPrecision)) {
				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": 	["average_price", true],
					"lowest": 	["average_price", 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 = await this.positionsClose(cmd, pos, i++, ohlc, symbols[this.sym(pos.instrument_name)])
			if (order) {
				closeOrders.push(order)
				Object.assign(closeOrders.last(), Object.without(pos, ["quantity", "size", "direction"]))
			}
		}
		return closeOrders
	}

	async transfer(cmd, ohlc) {
		const response = await this.req('GET', "/private/get_subaccounts")
		if (!response || !response.filter) {
			this.info("unable to get sub account information!")
			return false
		}
		if (!response.length) {
			this.info("no sub accounts found!")
			return false
		}
		let subs = {}
		response.forEach(info => {
			subs[info.username] = info
			subs[info.id] = info
		})
		const sub = subs[cmd.y]
		if (!sub) {
			throw new ReferenceError(`Account with alias or id "${cmd.y}" not found! (available: ${response.map(info => info.username + ' / ' + info.id).join(', ')})`)
		}

		let balances = await this.account(cmd.tb[0])
		this.updateBalance(balances, cmd.tb[0])

		let orders = []
		for (const currency of cmd.tb) {
			const balance = balances[currency]
			if (!balance) {
				throw new ReferenceError(`Account Balance ${currency} not available!`)
			}

			const quantity = cmd.q._().reference(balance.available_withdrawal_funds || balance.available_funds || balance.available).mul(this.ohlc(cmd, ohlc, 'qr') || 1).minmax(cmd.minq, cmd.maxq).resolve(8)
			this.info(`transferring ${quantity} ${currency} to account "${sub.username || sub.system_name}" (id ${sub.id})!`)

			const order = {
				currency: currency,
				amount: quantity,
				destination: sub.id
			}

			if (cmd.d) {
				this.msgDisabled(cmd)
				orders.push(order)
			} else {
				orders.push(await this.req('GET', "/private/submit_transfer_to_subaccount", order))
			}
		}
		return orders
	}
}

class Deribit extends DeribitTestnet {
	static meta = Object.assign({}, super.meta, {
		aliases: [
			"DERIBIT"
		],
		endpoint: "https://www.deribit.com",
		name: "Deribit",
		permissions: {
			origins: [
				"https://www.deribit.com/api/*"
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"derib1",
				"derib2",
			],
		}, d: false,
		website: "https://www.deribit.com/reg-2725.4676",
	})
	constructor(meta = Deribit.meta) {
		super(meta)
	}
}

Broker.add(new Deribit())
Broker.add(new DeribitTestnet())
