"use strict"

class KucoinSandbox extends Exchange {
	static meta = {
		aliases: [
			"KUCOINSANDBOX",
			"KUCOIN-SANDBOX",
		],
		endpoint: "https://openapi-sandbox.kucoin.com",
		fields: {
			public: {
				label: "API Key",
			},
			private: {
				label: "API Secret",
			},
			password: {
				label: "API Passphrase",
			},
		},
		name: "KuCoin Sandbox",
		permissions: {
			origins: [
				"https://openapi-sandbox.kucoin.com/api/*",
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		},
		version: "v1",
		recvWindow: 5 * 1000,
		website: "https://sandbox.kucoin.com/ucenter/signup?rcode=ytw15Q",
		desc: "The most advanced cryptocurrency exchange to buy and sell Bitcoin, Ethereum, Litecoin, Monero, ZCash, DigitalNote, Ardor, Kcs.",
		spot: true, margin: true, listPos: false, marginType: 2,
		testSymbol: "BTCUSDT"
	}
	constructor(meta = KucoinSandbox.meta) {
		super(meta)
	}

	async 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 = await request(method, this.meta.endpoint + resource, params, headers)
		if (!response && method === 'GET') {
			response = await request(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
	}

	async time() {
		return (await request('GET', this.meta.endpoint + "/api/" + this.meta.version + "/timestamp", null, null)).data
	}

	async account(currency, isMargin) {
		const isIso = currency && currency.endsWith(isoSuffix)
		currency = isIso ? currency.slice(0, -isoSuffix.length) : currency
		let balances = {}

		if (isIso) {
			const data = await this.req('GET', "/isolated/accounts")
			data.assets && data.assets.forEach(item => {
				item.symbol = item.symbol.replace('-', '')
				const totalBase = Number(item.baseAsset.totalBalance), totalQuote = Number(item.quoteAsset.totalBalance)
				if (totalBase || totalQuote || item.symbol === currency) {
					balances[item.symbol+isoSuffix] = {
						available: Number(item.baseAsset.availableBalance),
						balance: totalBase,
						borrowed: Number(item.baseAsset.liability),
						quote: {
							available: Number(item.quoteAsset.availableBalance),
							balance: totalQuote,
							borrowed: Number(item.quoteAsset.liability),
						}
					}
				}
			})
		} else {
			const data = await this.req('GET', "/accounts", {type: isMargin ? "margin" : "trade"})
			data.forEach(item => {
				const total = Number(item.balance)
				if (total || item.currency == currency) {
					balances[item.currency] = {
						available: Number(item.available),
						balance: total
					}
				}
			})
			balances[currency] = balances[currency] || {available: 0, balance: 0}
		}
		return balances
	}

	async symbolInfo(symbol) {
		const symbolCache = this.getSymbolCache()
		if (symbolCache._refresh) {
			const response = await this.req('GET', "/symbols", undefined, false)

			response.forEach(info => {
				info.pricePrecision = decimals(info.priceIncrement)
				info.basePrecision = decimals(info.baseIncrement)
				info.quotePrecision = decimals(info.quoteIncrement)
				info.baseMinSize = Number(info.baseMinSize)
				info.baseMaxSize = Number(info.baseMaxSize)
				info.quoteMinSize = Number(info.quoteMinSize)
				info.quoteMaxSize = Number(info.quoteMaxSize)
				symbolCache[this.sym(info.symbol)] = info
			})
			this.updatedSymbolCache()
		}
		if (!symbol) return symbolCache

		const lookup = this.sym(symbol)
		if (!symbolCache[lookup]) {
			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[lookup]
	}

	async symbolTickerImpl(symbol) {
		const ticker = await this.req('GET', "/market/orderbook/level1", {symbol: symbol}, false)
		return ticker && {
			ask: Number(ticker.bestAsk),
			bid: Number(ticker.bestBid),
			mid: (Number(ticker.bestAsk) + Number(ticker.bestBid)) / 2,
			last: Number(ticker.price)
		}
	}

	async trade(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.symbol
		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
			}
		}

		const currency = cmd.isMargin && cmd.iso ? cmd.s + isoSuffix : cmd.isBid ? market.quoteCurrency : market.baseCurrency
		let balance = {available: 0, balance: 0}
		if (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]
			if (cmd.isMargin && cmd.iso && cmd.isBid) {
				balance = balance.quote
			}
			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 : ticker.ask || ticker.bid)
		let price = cmd.fp ? cmd.fp.resolve(market.pricePrecision) : 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)")
		}

		const useQuote = cmd.isBid && cmd.t === "market"
		const base = useQuote ? "quote" : "base"
		const size = useQuote ? "funds" : "size"
		const leverage = !cmd.isMargin || cmd.mt !== "borrow" ? 1 : 5
		const available = (cmd.y === "equity" ? balance.balance : balance.available) * leverage / (cmd.isBid && !useQuote ? price : 1)

		let order = {
			symbol: cmd._sym,
			side: cmd.isBid ? "buy" : "sell",
			[size]: cmd.q._().div(cmd.u === "currency" && !cmd.q.wasPercent() && !useQuote ? price : 1)
				.reference(available).mul(this.ohlc(cmd, ohlc, 'qr') || 1).minmax(cmd.minq, cmd.maxq).resolve(market[base+'Precision']),
			type: cmd.t === "market" ? "market" : "limit"
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order[size] = (ohlc.left * (useQuote ? price : 1)).toFixed(market[base+'Precision'])
		}
		const q = Number(order[size])
		if (q < market[base+'MinSize']) {
			this.warn(`order quantity below instrument minimum of ${market[base+'MinSize']} (${base}) – use minq= to set minimum!`)
		} else if (q > market[base+'MaxSize']) {
			this.warn(`order quantity above instrument maximum of ${market[base+'MaxSize']} (${base}) – use maxq= to set maximum!`)
		} else if (q <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated)")
		}
		if (cmd.t !== "market") {
			order.price = price
			order.timeInForce = ({fok: "FOK", ioc: "IOC"})[cmd.t] || "GTC"
		}
		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'
			order.stopPrice = (cmd.tp || cmd.sl || cmd.so)._().relative(this.ohlc(cmd, ohlc, param, ticker, price) || first).resolve(market.pricePrecision)
			order.stop = order.stopPrice >= ticker.last ? "entry" : "loss"
		}
		if (cmd.isMargin) {
			order.tradeType = "MARGIN_TRADE"
			if (cmd.iso || cmd.mt === "isolated") order.marginMode = "isolated"
			if (cmd.mt === "borrow") {
				if (cmd.iso) this.warn("auto-borrow is only supported for cross margin!")
				else order.autoBorrow = true
			} else if (cmd.mt === "repay") {
				order.autoRepay = true
			}
		}
		order.clientOid = this.uniqueID(cmd.id)

		this.info(`placing ${currency} order${cmd.isMargin?` @ ${leverage}x ${cmd.iso?'isolated':'cross'}`:''}: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		return Object.assign(order, await this.req('POST', order.stop ? "/stop-order" : cmd.isMargin ? "/margin/order" : "/orders", order))
	}

	async ordersCancel(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.symbol
		const doLimit = (!cmd.so || !cmd.so.compare(0)) && (!cmd.t || cmd.t !== "close")
		const doStops = (!cmd.so || cmd.so.compare(0)) && (!cmd.t || cmd.t !== "open")
		let type = !cmd.isMargin ? 'spot' : cmd.iso === undefined ? 'margin' : cmd.iso ? 'isolated margin' : 'cross margin'

		if (!cmd.d && !cmd.b && !cmd.fp && !cmd.id && cmd.cm.wasPercent(true) && doLimit && doStops && !cmd.gt && !cmd.gte && !cmd.lt && !cmd.lte && !cmd.sv) {
			this.info(`canceling all ${cmd._sym} ${type} orders in bulk!`)
			const spot = !cmd.isMargin ? (await this.req('DELETE', "/orders", {symbol: cmd._sym, tradeType: "TRADE"})) : {cancelledOrderIds:[]}
			const margin = cmd.isMargin && !cmd.iso ? (await this.req('DELETE', "/orders", {symbol: cmd._sym, tradeType: "MARGIN_TRADE"})) : {cancelledOrderIds:[]}
			const marginIso = cmd.isMargin && (cmd.iso || cmd.iso === undefined) ? (await this.req('DELETE', "/orders", {symbol: cmd._sym, tradeType: "MARGIN_ISOLATED_TRADE"})) : {cancelledOrderIds:[]}
			if (!spot || !spot.cancelledOrderIds || !spot.cancelledOrderIds.filter || 
				!margin || !margin.cancelledOrderIds || !margin.cancelledOrderIds.filter || 
				!marginIso || !marginIso.cancelledOrderIds || !marginIso.cancelledOrderIds.filter) {
				this.msgOrderErr(type)
				return false
			} else if (!spot.cancelledOrderIds.length && !margin.cancelledOrderIds.length && !marginIso.cancelledOrderIds.length) {
				this.msgOrdersNone(type)
				return false
			}
			return [...spot.cancelledOrderIds, ...margin.cancelledOrderIds, ...marginIso.cancelledOrderIds].map(id => ({orderId: id}))
		}

		let filter = {
			status: "active",
			symbol: cmd._sym,
			pageSize: 500
		}
		if (cmd.b) filter.side = cmd.isAsk ? "sell" : "buy"
		if (!doLimit || !doStops) type += !doLimit ? ' stop' : ' limit'

		filter.tradeType = "TRADE"
		const spot = !cmd.isMargin && doLimit ? (await this.req('GET', "/orders", filter)).items : []
		filter.tradeType = "MARGIN_TRADE"
		const margin = cmd.isMargin && doLimit && !cmd.iso ? (await this.req('GET', "/orders", filter)).items : []
		filter.tradeType = "MARGIN_ISOLATED_TRADE"
		const marginIso = cmd.isMargin && doLimit && (cmd.iso || cmd.iso === undefined) ? (await this.req('GET', "/orders", filter)).items : []
		filter.tradeType = "TRADE"
		const spotStops = !cmd.isMargin && doStops ? (await this.req('GET', "/stop-order", filter)).items : []
		filter.tradeType = "MARGIN_TRADE"
		const marginStops = cmd.isMargin && doStops ? (await this.req('GET', "/stop-order", filter)).items : []
		if (!spot || !spot.filter || !margin || !margin.filter || !marginIso || !marginIso.filter || 
			!spotStops || !spotStops.filter || !marginStops || !marginStops.filter) {
			this.msgOrderErr(type)
			return false
		}
		let orders = [...spot, ...margin, ...marginIso, ...spotStops, ...marginStops]
		if (!orders.length) {
			this.msgOrdersNone(type)
			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 = {}
		await this.filterOrderStart(cmd, ohlc, tickers, orders)
		const totalOrders = orders.length
		orders = orders.filter(order => {
			if (cmd.b && ((cmd.isAsk && order.side !== "sell") || (cmd.isBid && order.side !== "buy"))) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if (hasType && !order.type.includes(cmd.t)) {
				return !match
			}
			if ((cmd.t === "open" && order.stop.length) || (cmd.t === "close" && !order.stop.length)) {
				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": 	["timestamp", true],
				"oldest": 	["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, type)
		if (cmd.ch || cmd.d) {
			orders.length && this.msgDisabled(cmd)
			return orders
		}

		let cancelOrders = []
		for (const order of orders) {
			const response = await this.req('DELETE', "/orders/" + order.id)
			if (response && response.cancelledOrderIds[0] == order.id) {
				cancelOrders.push(order)
			}
		}
		return cancelOrders
	}

	async positionsCloseAll(cmd) {
		if (!cmd.s) return false
		throw new Error(`${this.getName()} margin trading does not use classical positions! See "Setup: Commands" and https://www.kucoin.com/support/900005034163`)
	}

	async loan(cmd) {
		let orders = []
		for (const currency of cmd.loan) {
			if (cmd.q._().isPercent()) {
				const result = (await this.req('GET', `/margin/market`, {currency})) || []
				const total = result.reduce((total, item) => total + item.size, 0)
				if (!total) continue
				cmd.q.reference(total).minmax(cmd.minq, cmd.maxq)
			}

			const order = {
				currency: currency,
				[cmd.iso?'borrowStrategy':'type']: cmd.t === "ioc" ? "IOC" : "FOK",
				size: cmd.q.resolve(4)
			}
			cmd.iso && (order.symbol = cmd.s)
			this.info(`applying for ${cmd.iso?'isolated '+cmd.s:'cross'} margin loan of ${order.size} ${currency}!`)
			if (cmd.d) {
				this.msgDisabled(cmd)
				orders.push(order)
			} else {
				orders.push(await this.req('POST', `/${cmd.iso?'isolated':'margin'}/borrow`, order))
			}
		}
		return orders
	}

	async repay(cmd) {
		let orders = []
		for (const currency of cmd.repay) {
			if (cmd.q._().isPercent()) {
				const result = (await this.req('GET', `/${cmd.iso?'isolated':'margin'}/borrow/outstanding`, {currency})).items || []
				const total = result.reduce((total, item) => total + item.liability, 0)
				if (!total) continue
				cmd.q.reference(total).minmax(cmd.minq, cmd.maxq)
			}

			const order = {
				currency: currency,
				sequence: cmd.cmo === 'highest' ? 'HIGHEST_RATE_FIRST' : 'RECENTLY_EXPIRE_FIRST',
				size: cmd.q.resolve(8)
			}
			this.info(`repaying ${cmd.iso?'isolated':'cross'} margin loan for ${order.size} ${currency}!`)

			if (cmd.d) {
				this.msgDisabled(cmd)
				orders.push(order)
			} else {
				orders.push(await this.req('POST', `/${cmd.iso?'isolated':'margin'}/repay/all`, order))
			}
		}
		return orders
	}
}

class Kucoin extends KucoinSandbox {
	static meta = Object.assign({}, super.meta, {
		aliases: [
			"KUCOIN",
		],
		endpoint: "https://openapi-v2.kucoin.com",
		name: "KuCoin",
		permissions: {
			origins: [
				"https://openapi-v2.kucoin.com/api/*",
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"kucoi1",
				"kucoi2",
			],
		},
		website: "https://www.kucoin.com/ucenter/signup?rcode=ysxgwx",
	})
	constructor(meta = Kucoin.meta) {
		super(meta)
	}
}

Broker.add(new Kucoin())
Broker.add(new KucoinSandbox())
