"use strict"

class OANDASandbox extends Exchange {
	static meta = {
		aliases: [
			"OANDASANDBOX",
			"OANDA-SANDBOX",
		],
		endpoint: "https://api-fxpractice.oanda.com",
		fields: {
			public: {
				label: "Account Number",
				type: "text",
			},
			private: {
				label: "Auth Token",
			},
		},
		name: "OANDA Sandbox",
		permissions: {
			origins: [
				"https://api-fxpractice.oanda.com/*",
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		},
		version: "v3",
		website: "https://www.oanda.com/",
		desc: "Award-winning leader in Currency Data, Forex & CFD Trading, offering leveraged trading, payment and data services for businesses and investors.",
		defaultSymbol: "USD",
		testSymbol: "USD_CHF",
		symbolAuth: true, margin: true, stopsOnFill: true, reducePosOnly: true,
	}
	constructor(meta = OANDASandbox.meta) {
		super(meta)
	}

	async req(method, resource = "", params) {
		const credentials = this.getCredentials("private")
		const headers = {
			"Accept-Datetime-Format": "UNIX",
			"Authorization": "Bearer " + credentials.private,
			"Content-Type": "application/json"
		}
		resource = "/" + this.meta.version + "/accounts/" + credentials.public + resource
		if (params && method === 'GET') {
			resource += '?'+serialize(params).replace(/%20/g, '+')
			params = null
		}
		params = params ? JSON.stringify(params) : ''

		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.errorMessage) {
			throw new Error(response.errorMessage + (response.errorCode ? ` (${response.errorCode})` : ''))
		}
		return response
	}

	async account() {
		const account = (await this.req('GET')).account || {}

		return {
			available: Number(account.marginAvailable || 0),
			balance: Number(account.balance || 0),
			currency: account.currency,
			leverage: Math.round(1 / Number(account.marginRate)),
			isHedge: account.hedgingEnabled
		}
	}

	async symbolInfoImpl(cache) {
		const response = await this.req('GET', "/instruments")
		response?.instruments?.forEach(info => {
			info.leverage = Math.round(1 / Number(info.marginRate || 0.01))
			cache[this.sym(info.name)] = info
			cache[this.sym(info.displayName)] = info
		})
	}

	async symbolTickerImpl(symbol) {
		let ticker = await this.req('GET', "/pricing", {instruments: symbol})
		ticker = ticker?.prices?.[0]

		return ticker && {
			ask: Number(ticker.asks[0].price),
			bid: Number(ticker.bids[0].price),
			mid: (Number(ticker.asks[0].price) + Number(ticker.bids[0].price)) / 2
		}
	}

	async convert(from, to, useCache) {
		const symbols = this.getSymbolCache()
		let inv = false
		const direct = symbols[from + to] || ((inv = true) && symbols[to + from])
		const directTicker = direct && ((useCache && this.getPrice(direct.name)) || (await this.symbolTicker(!useCache, direct.name)))
		if (directTicker) {
			return inv ? 1 / directTicker.mid : directTicker.mid
		}

		inv = false
		const toUSD = symbols[from + 'USD'] || ((inv = true) && symbols['USD' + from])
		const toUSDTicker = toUSD && ((useCache && this.getPrice(toUSD.name)) || (await this.symbolTicker(!useCache, toUSD.name)))
		if (!toUSDTicker) {
			return false
		}
		let conv = inv ? 1 / toUSDTicker.mid : toUSDTicker.mid

		inv = false
		const fromUSD = symbols[to + 'USD'] || ((inv = true) && symbols['USD' + to])
		const fromUSDTicker = fromUSD && ((useCache && this.getPrice(fromUSD.name)) || (await this.symbolTicker(!useCache, fromUSD.name)))
		if (!fromUSDTicker) {
			return false
		}
		conv *= inv ? fromUSDTicker.mid : 1 / fromUSDTicker.mid
		return conv
	}

	async trade(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.name
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.name)) || (await this.symbolTicker(cmd.up, market.name))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.name} is not available!`)
		}
		this.checkSlip(cmd, ohlc, market.name, ticker)
		if (cmd.up) {
			this.updatePrice(market.name, 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() || (await this.account())
			this.checkBalance(cmd, balance, balance.currency)
			if (!cmd.bc || cmd.ub) {
				this.updateBalance(balance, balance.currency)
				if (cmd.ub) {
					return Object.assign(ticker, balance)
				}
			}
		}

		const precision = market.displayPrecision || 5
		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(precision) : cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(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) || balance.leverage || (this.getBalance("", false, true) || {}).leverage || 1
		if (leverage > market.leverage) {
			cmd.l && (cf = market.leverage / leverage)
			leverage = cmd._lev = market.leverage
			this.warn(`leverage limited to ${leverage}x by instrument!`)
		}
		const available = cmd.y === "equity" ? balance.balance : balance.available
		let quantity = ohlc[cmd.y] || available / price * (cmd.lc || leverage) * cf
		const baseCurrency = cmd.s.slice(-3)
		if (cmd.y !== "possize" && cmd.q.wasPercent() && balance.currency !== baseCurrency) {
			const conv = await this.convert(balance.currency, baseCurrency, cmd.pc && !cmd.up)
			if (conv && conv != 1) {
				this.info(`converting account currency ${balance.currency} to symbol base currency ${baseCurrency}: ${toPrecision(conv, 6)} (factor)`)
				quantity *= conv
				this.setBalanceRaw(balance.currency, setObject(baseCurrency, conv))
			} else {
				this.warn(`could not convert account currency ${balance.currency} to symbol base currency ${baseCurrency}, please use absolute units for buy in!`)
			}
		}

		quantity = cmd.q._().mul(cmd.u === "currency" && !cmd.q.wasPercent() ? (cmd.lc || leverage) * cf / price : 1)
			.reference(quantity).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc)).minmax(cmd.minq, cmd.maxq).resolve(market.tradeUnitsPrecision || 0)
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			quantity = ohlc.left.toFixed(market.tradeUnitsPrecision || 0)
		}
		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 = {
			instrument: market.name,
			positionFill: cmd.r ? "REDUCE_ONLY" : "DEFAULT",
			triggerCondition: ({mid: "MID", bid: "BID", ask: "ASK"})[cmd.pr] || "DEFAULT",
			units: `${cmd.isAsk ? '-' : ''}${quantity}`,
			type: cmd.t === "market" ? "MARKET" : "LIMIT",
			timeInForce: ({fok: "FOK", ioc: "IOC", day: "GFD", market: "FOK"})[cmd.t] || "GTC"
		}
		if (cmd.t !== "market") order.price = price
		if (cmd.pb) order.priceBound = cmd.pb._().relative(first).resolve(precision)
/*		if (cmd.gtd) {
			order.timeInForce = "GTD"
			order.gtdTime = cmd.gtd
		}*/

		if (cmd.so) {
			order.price = cmd.so._().relative(this.ohlc(cmd, ohlc, 'soref', ticker, price) || first).resolve(precision)
			order.type = "STOP"
		}
		if (cmd.sl) {
			order[cmd.gsl ? 'guaranteedStopLossOnFill' : 'stopLossOnFill'] = {price: cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(precision)}
			if (cmd.id) {
				order[cmd.gsl ? 'guaranteedStopLossOnFill' : 'stopLossOnFill'].clientExtensions = {id: this.uniqueID(cmd.id + 'SL')}
			}
		}
		if (cmd.tp) {
			order.takeProfitOnFill = {price: cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(precision)}
			if (cmd.id) {
				order.takeProfitOnFill.clientExtensions = {id: this.uniqueID(cmd.id + 'TP')}
			}
		}
		if (cmd.ts) {
			order.trailingStopLossOnFill = {distance: cmd.ts._().reference(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || first).abs().resolve(precision)}
			if (cmd.id) {
				order.trailingStopLossOnFill.clientExtensions = {id: this.uniqueID(cmd.id + 'TS')}
			}
		}
		if (cmd.id) {
			order.clientExtensions = {id: this.uniqueID(cmd.id)}
		}

		this.info(`placing order @ ${leverage}x: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		try {
			if (cmd.l !== undefined) {
				this.info(`setting account leverage to ${leverage}x...`)
				await this.req('PATCH', "/configuration", {marginRate: (1 / leverage).toFixed(2)})
			}
		} catch(ex) {
			this.warn(`couldn't set leverage, please set manually! (Error: ${ex.message})`)
		}
		return await this.ordersPlace(order)
	}

	async ordersPlace(order) {
		const result = await this.req('POST', "/orders", {order: order})

		if (result.orderCancelTransaction) {
			throw new Error(result.orderCancelTransaction.reason || "Unknown reason")
		}
		return result.orderFillTransaction || result.orderCreateTransaction || result
	}

	async ordersCancel(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.name
		let orders = ((await this.req('GET', "/orders", {
			state: "PENDING",
			instrument: market.name,
			count: 500
		})) || {}).orders

		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.units < 0) || (cmd.isAsk && order.units > 0)) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if ((cmd.t === "open" && order.type !== "LIMIT") || (cmd.t === "close" && order.type === "LIMIT")) {
				return !match
			}
			if ((cmd.tp && order.type !== "TAKE_PROFIT") || (cmd.sl && order.type !== "STOP_LOSS") || (cmd.so && order.type !== "STOP") || (cmd.ts && order.type !== "TRAILING_STOP_LOSS")) {
				return !match
			}
			if (cmd.r !== undefined && !cmd.r != (order.positionFill != "REDUCE_ONLY")) {
				return !match
			}
			if (cmd.id && (!order.clientExtensions || !order.clientExtensions.id || !order.clientExtensions.id.startsWithAny(ids, true))) {
				return !match
			}
			order.side = order.units < 0 ? "SELL" : "BUY"
			if (!this.filterOrder(cmd, ohlc, tickers, order)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(orders.length).getMax() < orders.length) {
			const sort = {
				"newest": 	["createTime", true],
				"oldest": 	["createTime", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["units", true],
				"smallest": ["units", 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('PUT', `/orders/${order.id}/cancel`)
			if (result?.orderCancelTransaction?.orderID === order.id) {
				cancelOrders.push(order)
			}
		}
		return cancelOrders
	}

	async positionsClose(cmd, pos, i, ohlc = {}, market) {
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.name)) || (await this.symbolTicker(cmd.up, market.name))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.name} is not available!`)
		}
		const precision = market.displayPrecision || 5
		ticker.pos = pos.averagePrice
		this.checkSlip(cmd, ohlc, market.name, ticker)
		cmd.up && this.updatePrice(market.name, 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(precision)
		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 quantity = cmd.q._().mul(cmd.u === "currency" && !cmd.q.wasPercent() ? (cmd.lc || leverage) / 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.tradeUnitsPrecision || 0)
		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)")
		}

		if (cmd.so || cmd.sl || cmd.tp || cmd.ts || (cmd.t === "market" && cmd.r !== false && quantity >= pos.quantity)/* || cmd.fid*/ || cmd.ft) {
			let trades = pos.tradeIDs
			if (/*cmd.fid || */cmd.ft) {
				if (/*cmd.fid || */["highest", "lowest", "biggest", "smallest"].includes(cmd.cmo)) {
					let open = await this.req('GET', "/openTrades")
					open = open?.trades?.filter(trade => trades.includes(trade.id))
					if (open?.length) {
						const sort = {
							"highest": 	["price", true],
							"lowest": 	["price", false],
							"biggest": 	["currentUnits", true],
							"smallest": ["currentUnits", false]
						}
						sortByIndex(open, sort[cmd.cmo][0], sort[cmd.cmo][1])
						trades = open.map(trade => trade.id)
					} else {
						this.msgPosErr("trades")
					}
				} else if (cmd.cmo === 'newest') {
					trades = trades.reverse()
				} else if (cmd.cmo === 'shuffle') {
					shuffle(trades)
				}
				if (cmd.cm._().reference(trades.length).getMax() < trades.length) {
					trades = trades.slice(0, cmd.cm.resolve(0))
				}
			}

			let closeOrders = []
			for (const trade of trades) {
				let order = {tradeID: trade}

				if (cmd.so) {
					order.type = "STOP"
					order.price = cmd.so._().relative(this.ohlc(cmd, ohlc, 'soref', ticker, price) || first).resolve(precision)
				} else if (cmd.sl) {
					order.type = cmd.gsl ? "GUARANTEED_STOP_LOSS" : "STOP_LOSS"
					order.price = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(precision)
				} else if (cmd.tp) {
					order.type = "TAKE_PROFIT"
					order.price = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(precision)
				} else if (cmd.ts) {
					order.type = "TRAILING_STOP_LOSS"
					order.distance = cmd.ts._().reference(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || first).abs().resolve(precision)
				} else {
					order.units = "ALL"
				}
				if (cmd.pb) order.priceBound = cmd.pb._().relative(first).resolve(precision)
/*				if (cmd.gtd) {
					order.timeInForce = "GTD"
					order.gtdTime = cmd.gtd
				}*/
				if (cmd.id) {
					order.clientExtensions = {id: this.uniqueID(cmd.id, i, closeOrders.length)}
				}
				closeOrders.push(order)
			}

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

			let results = []
			for (const order of closeOrders) {
				if (order.units) {
					const result = await this.req('PUT', `/trades/${order.tradeID}/close`, order)
					results.push(result.orderFillTransaction || result.orderCreateTransaction || result)
				} else {
					results.push(await this.ordersPlace(order))
				}
			}
			return results
		}

		let order = {
			instrument: market.name,
			positionFill: cmd.r !== false ? "REDUCE_ONLY" : "REDUCE_FIRST",
			triggerCondition: ({mid: "MID", bid: "BID", ask: "ASK"})[cmd.pr] || "DEFAULT",
			units: `${pos.units > 0 ? '-' : ''}${quantity}`,
			type: cmd.t === "market" ? "MARKET" : "LIMIT",
			timeInForce: ({fok: "FOK", ioc: "IOC", day: "GFD", market: "FOK"})[cmd.t] || "GTC"
		}
		if (cmd.t !== "market") order.price = price
		if (cmd.pb) order.priceBound = cmd.pb._().relative(first).resolve(precision)
/*		if (cmd.gtd) {
			order.timeInForce = "GTD"
			order.gtdTime = cmd.gtd
		}*/
		if (cmd.id) {
			order.clientExtensions = {id: this.uniqueID(cmd.id, i)}
		}

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

	async positionsCloseAll(cmd, ohlc, positions) {
		if (!positions) {
			let rawPos = await this.req('GET', "/openPositions")
			rawPos = rawPos?.positions
			if (!rawPos?.filter) {
				this.msgPosErr()
				return false
			}
			positions = []
			const symbol = cmd.s && cmd.s !== '*' && (await this.symbolInfo(cmd.s)).name
			for (const pos of rawPos) {
				pos.long.units = Number(pos.long.units)
				pos.short.units = Number(pos.short.units)
				if (!symbol || pos.instrument === symbol) {
					pos.marginUsed = Number(pos.marginUsed)
					pos.value = Math.max(pos.long.units * (pos.long.averagePrice || 1), Math.abs(pos.short.units) * (pos.short.averagePrice || 1))
					pos.isHedge = (pos.long.units || Number(pos.long.pl) || Number(pos.long.resettablePL)) != 0 && (pos.short.units || Number(pos.short.pl) || Number(pos.short.resettablePL)) != 0
					pos.long.units && positions.push(Object.assign({}, pos, pos.long, {long: null, short: null}, pos.isHedge && {positionSide: 'long'}))
					pos.short.units && positions.push(Object.assign({}, pos, pos.short, {long: null, short: null}, pos.isHedge && {positionSide: 'short'}))
				}
			}
			if (!positions.length) {
				this.msgPosNone()
				return false
			}
		}

		const symbols = await this.symbolInfo()
		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.units < 0) || (cmd.isAsk && pos.units > 0)) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(pos.averagePrice)) {
				return !match
			}
			pos.symbol = this.sym(pos.instrument)
			const market = symbols[pos.symbol] || {}
			pos._sym = cmd._sym = market.name
			// $$$$ check pos monitor => balance.lastSymbol ???
			pos.currency = cmd.currency = ""
			const balance = this.getBalance(cmd.currency, false, true) || {}
			const baseCurrency = pos.symbol.slice(-3)
			const conv = balance[baseCurrency] || 1
			pos.side = pos.units > 0 ? 'long' : 'short'
			pos.quantity = Math.abs(pos.units)
			pos._sizeUSD = pos.marginUsed
			pos.leverage = pos.value / conv / pos._sizeUSD
			const sideShift = pos.value / pos.averagePrice / pos.quantity
			pos.pnl = pos.unrealizedPL / pos._sizeUSD / pos.leverage
			pos.markPrice = pos.markPrice || toPrecision(pos.averagePrice * (1 + pos.pnl), market.tradeUnitsPrecision)
			pos.leverage = Math.round(pos.leverage)
			if (!this.filterPos(cmd, ohlc, tickers, pos, total => total / pos.averagePrice * conv * pos.leverage / sideShift, market.tradeUnitsPrecision)) {
				return !match
			}
			return match
		})

		if (!cmd.ft && cmd.cm._().reference(positions.length).getMax() < positions.length) {
			if (cmd.cmo === 'newest') {
				positions = positions.reverse()
			} else if (cmd.cmo !== 'oldest') {
				const sort = {
					"highest": 	["averagePrice", true],
					"lowest": 	["averagePrice", 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[pos.symbol])
			if (order) {
				Array.isArray(order) ? closeOrders.push(...order) : closeOrders.push(order)
				Object.assign(closeOrders.last(), Object.without(pos, ["units", "side"]))
			}
		}
		return closeOrders
	}
}

class OANDA extends OANDASandbox {
	static meta = Object.assign({}, super.meta, {
		aliases: [
			"OANDA",
		],
		endpoint: "https://api-fxtrade.oanda.com",
		name: "OANDA",
		permissions: {
			origins: [
				"https://api-fxtrade.oanda.com/*",
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"oanda1",
				"oanda2",
			],
		},
	})
	constructor(meta = OANDA.meta) {
		super(meta)
	}
}

Broker.add(new OANDA())
Broker.add(new OANDASandbox())
