"use strict"

class Binance extends Exchange {
	static meta = {
		aliases: [
			"BINANCE",
		],
		endpoint: "https://api.binance.com",
		fields: {
			public: {
				label: "API Key",
			},
			private: {
				label: "API Secret",
			},
		},
		name: "Binance",
		patterns: [],
		permissions: {
			origins: [
				"https://api.binance.com/*"
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"binan1",
				"binan2",
			],
		},
		recvWindow: 30 * 1000,
		website: "https://www.binance.com/?ref=10761328",
		desc: "Binance is a global cryptocurrency exchange that provides a platform for trading more than 100 cryptocurrencies.",
		testSymbol: "BNBBTC",
		marginType: 2
	}
	constructor() {
		super(Binance.meta)
	}

	*req(method, resource, params = {}, sign = true) {
		const headers = {}
		if (sign) {
			const credentials = this.getCredentials("private")
			params.timestamp = this.getTimestamp()
			params.recvWindow = this.meta.recvWindow

			let sha = new jsSHA("SHA-256", "TEXT")
			sha.setHMACKey(credentials.private, "TEXT")
			sha.update(serialize(params))
			params.signature = sha.getHMAC("HEX")
			headers["X-MBX-APIKEY"] = credentials.public
		} else this.hasAccess()
		return yield request.bind(this, method, this.meta.endpoint + resource, params, headers)
	}

	*time() {
		const result = yield request.bind(this, 'GET', this.meta.endpoint + "/api/v3/time", null, null)
		return result.serverTime
	}

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

		const data = (yield* this.req('GET', isMargin ? `/sapi/v1/margin${isIso?'/isolated':''}/account` : `/api/v3/account`)) || {balances: []}
		const balanceData = (isMargin && (data.userAssets || data.assets)) || data.balances
		let balances = {}

		balanceData && balanceData.forEach && balanceData.forEach(item => {
			if (isIso) {
				const totalBase = Number(item.baseAsset.netAsset), totalQuote = Number(item.quoteAsset.netAsset)
				if (totalBase || totalQuote || item.symbol === currency) {
					balances[item.symbol+isoSuffix] = {
						available: Number(item.baseAsset.free),
						balance: totalBase,
						borrowed: Number(item.baseAsset.borrowed),
						quote: {
							available: Number(item.quoteAsset.free),
							balance: totalQuote,
							borrowed: Number(item.quoteAsset.borrowed),
						}
					}
				}
			} else {
				const total = isMargin ? Number(item.netAsset) : (Number(item.free) + Number(item.locked))
				if (total || item.asset === currency) {
					balances[item.asset] = {
						available: Number(item.free),
						balance: total,
					}
					isMargin && (balances[item.asset].borrowed = Number(item.borrowed)/* + Number(item.interest)*/)
				}
			}
		})
		return balances
	}

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

			response.symbols.forEach(info => {
				let filters = {}
				info.filters.forEach(filter => filters[filter.filterType] = filter)
				info.pricePrecision = filters.PRICE_FILTER ? decimals(filters.PRICE_FILTER.tickSize) : 8
				info.quantityPrecision = filters.LOT_SIZE ? decimals(filters.LOT_SIZE.stepSize) : 8
				info.minimumTradeSize = filters.LOT_SIZE ? Number(filters.LOT_SIZE.minQty) : 0
				info.minNotional = filters.NOTIONAL ? Number(filters.NOTIONAL.minNotional) : 0
				info.filters = filters
				symbolCache[info.symbol] = info
			})
			this.updatedSymbolCache()
		}
		if (!symbol) return symbolCache

		const lookup = symbol.replace(/[-_/\\:]/g, '')
		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]
	}

	*symbolTickerImpl(symbol) {
		const ticker = yield* this.req('GET', "/api/v3/ticker/bookTicker", {symbol: symbol}, false)
		return ticker && {
			ask: ticker.askPrice,
			bid: ticker.bidPrice,
			mid: (ticker.askPrice + ticker.bidPrice) / 2
		}
	}

	*trade(cmd, ohlc = {}) {
		const hasStop = cmd.sl || cmd.so || cmd.tp
		const isOCO = (cmd.sl || cmd.so) && cmd.tp
		const isMargin = cmd.isMargin || cmd.mt

		const market = yield* this.symbolInfo(cmd.s)
		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
			}
		}

		const currency = isMargin && cmd.iso ? cmd.s + isoSuffix : (cmd.isBid ? !hasStop : hasStop) ? market.quoteAsset : market.baseAsset

		let balance = {available: 0, balance: 0}
		if (cmd.q.wasPercent() || cmd.ub) {
			balance = cmd.bc && !cmd.ub && this.getBalance(currency, isMargin)
			const balances = !balance && (yield* this.account(currency, isMargin))
			balance = balance || balances[currency]
			if (isMargin && cmd.iso && cmd.isBid) {
				balance = balance.quote
			}
			this.checkBalance(cmd, balance, currency)
			if (!cmd.bc || cmd.ub) {
				this.updateBalance(balances, currency, isMargin)
				if (cmd.ub) {
					return Object.assign(ticker, balance)
				}
			}
		}

		let first = ticker[cmd.pr] || this.ohlc(cmd, ohlc, 'pr') || ((cmd.isBid && cmd.t !== "market") || (cmd.isAsk && cmd.t === "market") ? ticker.bid : ticker.ask)
		if (cmd.pr === "last") {
			const stats = yield* this.req('GET', "/api/v3/ticker/24hr", {symbol: market.symbol, type: "MINI"}, false)
			if (stats && stats.lastPrice) {
				first = stats.lastPrice
			}
		}
		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)")
		}

		let available = cmd.y === "equity" ? balance.balance : balance.available
		if (isMargin && cmd.mt === "borrow" && cmd.q.wasPercent()) {
			const query = {asset: cmd.isBid ? market.quoteAsset : market.baseAsset}
			cmd.iso && (query.isolatedSymbol = cmd.s)
			try {
				const response = yield* this.req('GET', "/sapi/v1/margin/maxBorrowable", query)
				this.info(`adding max borrowable amount of ${response.amount} to available balance!`)
				available += Number(response.amount)
			} catch(ex) {}
		}
		if ((cmd.isBid && !hasStop) || (cmd.isAsk && hasStop)) {
			available /= price
		}

		const quantity = cmd.q._().div(cmd.u === "currency" && !cmd.q.wasPercent() ? price : 1)
			.reference(available).mul(this.ohlc(cmd, ohlc, 'qr') || 1).minmax(cmd.minq, cmd.maxq).resolve(market.quantityPrecision)
		if (market.minNotional && quantity * price < market.minNotional) {
			this.warn(`order value below instrument minimum notional of ${market.minNotional} – use minq= to set minimum!`)
		} else if (market.minimumTradeSize && quantity < market.minimumTradeSize) {
			this.warn(`order quantity below instrument minimum of ${market.minimumTradeSize} – use minq= to set minimum!`)
		} else if (quantity <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated, use minq= to set minimum)")
		}

		let order = {
			symbol: market.symbol,
			side: cmd.isBid ? "BUY" : "SELL",
			quantity: quantity
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.quantity = ohlc.left.toFixed(market.quantityPrecision)
		}
		if (!isOCO) order.type = cmd.t === "market" ? "MARKET" : "LIMIT"
		if (cmd.t !== "market") {
			order.price = price
			order[isOCO ? "stopLimitTimeInForce" : "timeInForce"] = ({fok: "FOK", ioc: "IOC"})[cmd.t] || "GTC"
		}
		if (cmd.h) {
			order[isOCO ? "stopIcebergQty" : "icebergQty"] = cmd.h._().reference(quantity).resolve(market.quantityPrecision)
			if (isOCO) order.limitIcebergQty = order.stopIcebergQty
		}
		if (isMargin && cmd.iso) order.isIsolated = "TRUE"
		if (cmd.mt) order.sideEffectType = {borrow: "MARGIN_BUY", repay: "AUTO_REPAY"}[cmd.mt]
		order.newOrderRespType = "RESULT"

		if (hasStop && cmd.ts) {
			const ts = cmd.ts._().reference(price).abs()
			order.trailingDelta = ts.div(price).mul(10000).resolve(0)
		}
		if (isOCO) {
			order.side = cmd.isBid ? "SELL" : "BUY"
			if (cmd.t === "limit") order.stopLimitPrice = order.price
			order.price = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pricePrecision)
			order.stopPrice = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.pricePrecision)
		} else if (cmd.sl || cmd.so) {
			// $$$ handle cmd.so
			order.type = "STOP_LOSS" + (cmd.t !== "market" ? "_LIMIT" : "")
			order.side = cmd.isBid ? "SELL" : "BUY"
			order.stopPrice = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.pricePrecision)
		} else if (cmd.tp) {
			order.type = "TAKE_PROFIT" + (cmd.t !== "market" ? "_LIMIT" : "")
			order.side = cmd.isBid ? "SELL" : "BUY"
			order.stopPrice = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pricePrecision)
		}
		// $$$ check market.orderTypes && market.orderTypes.includes(order.type)
		if (isOCO) {
			order.stopClientOrderId = this.prefixID(_('eC1OV0hLTkgzWA'))+this.uniqueIDA(26, cmd.id, "SL")
			order.limitClientOrderId = this.prefixID(_('eC1OV0hLTkgzWA'))+this.uniqueIDA(26, cmd.id, "TP")
		} else {
			order.newClientOrderId = this.prefixID(_('eC1OV0hLTkgzWA'))+this.uniqueIDA(26, cmd.id)
		}

		this.info(`placing ${currency} order${isMargin?` @ ${cmd.iso?'isolated':'cross'} margin`:''}: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		this.checkOrder(order, 202795416, 10)
		return yield* this.req('POST', isMargin ? `/sapi/v1/margin/order${isOCO?'/oco':''}` : `/api/v3/order${isOCO?'/oco':''}`, order)
	}

	*ordersCancel(cmd, ohlc = {}) {
		const market = yield* this.symbolInfo(cmd.s)
		cmd._sym = market.symbol
		let orders = null

		if (cmd.isMargin) {
			orders = []
			if (cmd.iso === undefined || cmd.iso === 0) {
				orders.push(...((yield* this.req('GET', "/sapi/v1/margin/openOrders", {symbol: market.symbol, isIsolated: "FALSE"})) || []))
			}
			if (cmd.iso === undefined || cmd.iso === 1) {
				orders.push(...((yield* this.req('GET', "/sapi/v1/margin/openOrders", {symbol: market.symbol, isIsolated: "TRUE"})) || []))
			}
		} else {
			orders = yield* this.req('GET', "/api/v3/openOrders", {symbol: market.symbol})
		}
		if (!orders || !orders.filter) {
			this.msgOrderErr()
			return false
		} else if (!orders.length) {
			this.msgOrdersNone()
			return false
		}

		let lastListId = 0
		const ids = (cmd.id || "").split(',').map(e => alnum(e))
		const id = this.prefixID(_('eC1OV0hLTkgzWA'))
		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.isBid && order.side !== 'BUY') || (cmd.isAsk && order.side !== 'SELL')) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if (hasType && !order.type.toLowerCase().includes(cmd.t)) {
				return !match
			}
			if ((cmd.t === "open" && order.type !== "LIMIT") || (cmd.t === "close" && order.type === "LIMIT")) {
				return !match
			}
			if (cmd.id && (!order.clientOrderId || !order.clientOrderId.substr(id.length).startsWithAny(ids, true))) {
				return !match
			}
			if (order.orderListId > 0) {
				if (order.orderListId === lastListId) {
					return false
				}
				lastListId = order.orderListId
			}
			if (!this.filterOrder(cmd, ohlc, tickers, order)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(orders.length).getMax() < orders.length) {
			const sort = {
				"newest": 	["time", true],
				"oldest": 	["time", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["origQty", true],
				"smallest": ["origQty", 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', cmd.isMargin ? "/sapi/v1/margin/order" : "/api/v3/order", {
				symbol: order.symbol,
				orderId: order.orderId
			})
			if (result && result.orderReports) {
				cancelOrders.push(...result.orderReports)
			} else if (result && result.orderId) {
				cancelOrders.push(result)
			}
		}
		return cancelOrders
	}

	*positionsCloseAll() {
		throw new ReferenceError(`${this.getName()} margin trading does not use classical positions! See "Setup: Commands" and https://www.binance.vision/tutorials/binance-margin-trading-guide`)
	}

	*transfer(cmd, ohlc) {
		const fromMargin = cmd.y === "margin"
		const fromIso = fromMargin && cmd.iso
		if (cmd.iso && !cmd.s) {
			throw new ReferenceError(`Transferring to or from isolated margin requires symbol (s=) to be specified!`)
		}
		let symbol = fromIso ? cmd.s+isoSuffix : cmd.tb[0]
		const balances = yield* this.account(symbol, fromMargin)
		this.updateBalance(balances, symbol, fromMargin)

		let orders = []
		for (const currency of cmd.tb) {
			symbol = fromIso ? symbol : currency
			let balance = balances[symbol]
			if (fromIso && cmd.s.endsWith(currency)) {
				balance = balance && balance.quote
			}
			if (!balance) {
				throw new ReferenceError(`Account Balance ${symbol} not available!`)
			}

			const quantity = cmd.q._().reference(balance.available).mul(this.ohlc(cmd, ohlc, 'qr') || 1).minmax(cmd.minq, cmd.maxq).resolve(8)
			this.info(`transferring ${quantity} ${currency} from ${fromIso?'isolated margin':fromMargin?'cross margin':'spot'} to ${fromMargin?'spot':cmd.iso?'isolated margin':'cross margin'} account!`)

			const order = {
				asset: currency,
				amount: quantity,
				type: fromMargin ? 2 : 1
			}
			if (cmd.iso) {
				delete order.type
				order.symbol = cmd.s
				order.transFrom = fromMargin ? "ISOLATED_MARGIN" : "SPOT"
				order.transTo = fromMargin ? "SPOT" : "ISOLATED_MARGIN"
			}
			if (cmd.d) {
				this.msgDisabled(cmd)
				orders.push(order)
			} else {
				orders.push(yield* this.req('POST', `/sapi/v1/margin${cmd.iso?'/isolated':''}/transfer`, order))
			}
		}
		return orders
	}

	*loan(cmd) {
		if (cmd.iso && !cmd.s) {
			throw new ReferenceError(`Requesting a loan on isolated margin requires symbol (s=) to be specified!`)
		}

		let orders = []
		for (const currency of cmd.loan) {
			if (cmd.q._().isPercent()) {
				const query = {asset: currency}
				cmd.iso && (query.isolatedSymbol = cmd.s)
				const result = yield* this.req('GET', "/sapi/v1/margin/maxBorrowable", query)
				if (!Number(result.amount)) continue
				cmd.q.reference(result.amount).minmax(cmd.minq, cmd.maxq)
			}

			const order = {
				asset: currency,
				amount: cmd.q.resolve(8)
			}
			this.info(`applying for ${cmd.iso?'isolated':'cross'} margin loan of ${order.amount} ${currency}!`)
			if (cmd.iso) {
				order.isIsolated = "TRUE"
				order.symbol = cmd.s
			}
			if (cmd.d) {
				this.msgDisabled(cmd)
				orders.push(order)
			} else {
				orders.push(yield* this.req('POST', "/sapi/v1/margin/loan", order))
			}
		}
		return orders
	}

	*repay(cmd) {
		if (cmd.iso && !cmd.s) {
			throw new ReferenceError(`Repaying a loan on isolated margin requires symbol (s=) to be specified!`)
		}

		let balances = []
		if (cmd.q.wasPercent()) {
			balances = yield* this.account(cmd.repay[0], true)
			this.updateBalance(balances, cmd.repay[0], true)
		}

		let orders = []
		for (const currency of cmd.repay) {
			const balance = balances[currency] || {}
			const quantity = cmd.q._().reference(balance.borrowed || 0).minmax(cmd.minq, cmd.maxq).resolve(8)
			if (!Number(quantity)) continue
			this.info(`repaying margin loan for ${quantity} ${currency}!`)

			const order = {
				asset: currency,
				amount: quantity
			}
			if (cmd.iso) {
				order.isIsolated = "TRUE"
				order.symbol = cmd.s
			}
			if (cmd.d) {
				this.msgDisabled(cmd)
				orders.push(order)
			} else {
				orders.push(yield* this.req('POST', "/sapi/v1/margin/repay", order))
			}
		}
		return orders
	}
}

Broker.add(new Binance())
