"use strict"

class SimpleFXSandbox extends Exchange {
	static meta = {
		aliases: [
			"SIMPLEFXDEMO",
			"SIMPLEFXSANDBOX",
			"SIMPLEFX-SANDBOX",
			"SIMPLEFX-DEMO",
		],
		endpoint: "https://rest.simplefx.com/api/",
		fields: {
			public: {
				label: "Key",
			},
			private: {
				label: "Secret",
			},
			login: {
				label: "Account #",
				message: "Number of the SimpleFX account to be addressed through this PV account – found top right on the SimpleFX trading screen",
				type: "text",
			},
		},
		name: "SimpleFX Demo",
		permissions: {
			origins: [
				"https://rest.simplefx.com/api/*",
				"https://simplefx.com/utils/instruments.json",
				"wss://web-quotes-core.simplefx.com/websocket/quotes",
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		}, d: 'DEMO',
		version: "v3",
		website: "https://simplefx.com/n/l16_12088",
		desc: "Award-winning leader in Currency Data, Forex & CFD Trading, offering leveraged trading, payment and data services for businesses and investors.",
		defaultSymbol: "BTC",
		instrumentSource: "https://simplefx.com/utils/instruments.json",
		quoteSource: "wss://web-quotes-core.simplefx.com/websocket/quotes",
		margin: true, stopsOnFill: true, reducePosOnly: true,
		testSymbol: "BTCUSD"
	}
	constructor(meta = SimpleFXSandbox.meta) {
		super(meta)
	}

	async getAuth(credentials) {
		try {
			const auth = await request.call(this, 'POST', this.meta.endpoint + this.meta.version + "/auth/key", JSON.stringify({
				clientId: credentials.public, clientSecret: credentials.private
			}), {"Content-Type": "application/json"})
			credentials.bearer = auth.data.token
			// $$$$ api-fy
			(opt.accLocal ? localStore : syncStore).updated()
		} catch(ex) {
			if (ex?.message?.endsWith("CREDENTIALS")) {
				throw new Error("Invalid credentials provided – please check!")
			}
		}
	}

	async req(method, resource, params) {
		const credentials = this.getCredentials("private")
		if (!credentials.bearer) {
			await this.getAuth(credentials)
		}
		const headers = {"Content-Type": "application/json"}
		resource = this.meta.version + resource.replace('{reality}', this.meta.d).replace("{account}", credentials.login)

		if (params && method !== 'GET') {
			params['Reality'] = this.meta.d
			params['Login'] = credentials.login
			params = JSON.stringify(params)
		}
		let response = null
		for (let retry=0; retry < 2; ++retry) {
			try {
				headers["Authorization"] = "Bearer " + credentials.bearer
				response = await request.call(this, method, this.meta.endpoint + resource, params, headers)
				break
			} catch(ex) {
				const msg = ex.message || ex.Message
				if (!retry && msg?.startsWith("Authorization")) {
					this.info("authorization token refresh...")
					await this.getAuth(credentials)
				} else {
					throw new Error(msg)
				}
			}
		}
		if (!response) {
			throw new Error("Unknown connection error with API endpoint!")
		}
		const msg = response.message || response.Message
		if (!response.data || msg !== "OK") {
			throw new Error(msg || "Unknown connection error with API endpoint!")
		}
		return response.data
	}

	async account() {
		const account = await this.req('GET', "/accounts/{reality}/{account}")
		const marginConv = account.currency === "BIT" ? 1000000 : 1

		return {
			available: Number(account.freeMargin || 0) / marginConv,
			balance: Number(account.equity || 0) / marginConv,
			currency: account.currency === "BIT" ? "BTC" : (account.currency || this.meta.defaultSymbol),
			leverage: account.leverage || 1
		}
	}

	async symbolInfoImpl(cache) {
		const response = await request.call(this, 'GET', this.meta.instrumentSource, null, null)
		response?.forEach(info => cache[info.symbol] = info)
	}

	async symbolTickerImpl(symbol) {
		function connect(url) {
			return new Promise((res, rej) => {
				let socket = new WebSocket(url)
				socket.onopen = () => res(socket)
				socket.onerror = err => rej(err)
			})
		}
		function getLastPrices(symbols) {
			return new Promise((res, rej) => {
				const p = "/lastprices/list", i = 4
				globalThis.SIMPLEFX_WS.send(JSON.stringify({p, i, d: symbols}))
				globalThis.SIMPLEFX_WS.onmessage = ev => {
					const msg = JSON.parse(ev.data)
					if (msg.p === p) res(msg.d)
				}
			})
		}

		if (globalThis.SIMPLEFX_WS?.readyState !== 1) {
			this.debug(`Connecting to WebSocket ${this.meta.quoteSource}...`)
			globalThis.SIMPLEFX_WS = await connect(this.meta.quoteSource)
			this.debug("Quote source connected!")
		}
		this.debug(`Invoking WebSocket method: getLastPrices(${symbol})`)
		let ticker = await getLastPrices([symbol])
		ticker = ticker?.[0]

		return ticker && {
			ask: ticker.a,
			bid: ticker.b,
			mid: (Number(ticker.a) + Number(ticker.b)) / 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.symbol)) || (await this.symbolTicker(!useCache, direct.symbol)))
		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.symbol)) || (await this.symbolTicker(!useCache, toUSD.symbol)))
		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.symbol)) || (await this.symbolTicker(!useCache, fromUSD.symbol)))
		if (!fromUSDTicker) {
			return false
		}
		conv *= inv ? fromUSDTicker.mid : 1 / fromUSDTicker.mid
		return conv
	}

	async trade(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		const isIndex = market.marginMode == 4
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || (await 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() || (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 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.digits) : cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(market.digits)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated, use minp= to set minimum)")
		}

		const available = cmd.y === "equity" ? balance.balance : balance.available
		let quantity = ohlc[cmd.y] || available * balance.leverage / market.contractSize * market.marginDivider / (isIndex ? price : 1)

		if (cmd.y !== "possize" && cmd.q.wasPercent()) {
			quantity *= first / price
			if (balance.currency !== market.marginCurrency) {
				const conv = await this.convert(balance.currency, market.marginCurrency, cmd.pc && !cmd.up)
				if (conv && conv != 1) {
					this.info(`converting account currency ${balance.currency} to symbol margin currency ${market.marginCurrency}: ${toPrecision(conv, 6)} (factor)`)
					quantity *= conv
					this.setBalanceRaw(balance.currency, setObject(market.marginCurrency, conv))
				} else {
					this.warn(`could not convert account currency ${balance.currency} to symbol margin currency ${market.marginCurrency}, please use absolute units for buy in!`)
				}
			}
		}

		quantity = cmd.q._().mul(cmd.u === "currency" && !cmd.q.wasPercent() ? balance.leverage / market.contractSize * market.marginDivider / price : 1)
			.reference(quantity).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc)).minmax(cmd.minq, cmd.maxq).resolve(decimals(market.step || 0.01))
		if (quantity <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated, use minq= to set minimum)")
		}
		if (cmd.l) {
			this.warn("does not support setting leverage via API, please set manually!")
		}

		let order = {
			Symbol: market.symbol,
			Side: cmd.isBid ? "BUY" : "SELL",
			Volume: quantity,
			IsFIFO: cmd.fifo === false ? false : true
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.Volume = ohlc.left.toFixed(decimals(market.step || 0.01))
		}
		if (cmd.t !== "market") {
			order.ActivationPrice = price
		}

		if (cmd.sl) {
			order.StopLoss = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.digits)
		}
		if (cmd.tp) {
			order.TakeProfit = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.digits)
		}
		if (cmd.id) {
			order.Comment = this.uniqueID(cmd.id)
		}

		this.info(`placing order @ ${balance.leverage}x: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		const result = await this.req('POST', "/trading/orders/" + (cmd.t !== "market" ? "pending" : "market"), order)

		if (result.accountStatus) {
			const marginConv = balance.currency === "BTC" ? 1000000 : 1
			balance.available = Number(result.accountStatus.freeMargin) / marginConv
			balance.balance = Number(result.accountStatus.equity) / marginConv
			this.updateBalance(balance, balance.currency)
		}
		return (result.marketOrders && result.marketOrders.last().order) || (result.pendingOrders && result.pendingOrders.last().order)
	}

	async ordersCancel(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		let orders = (await this.req('POST', "/trading/orders/active", {})).pendingOrders

		if (!orders || !orders.filter) {
			this.msgOrderErr()
			return false
		}
		orders = orders.filter(order => order.symbol === market.symbol)
		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.side !== 'BUY') || (cmd.isAsk && order.side !== 'SELL')) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(order.activationPrice)) {
				return !match
			}
			if (cmd.id && (!order.comment || !order.comment.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": 	["createTime", true],
				"oldest": 	["createTime", false],
				"highest": 	["activationPrice", true],
				"lowest": 	["activationPrice", false],
				"biggest": 	["volume", true],
				"smallest": ["volume", false]
			}
			cmd.cmo == 'random' ? shuffle(orders) : sortByIndex(orders, sort[cmd.cmo][0], sort[cmd.cmo][1])
			orders = orders.slice(0, cmd.cm.resolve(0))
		}

		if(!orders.length) {
			this.msgOrders(cmd, orders, totalOrders)
			return []
		}

		this.msgOrders(cmd, orders, totalOrders)
		if (cmd.ch || cmd.d) {
			this.msgDisabled(cmd)
			return orders
		}

		let cancelOrders = []
		for (let i = 0; i < orders.length; i++) {
			const order = await this.req('DELETE', "/trading/orders/pending", {Id: orders[i].id})
			if (order?.pendingOrders?.last()?.order?.id === orders[i].id) {
				cancelOrders.push(order.pendingOrders.last().order)
			}
		}
		return cancelOrders
	}

	async positionsClose(cmd, pos, i, ohlc = {}, market) {
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || (await this.symbolTicker(cmd.up, market.symbol))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.symbol} is not available!`)
		}
		ticker.pos = pos.openPrice
		this.checkSlip(cmd, ohlc, market.symbol, ticker)
		cmd.up && this.updatePrice(market.symbol, 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.digits)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated, use minp= to set minimum)")
		}

		if (cmd.sl || cmd.tp) {
			let order = {Id: pos.id}
			if (cmd.sl) {
				order.StopLoss = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.digits)
			}
			if (cmd.tp) {
				order.TakeProfit = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.digits)
			}

			!cmd.ch && cmd.id && this.warn("does not support custom IDs for Trading Stop position attributes!")
			!cmd.ch && this.info(`placing SL/TP change order @ ${pos.leverage}x: `+stringify(order))
			if (cmd.ch || cmd.d) {
				this.msgDisabled(cmd)
				return order
			}

			const result = await this.req('PUT', "/trading/orders/market", order)
			return result.marketOrders && result.marketOrders.last().order
		}

		const quantity = cmd.q._().mul(cmd.u === "currency" && !cmd.q.wasPercent() ? (cmd.lc || pos.leverage) / market.contractSize * market.marginDivider / price : 1)
			.reference(pos.volume).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc, pos)).minmax(cmd.minq, cmd.maxq).resolve(decimals(market.step || 0.01))
		if (quantity <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated, use minq= to set minimum)")
		}

		let order = {Volume: quantity}
		if (cmd.t === "market") {
			order.Id = pos.id
			if (cmd.id) {
				order.RequestId = this.uniqueID(cmd.id, i)
			}
		} else {
			order.Symbol = market.symbol
			order.Side = pos.side === 'BUY' ? 'SELL' : 'BUY'
			order.IsFIFO = cmd.fifo === false ? false : true
			order.ActivationPrice = price
			if (cmd.id) {
				order.Comment = 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
		}
		const result = cmd.t === "market" ? (await this.req('DELETE', "/trading/orders/market", order)) : 
			(await this.req('POST', "/trading/orders/" + (cmd.t !== "market" ? "pending" : "market"), order))

		let balance
		if (result.accountStatus && (balance = this.getBalance(undefined, undefined, true))) {
			const marginConv = balance.currency === "BTC" ? 1000000 : 1
			balance.available = Number(result.accountStatus.freeMargin) / marginConv
			balance.balance = Number(result.accountStatus.equity) / marginConv
			this.updateBalance(balance, balance.currency)
		}
		return (result.marketOrders && result.marketOrders.last().order) || (result.pendingOrders && result.pendingOrders.last().order)
	}

	async positionsCloseAll(cmd, ohlc, positions) {
		if (!positions) {
			positions = (await this.req('POST', "/trading/orders/active", {})).marketOrders
			if (!positions || !positions.filter) {
				this.msgPosErr()
				return false
			}
			const symbol = cmd.s && cmd.s !== '*' && this.sym(cmd.s)
			positions = positions.filter(pos => (!symbol || this.sym(pos.symbol) === symbol) && pos.volume)
			if (!positions.length) {
				this.msgPosNone()
				return false
			}
		}

		const tickers = {}
		for (const pos of positions) {
			if (!tickers[pos.symbol]) {
				tickers[pos.symbol] = ((cmd.up || cmd.minpl || cmd.maxpl || !cmd.s) && (await this.symbolTicker(cmd.up, pos.symbol))) || (cmd.pc && this.getPrice(pos.symbol))
			}
		}

		const balance = this.getBalance(undefined, undefined, true) || {}
		const symbols = await this.symbolInfo()
		const match = cmd.cr ? false : true
		await this.filterPosStart(cmd, ohlc, tickers, positions)
		const totalPos = positions.length
		positions = positions.filter(pos => {
			if ((cmd.isBid && pos.side !== 'BUY') || (cmd.isAsk && pos.side !== 'SELL')) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(pos.openPrice)) {
				return !match
			}
			pos._sym = this.sym(pos.symbol)
			const market = symbols[pos._sym]
			const isIndex = market.marginMode == 4
			const conv = balance[market.marginCurrency] || 1
			const marginConv = balance.currency === "BTC" ? 1000000 : 1
			pos.currency = cmd.currency = balance.currency || ""
			pos.margin /= marginConv
			pos.profit /= marginConv
			pos.leverage = Math.round(pos.volume * market.contractSize / market.marginDivider / conv * (isIndex ? pos.openPrice : 1) / pos.margin)
			const ticker = tickers[pos.symbol]
			if (ticker) {
				pos.profit = (pos.side === 'BUY' ? ticker.bid - pos.openPrice : pos.openPrice - ticker.ask) / pos.openPrice * 
					pos.margin * pos.leverage * market.marginDivider
				pos.profit = toPrecision(pos.profit, 4)
				pos.markPrice = pos.side === 'BUY' ? ticker.bid : ticker.ask
			}
			pos.pnl = pos.profit / pos.margin / pos.leverage
			pos._sizeUSD = pos.margin
			if (!this.filterPos(cmd, ohlc, tickers, pos, volume => volume * pos.leverage / market.contractSize * market.marginDivider * conv / (isIndex ? pos.openPrice : 1), 
					decimals(market.step || 0.01))) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(positions.length).getMax() < positions.length) {
			const sort = {
				"newest": 	["openTime", true],
				"oldest": 	["openTime", false],
				"highest": 	["openPrice", true],
				"lowest": 	["openPrice", 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) {
			this.msgPos(cmd, positions, totalPos)
			return []
		} else if (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._sym])
			if (order) {
				closeOrders.push(order)
				Object.assign(closeOrders.last(), Object.without(pos, ["volume", "side"]))
			}
		}
		return closeOrders
	}
}

class SimpleFX extends SimpleFXSandbox {
	static meta = Object.assign({}, super.meta, {
		aliases: [
			"SIMPLEFX",
		],
		name: "SimpleFX",
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"simpl1",
				"simpl2",
			],
		}, d: 'LIVE',
	})
	constructor(meta = SimpleFX.meta) {
		super(meta)
	}
}

Broker.add(new SimpleFX())
Broker.add(new SimpleFXSandbox())
