"use strict"

class Kraken extends Exchange {
	static meta = {
		aliases: [
			"KRAKEN",
		],
		endpoint: "https://api.kraken.com",
		fields: {
			public: {
				label: "API Key",
			},
			private: {
				label: "Private Key",
			}
		},
		name: "Kraken",
		permissions: {
			origins: [
				"https://api.kraken.com/*"
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"krake1",
				"krake2",
			],
		},
		version: "0",
		website: "https://www.kraken.com",
		desc: "Buy, sell and margin trade Bitcoin (BTC) and Etherum (ETH) in exchange with EUR, USD, CAD, GBP, and JPY. Leveraged trading on US based Bitcoin and Ethereum exchange.",
		defaultSymbol: "XXBT",
		testSymbol: "XBTUSD",
		margin: true, spot: true, leavesQuantity: true
	}
	constructor(meta = Kraken.meta) {
		super(meta)
	}

	async req(method, resource, params = {}) {
		resource = `/${this.meta.version}/${method === 'GET' ? 'public' : 'private'}`+resource

		let headers = {}
		if (method === 'GET') {
			this.hasAccess()
			resource += '?' + serialize(params).replace(/%20/g, '+')//.replace(/\+/g, '%2B')
			params = null
		} else {
			const crypt = (path, nonce, data, key) => {
				const hashBytes = CryptoJS.SHA256(nonce + data).toString(CryptoJS.enc.Latin1)
				return CryptoJS.HmacSHA512(CryptoJS.enc.Latin1.parse(path + hashBytes), CryptoJS.enc.Base64.parse(key)).toString(CryptoJS.enc.Base64)
			}
			const credentials = this.getCredentials("private")
			const nonce = this.getNonce()
			params.nonce = nonce
			params = serialize(params)
			headers["api-key"] = credentials.public
			headers["api-sign"] = crypt(resource, nonce, params, credentials.private)
		}

		const response = await request.call(this, method, this.meta.endpoint + resource, params, headers)
		if (response.error && response.error.length) {
			const error = response.error[0]
			if (error[0] === 'E') {
				throw new Error(error.substr(1))
			}
			this.warn(error.substr(1))
		}
		return response.result
	}

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

	async account(currency, isMargin) {
		const isTest = this.isTest()
		let balances = {}
		if (isMargin) {
			const data = await this.req('POST', "/TradeBalance", {asset: currency})
			if (data) {
				balances[currency] = {
					available: data.mf,
					balance: data.e
				}
			}
		} else {
			const data = await this.req('POST', "/BalanceEx")
			Object.each(data, (asset, info) => {
				const amount = Number(info.balance)
				const avail = amount - Number(info.hold_trade)
				if (amount || asset == currency) {
					balances[asset] = {
						available: avail,
						balance: amount
					}
				}
			})
		}
		isTest && currency && (balances[currency] = balances[currency] || {available: 0, balance: 0})
		return balances
	}

	async symbolInfoImpl(cache) {
		const response = await this.req('GET', "/AssetPairs")
		for (const symbol in response) {
			const info = response[symbol]
			info.minLeverage = Math.min(info.leverage_buy?.[0] || 2, info.leverage_sell?.[0] || 2)
			info.maxLeverage = Math.max(info.leverage_buy?.last() || 10, info.leverage_sell?.last() || 10)
			cache[(info.symbol = this.sym(symbol))] = info
			cache[this.sym(info.altname)] = info
		}
	}

	async symbolTickerImpl(symbol) {
		let ticker = await this.req('GET', "/Ticker", {pair: symbol})
		ticker = ticker && ticker[symbol]

		return ticker && {
			ask: Number(ticker.a[0]),
			bid: Number(ticker.b[0]),
			mid: (Number(ticker.a[0]) + Number(ticker.b[0])) / 2,
			last: Number(ticker.c[0])
		}
	}

	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.currency = cmd.isBid ? market.quote : market.base
		if (cmd.isMargin) cmd.currency += marginSuffix
		let balance = {available: 0, balance: 0}
		if ((cmd.y !== "possize" && 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]
			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)
		let price = cmd.fp ? cmd.fp.resolve(market.pair_decimals) : cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(market.pair_decimals)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated)")
		}

		let leverage = 1
		if (cmd.isMargin) {
			leverage = (cmd.l * (this.ohlc(cmd, ohlc, 'lr') || 1)) || 2
			if (leverage < market.minLeverage) {
				leverage = market.minLeverage
				this.warn(`leverage adjusted to instrument minimum of ${leverage}x!`)
			} else if (leverage > market.maxLeverage) {
				leverage = market.maxLeverage
				this.warn(`leverage limited to instrument maximum of ${leverage}x!`)
			}
		}
		let available = ohlc[cmd.y] || (cmd.y === "equity" ? balance.balance : balance.available) * (cmd.lc || leverage)
		if (cmd.y !== "possize" && cmd.isBid) {
			available /= price
		}

		let order = {
			pair: cmd._sym,
			ordertype: cmd.t === "market" ? "market" : "limit",
			type: cmd.isBid ? "buy" : "sell",
			volume: cmd.q._().div(cmd.u === "currency" && !cmd.q.wasPercent() ? price : 1)
				.reference(available).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc)).minmax(cmd.minq, cmd.maxq).resolve(market.lot_decimals),
			trading_agreement: "agree"
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.volume = ohlc.left.toFixed(market.lot_decimals)
		}
		if (order.volume <= 0) this.warn("your q parameter might be incorrect! (quantity of 0 calculated)")
		if (cmd.t !== "market") order.price = price
		if (cmd.t === "post") order.oflags = "post"
		if (cmd.r) order.reduce_only = true
		if (cmd.isMargin) cmd._lev = order.leverage = leverage

		if (cmd.sl && cmd.tp) {
			order.ordertype = "stop-loss-profit" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.pair_decimals)
			order.price2 = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pair_decimals)
		} else if (cmd.sl) {
			order.ordertype = "stop-loss" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.pair_decimals)
 			cmd.t !== "market" && (order.price2 = price)
		} else if (cmd.tp) {
			order.ordertype = "take-profit" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pair_decimals)
 			cmd.t !== "market" && (order.price2 = price)
		} else if (cmd.ts) {
			order.ordertype = "trailing-stop" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.ts._().relative(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || first).resolve(market.pair_decimals)
 			cmd.t !== "market" && (order.price2 = price)
		}

		this.info(`placing ${currency} ${cmd.isMargin?'margin ':''}order${cmd.isMargin?` @ ${leverage}x`:''}: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		const result = await this.req('POST', "/AddOrder", order)
		cmd.id && result?.txid?.forEach(txid => this.cacheId(txid, cmd.id))
		return result
	}

	async ordersCancel(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.symbol

		let orders = await this.req('POST', "/OpenOrders")
		if (!orders || !orders.open) {
			this.msgOrderErr()
			return false
		}
		orders = Object.filterAsArray(orders.open, (txid, order) => {
			if (order.status !== "open" && order.status !== "pending") {
				return false
			}
			if (order.descr.pair !== market.altname) {
				return false
			}
			order = Object.assign(order, order.descr)
			delete order.descr
			order.txid = txid
			order.leverage = parseInt(order.leverage) || 0
			return true
		})
		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.b) {
				if (order.type !== (cmd.isBid ? 'buy' : 'sell')) {
					return false
				}
				if (!cmd.isMargin != !order.margin) {
					return false
				}
			}
			if (cmd.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if (cmd.l && order.leverage != cmd.l) {
				return !match
			}
			if (cmd.r !== undefined && !cmd.r != !order.reduceOnly) {
				return !match
			}
			if (cmd.id && !this.checkId(order.txid, ids)) {
				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": 	["opentm", true],
				"oldest": 	["opentm", false],
				"highest": 	[["descr", "price"], true],
				"lowest": 	[["descr", "price"], false],
				"biggest": 	["vol", true],
				"smallest": ["vol", 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 response = await this.req('POST', "/CancelOrder", {txid: order.txid})
			if (response?.count) {
				cancelOrders.push(order)
				this.removeId(order.txid)
			}
		}
		return cancelOrders
	}

	async positionsClose(cmd, pos, i, ohlc = {}, market) {
		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!`)
		}
		ticker.pos = pos.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.pair_decimals)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated)")
		}

		let order = {
			pair: pos.pair,
			type: pos.type === "buy" ? "sell" : "buy",
			ordertype: cmd.t === "market" ? "market" : "limit",
			volume: cmd.q._().div(cmd.u === "currency" && !cmd.q.wasPercent() ? price : 1)
				.reference(pos.quantity).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc, pos)).minmax(cmd.minq, cmd.maxq).resolve(market.lot_decimals),
			leverage: cmd.l || pos.leverage || 2,
			trading_agreement: "agree",
			reduce_only: cmd.r === undefined ? true : cmd.r
		}
		if (cmd.t !== "market") order.price = price
		if (cmd.t === "post") order.oflags = "post"

		if (cmd.sl && cmd.tp) {
			order.ordertype = "stop-loss-profit" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.pair_decimals)
			order.price2 = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pair_decimals)
		} else if (cmd.sl) {
			order.ordertype = "stop-loss" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.pair_decimals)
			cmd.t !== "market" && (order.price2 = price)
 		} else if (cmd.tp) {
			order.ordertype = "take-profit" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pair_decimals)
			cmd.t !== "market" && (order.price2 = price)
		} else if (cmd.ts) {
			order.ordertype = "trailing-stop" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.ts._().relative(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || first).resolve(market.pair_decimals)
			cmd.t !== "market" && (order.price2 = price)
		}

		if (cmd.t === "settle") {
			order.ordertype = "settle-position"
			order.type = pos.type
			order.volume = 0
		} else if (order.volume <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated)")
		}

		!cmd.ch && this.info(`placing Close Position order @ ${order.leverage}x: `+stringify(order))
		if (cmd.ch || cmd.d) {
			this.msgDisabled(cmd)
			return order
		}

		const result = await this.req('POST', "/AddOrder", order)
		cmd.id && result?.txid?.forEach(txid => this.cacheId(txid, cmd.id))
		return result
	}

	async positionsCloseAll(cmd, ohlc, positions) {
		const symbols = await this.symbolInfo()
		if (!positions) {
			positions = await this.req('POST', "/OpenPositions", {docalcs: true})
			if (!positions) {
				this.msgPosErr()
				return false
			}
			const symbol = cmd.s && cmd.s !== '*' && symbols[cmd.s] && symbols[cmd.s].symbol
			positions = Object.filterAsArray(positions, (txid, pos) => {
				if (symbol && pos.pair !== symbol) {
					return false
				}
				pos.txid = txid
				pos.price = toPrecision(pos.cost / pos.vol, symbols[pos.pair] && symbols[pos.pair].pair_decimals || 4)
				pos.quantity = pos.vol - pos.vol_closed
				pos.leverage = Math.round(pos.cost / pos.margin)
				pos.pnl = pos.net / pos.margin / pos.leverage
				pos.time *= 1000
				return true
			})
			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.type !== 'buy') || (cmd.isAsk && pos.type !== 'sell')) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(pos.price)) {
				return !match
			}
			if (cmd.l && pos.leverage !== cmd.l) {
				return !match
			}
			const market = symbols[pos.pair]
			cmd._sym = market.symbol
			pos.currency = cmd.currency = market.quote + marginSuffix
			pos._sizeUSD = Number(pos.margin)
			pos._sizeCoin = pos._sizeUSD / pos.price
			if (!this.filterPos(cmd, ohlc, tickers, pos, total => total * pos.leverage / pos.price, market.lot_decimals)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(positions.length).getMax() < positions.length) {
			const sort = {
				"newest": 	["time", true],
				"oldest": 	["time", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["volume", true],
				"smallest": ["volume", 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.info(`${positions.length} position${positions.length > 1 ?'s':''} found!`)
			return positions
		}

		let closeOrders = [], i = 0
		for (const pos of positions) {
			const order = await this.positionsClose(cmd, pos, i++, ohlc, symbols[pos.pair])
			if (order) {
				closeOrders.push(order)
				Object.assign(closeOrders.last(), Object.without(pos, ["price", "type", "ordertype", "txid"]))
			}
		}
		return closeOrders
	}
}

Broker.add(new Kraken())
