"use strict"

class KuMEXSandbox extends Exchange {
	static meta = {
		aliases: [
			"KUMEXSANDBOX",
			"KUMEX-SANDBOX",
			"KUCOINFUTURESSANDBOX",
			"KUCOINFUTURES-SANDBOX",
			"KUCOINFTSANDBOX",
			"KUCOINFT-SANDBOX",
		],
		endpoint: "https://api-sandbox-futures.kucoin.com",
		fields: {
			public: {
				label: "API Key",
			},
			private: {
				label: "API Secret",
			},
			password: {
				label: "API Passphrase",
			},
		},
		name: "KuCoin Futures Sandbox",
		permissions: {
			origins: [
				"https://api-sandbox-futures.kucoin.com/api/*"
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		},
		version: "v1",
		recvWindow: 5 * 1000,
		website: "https://sandbox-futures.kucoin.com/?rcode=21eMxvv",
		desc: "KuCoin Futures is a cryptocurrency contract trading platform developed by KuCoin and offers up to 100x leveraged perpetual contracts.",
		defaultSymbol: "XBT",
		testSymbol: "XBTUSDM",
		margin: true
	}
	constructor(meta = KuMEXSandbox.meta) {
		super(meta)
	}

	*req(method, resource, params, sign = true) {
		const headers = {
			"Accept-Language": "en_US", 
			"Content-Type": "application/json"
		}
		if (params && method !== 'POST') {
			resource += '?'+serialize(params)
			params = null
		}
		params = params ? JSON.stringify(params) : ''
		resource = "/api/" + this.meta.version + resource

		if (sign) {
			const credentials = this.getCredentials("private")
			const ts = this.getTimestamp()
			headers["KC-API-KEY"] = credentials.public
			headers["KC-API-PASSPHRASE"] = CryptoJS.HmacSHA256(credentials.password, credentials.private).toString(CryptoJS.enc.Base64)
			headers["KC-API-SIGN"] = CryptoJS.HmacSHA256(ts + method + resource + params, credentials.private).toString(CryptoJS.enc.Base64)
			headers["KC-API-TIMESTAMP"] = ts
			headers["KC-API-KEY-VERSION"] = 2
		} else this.hasAccess()

		let response = yield request.bind(this, method, this.meta.endpoint + resource, params, headers)
		if (!response && method === 'GET') {
			response = yield request.bind(this, method, this.meta.endpoint + resource, params, headers)
		}
		if (!response.data || (response.msg && response.msg !== "success")) {
			throw new Error(response.msg || `Unknown connection error with API endpoint! (Code ${response.code || 0})`)
		}
		return response.data
	}

	*time() {
		return yield* this.req('GET', "/timestamp", undefined, false)
	}

	*account(currency) {
		let balances = {}
		const isTest = this.isTest()

		for (const ccy of (!currency || isTest ? ['XBT', 'USDT'] : [currency])) {
			const data = yield* this.req('GET', "/account-overview", {currency: ccy})
			if (!data) {
				this.info("unable to get account info!")
				return balances
			}
			balances[data.currency] = {
				available: data.availableBalance,
				balance: data.accountEquity
			}
		}
		return balances
	}

	*symbolInfo(symbol) {
		const symbolCache = this.getSymbolCache()
		if (symbolCache._refresh) {
			const response = yield* this.req('GET', "/contracts/active", undefined, false)

			response.forEach(info => {
				info.precision = decimals(info.tickSize) + (!info.tickSize.toString().endsWith('1') ? info.tickSize % 1 : 0)
				info.type = (info.typ || info.type) === 'FFCCSX' ? 'FUTURES' : 'SWAP' /* FFWCSX */
				symbolCache[info.symbol] = info
			})
			this.updatedSymbolCache()
		}
		if (!symbol) return symbolCache

		if (!symbolCache[symbol]) {
			throw new Error(`Unknown market symbol: ${symbol} – Use the 'Symbols' button in the exchange settings or alert editor to browse for valid symbols!`)
		}
		return symbolCache[symbol]
	}

	*symbolTickerImpl(symbol) {
		const ticker = yield* this.req('GET', "/ticker", {symbol: symbol}, false)
		return ticker && {
			ask: Number(ticker.bestAskPrice),
			bid: Number(ticker.bestBidPrice),
			mid: (Number(ticker.bestAskPrice) + Number(ticker.bestBidPrice)) / 2,
			last: Number(ticker.price)
		}
	}

	*trade(cmd, ohlc = {}) {
		const market = yield* this.symbolInfo(cmd.s)
		const currency = cmd.currency = market.rootSymbol || this.meta.defaultSymbol
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || (yield* this.symbolTicker(cmd.up, market.symbol))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.symbol} is not available!`)
		}
		this.checkSlip(cmd, ohlc, market.symbol, ticker)
		if (cmd.up) {
			this.updatePrice(market.symbol, 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 && (yield* 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.last) : (ticker.ask || ticker.last))
		let price = cmd.fp ? cmd.fp.resolve(market.precision) : cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(market.precision)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated, use minp= to set minimum)")
		}

		let cf = 1.0
		let leverage = cmd._lev = cmd.l * (this.ohlc(cmd, ohlc, 'lr') || 1) || 1
		if (leverage > market.maxLeverage) {
			cmd.l && (cf = market.maxLeverage / leverage)
			leverage = cmd._lev = market.maxLeverage
			this.warn(`leverage limited to ${leverage}x by instrument!`)
		}
		const available = cmd.y === "equity" ? balance.balance : balance.available
		const factor = (market.isInverse ? price : 1/price) / Math.abs(market.multiplier || 1)
		const contracts = ohlc[cmd.y] || available * (cmd.lc || leverage) * cf * factor

		let order = {
			symbol: market.symbol,
			side: cmd.isBid ? "buy" : "sell",
			type: cmd.t === "market" ? "market" : "limit",
			leverage: leverage,
			timeInForce: cmd.t === "ioc" ? "IOC" : "GTC",
			size: cmd.q._().mul(!cmd.q.wasPercent() ? (cmd.lc || leverage) * cf * (cmd.u === "currency" ? factor : 1) : 1)
				.reference(contracts).mul(this.ohlc(cmd, ohlc, 'qr') || 1).minmax(cmd.minq, cmd.maxq).resolve(0)
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.size = ohlc.left.toFixed(0)
		}
		if (cmd.t !== "market") {
			order.price = price
		}
		if (cmd.h) {
			if (!cmd.h.compare(0)) {
				order.hidden = true
			} else {
				order.iceberg = true
				order.visibleSize = cmd.h._().reference(contracts).resolve(0)
			}
		}
		if (order.size <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated, use minq= to set minimum)")
		}
		if (cmd.t === "post") {
			order.postOnly = true
		}

		if (cmd.tp || cmd.sl || cmd.so) {
			const param = cmd.tp && 'tpref' || cmd.sl && 'slref' || cmd.so && 'soref'
			const stop = this.ohlc(cmd, ohlc, param, ticker, price) || first
			order.stopPrice = (cmd.tp || cmd.sl || cmd.so)._().relative(stop).resolve(market.pricePrecision)
			order.stopPriceType = ({last:"TP", mark:"MP", index:"IP"})[cmd.sr]
			order.stop = order.stopPrice >= ticker.last ? 'up' : 'down'
			if (cmd.r && cmd.q.wasPercent(true)) {
				delete order.size
			}
		}
		if (cmd.r) {
			if (order.size) {
				order.reduceOnly = true
			} else {
				order.closeOrder = true
				delete order.leverage
			}
		}
		order.clientOid = this.uniqueID(cmd.id)

		this.info(`placing ${currency} order @ ${leverage}x: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		return Object.assign(order, yield* this.req('POST', "/orders", order))
	}

	*ordersCancel(cmd, ohlc = {}) {
		if (!cmd.ch && !cmd.d && !cmd.b && !cmd.fp && cmd.r === undefined && !cmd.id && cmd.cm.wasPercent(true) && !cmd.gt && !cmd.gte && !cmd.lt && !cmd.lte && !cmd.sv) {
			let orders = []
			if (!cmd.tp && !cmd.sl && !cmd.so && cmd.t !== "close") {
				this.info(`canceling all ${cmd.s} limit orders in bulk!`)
				orders.push(...(yield* this.req('DELETE', "/orders", {symbol: cmd.s})).cancelledOrderIds)
			}
			if (cmd.t !== "open") {
				this.info(`canceling all ${cmd.s} stop orders in bulk!`)
				orders.push(...(yield* this.req('DELETE', "/stopOrders", {symbol: cmd.s})).cancelledOrderIds)
			}
			if (!orders.length) {
				this.msgOrdersNone()
				return false
			}
			return orders
		}

		let get = {symbol: cmd.s, pageSize: 50}
		cmd.b && (get.side = cmd.isBid ? 'buy' : 'sell')
		let page = 0
		let orders = []
		let result = null
		do {
			result = yield* this.req('GET', "/orders", Object.assign(get, {status: "active", currentPage: ++page}))
			result && result.items && orders.push(...result.items)
		} while (result && result.items && page < result.totalPage)
		page = 0
		do {
			result = yield* this.req('GET', "/stopOrders", Object.assign(get, {currentPage: ++page}))
			result && result.items && orders.push(...result.items)
		} while (result && result.items && page < result.totalPage)

		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.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if (hasType && order.type !== cmd.t) {
				return !match
			}
			if (cmd.r !== undefined && !cmd.r != !order.reduceOnly) {
				return !match
			}
			if ((cmd.t === "open" && Number(order.stopPrice)) || ((cmd.t === "close" || cmd.tp || cmd.sl || cmd.so) && !Number(order.stopPrice))) {
				return !match
			}
			if (cmd.id && (!order.clientOid || !order.clientOid.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": 	["createdAt", true],
				"oldest": 	["createdAt", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["size", true],
				"smallest": ["size", 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 = yield* this.req('DELETE', "/orders/" + order.id)
			if (result && result.cancelledOrderIds) {
				cancelOrders.push(order)
			}
		}
		return cancelOrders
	}

	*positionsClose(cmd, pos, i, ohlc = {}, market) {
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || (yield* this.symbolTicker(cmd.up, market.symbol))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.symbol} is not available!`)
		}
		ticker.pos = pos.avgEntryPrice
		ticker.liq = pos.liquidationPrice
		this.checkSlip(cmd, ohlc, market.symbol, ticker)
		cmd.up && this.updatePrice(market.symbol, ticker)

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

		const factor = (market.isInverse ? price : 1/price) / Math.abs(market.multiplier || 1)
		let order = {
			symbol: pos.symbol,
			side: pos.side === 'long' ? 'sell' : 'buy',
			type: cmd.t === "market" ? "market" : "limit",
			leverage: pos.leverage,
			timeInForce: cmd.t === "ioc" ? "IOC" : "GTC",
			size: cmd.q._().mul(!cmd.q.wasPercent() ? (cmd.lc || pos.leverage) * (cmd.u === "currency" ? factor : 1) : 1)
				.reference(pos.quantity).mul(this.ohlc(cmd, ohlc, 'qr') || 1).minmax(cmd.minq, cmd.maxq).resolve(0),
			reduceOnly: cmd.r === undefined ? true : cmd.r
		}
		if (cmd.t !== "market") {
			order.price = price
		}
		if (cmd.h) {
			if (cmd.h.wasPercent(true)) {
				order.hidden = true
			} else {
				order.iceberg = true
				order.visibleSize = cmd.h._().reference(pos.quantity).resolve(0)
			}
		}
		if (order.size <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated, use minq= to set minimum)")
		} else if (cmd.q.wasPercent(true)) {
			delete order.reduceOnly
			delete order.leverage
			delete order.size
			order.closeOrder = true
		}
		if (cmd.t === "post") order.postOnly = true

		if (cmd.tp || cmd.sl || cmd.so) {
			const param = cmd.tp && 'tpref' || cmd.sl && 'slref' || cmd.so && 'soref'
			const stop = this.ohlc(cmd, ohlc, param, ticker, price) || first
			order.stopPrice = (cmd.tp || cmd.sl || cmd.so)._().relative(stop).resolve(market.pricePrecision)
			order.stopPriceType = ({last:"TP", mark:"MP", index:"IP"})[cmd.sr]
			order.stop = order.stopPrice >= ticker.last ? 'up' : 'down'
		}
		order.clientOid = this.uniqueID(cmd.id, i)

		!cmd.ch && this.info(`placing Close Position order @ ${pos.leverage}x: `+stringify(order))
		if (cmd.ch || cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		return yield* this.req('POST', "/orders", order)
	}

	*positionsCloseAll(cmd, ohlc, positions) {
		if (!positions) {
			positions = yield* this.req('GET', "/positions")
			if (!positions || !positions.filter) {
				this.msgPosErr()
				return false
			}
			positions = positions.filter(pos => (!cmd.s || cmd.s === '*' || pos.symbol == cmd.s) && pos.isOpen && pos.currentQty)
			if (!positions.length) {
				this.msgPosNone()
				return false
			}
		}

		const symbols = yield* this.symbolInfo()
		const match = cmd.cr ? false : true
		const tickers = {}
		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 market = symbols[pos.symbol]
			pos.currency = cmd.currency = market.rootSymbol || this.meta.defaultSymbol
			pos.side = pos.currentQty < 0 ? 'short' : 'long'
			pos.quantity = Math.abs(pos.currentQty)
			pos.pnl = pos.unrealisedPnlPcnt
			pos.roe = pos.unrealisedRoePcnt
			pos.leverage = pos.realLeverage
			pos._sizeCoin = pos.posMargin / (market.isInverse ? 1: pos.markPrice)
			pos._sizeUSD = pos.posMargin * (market.isInverse ? pos.markPrice : 1)
			if (!this.filterPos(cmd, ohlc, tickers, pos, balance => balance * pos.leverage * (market.isInverse ? pos.avgEntryPrice :  1/pos.avgEntryPrice) / Math.abs(market.multiplier || 1))) {
				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, symbols[pos.symbol])
			if (order) {
				closeOrders.push(order)
				Object.assign(closeOrders.last(), Object.without(pos, ["quantity", "side"]))
			}
		}
		return closeOrders
	}
}

class KuMEX extends KuMEXSandbox {
	static meta = Object.assign({}, super.meta, {
		aliases: [
			"KUMEX",
			"KUCOINFUTURES",
			"KUCOINFT",
		],
		endpoint: "https://api-futures.kucoin.com",
		name: "KuCoin Futures",
		permissions: {
			origins: [
				"https://api-futures.kucoin.com/api/*"
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"kumex1",
				"kumex2",
			],
		},
		website: "https://futures.kucoin.com/?rcode=21eMxvv",
	})
	constructor(meta = KuMEX.meta) {
		super(meta)
	}
}

Broker.add(new KuMEX())
Broker.add(new KuMEXSandbox())
