"use strict"

class SimpleFX extends Exchange {
	static meta = {
		aliases: [
			"SIMPLEFX",
		],
		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",
		patterns: [],
		permissions: {
			origins: [
				"https://rest.simplefx.com/api/*",
				"https://simplefx.com/utils/instruments.json",
				"https://webquotes-v3.simplefx.com/*",
			],
			permissions: []
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"simpl1",
				"simpl2",
			],
		},
		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: "https://webquotes-v3.simplefx.com",
		margin: true, stopsOnFill: true, reducePosOnly: true,
		testSymbol: "BTCUSD"
	}
	constructor() {
		super(SimpleFX.meta)
	}

	*getAuth(credentials) {
		try {
			const auth = yield request.bind(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
			syncStore.updated()
		} catch(ex){
			if (ex && ex.message && ex.message.endsWith("CREDENTIALS")) {
				throw new Error("Invalid credentials provided – please check!")
			}
		}
	}

	*req(method, resource, params) {
		const credentials = this.getCredentials("private")
		if (!credentials.bearer) {
			yield* this.getAuth(credentials)
		}
		const headers = {"Content-Type": "application/json"}
		resource = this.meta.version + resource.replace(_("e3JlYWxpdHl9"), _("TElWRQ")).replace("{account}", credentials.login)

		if (params && method !== 'GET') {
			params[_("UmVhbGl0eQ")] = _("TElWRQ")
			params[_("TG9naW4")] = credentials.login
			params = JSON.stringify(params)
		}
		let response = null
		for (let retry=0; retry < 2; ++retry) {
			try {
				headers["Authorization"] = "Bearer " + credentials.bearer
				response = yield request.bind(this, method, this.meta.endpoint + resource, params, headers)
				break
			} catch(ex) {
				const msg = ex.message || ex.Message
				if (!retry && msg && msg.startsWith("Authorization")) {
					this.info("authorization token refresh...")
					yield* 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
	}

	*account() {
		const account = yield* 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
		}
	}

	*symbolInfo(symbol) {
		const symbolCache = this.getSymbolCache()
		if (symbolCache._refresh) {
			const response = yield request.bind(this, 'GET', this.meta.instrumentSource, null, null)

			for (const key in response) {
				const info = response[key]
				symbolCache[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]
	}

	connectQuotes(cb) {
		window.SIMPLEFX_CX || (window.SIMPLEFX_CX = $.hubConnection(this.meta.quoteSource))
		window.SIMPLEFX_HUBS || (window.SIMPLEFX_HUBS = window.SIMPLEFX_CX.createHubProxies())
		window.SIMPLEFX_CX.start()
			.catch(ex => {
				this.debug("Connection error: "+stringify(ex))
				cb(ex)
			}).done(e => {
				this.debug("Quote source connected!")
				cb(null, e)
			})
		this.debug(`Connecting to SignalR hub ${this.meta.quoteSource}...`)
	}

	getLastPrices(symbol, cb) {
		window.SIMPLEFX_HUBS && window.SIMPLEFX_HUBS.quotesSubscribeHub.server.getLastPrices([symbol])
			.then(msg => {
				this.debug("getLastPrices returned: "+stringify(msg))
				cb(null, msg)
			}).catch(ex => {
				this.debug("getLastPrices error: "+stringify(ex))
				cb(ex)
			})
		this.debug(`Invoking SignalR method: getLastPrices(${symbol})`)
	}

	*symbolTicker(symbol) {
		if (!window.SIMPLEFX_CX || window.SIMPLEFX_CX.state !== 1) {
			yield this.connectQuotes.bind(this)
		}
		let ticker = yield this.getLastPrices.bind(this, symbol)
		ticker = ticker && ticker.data[0]

		return ticker && {
			ask: ticker.a,
			bid: ticker.b,
			mid: (Number(ticker.a) + Number(ticker.b)) / 2
		}
	}

	*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)) || (yield* this.symbolTicker(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)) || (yield* this.symbolTicker(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)) || (yield* this.symbolTicker(fromUSD.symbol)))
		if (!fromUSDTicker) {
			return false
		}
		conv *= inv ? fromUSDTicker.mid : 1 / fromUSDTicker.mid
		return conv
	}

	*trade(cmd, ohlc = {}) {
		const market = yield* this.symbolInfo(cmd.s)
		const isIndex = market.marginMode == 4
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || (yield* this.symbolTicker(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() || (yield* 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] || ohlc[cmd.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 = yield* 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
					localStore.set('balances', this.getAlias(), this.getAccount().toUpperCase(), balance.currency, 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!`)
				}
			}
		}

		ohlc[cmd.qr] && this.info(`using custom quantity multiplier: ${ohlc[cmd.qr]} (${cmd.qr})`)
		quantity = cmd.q._().mul(cmd.u === "currency" && !cmd.q.wasPercent() ? balance.leverage / market.contractSize * market.marginDivider / price : 1)
			.reference(quantity).mul(ohlc[cmd.qr] || 1).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) {
			cmd.slref && cmd.slref !== "price" && this.info(`using custom SL reference: ${ohlc[cmd.slref] || ticker[cmd.slref] || "NOT FOUND!"} (${cmd.slref})`)
			const stop = cmd.slref === "price" ? price : ((cmd.slref && (ohlc[cmd.slref] || ticker[cmd.slref])) || first)
			order.StopLoss = cmd.sl._().relative(stop).resolve(market.digits)
		}
		if (cmd.tp) {
			cmd.tpref && cmd.tpref !== "price" && this.info(`using custom TP reference: ${ohlc[cmd.tpref] || ticker[cmd.tpref] || "NOT FOUND!"} (${cmd.tpref})`)
			const stop = cmd.tpref === "price" ? price : ((cmd.tpref && (ohlc[cmd.tpref] || ticker[cmd.tpref])) || first)
			order.TakeProfit = cmd.tp._().relative(stop).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 = yield* 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)
	}

	*ordersCancel(cmd, ohlc = {}) {
		const market = yield* this.symbolInfo(cmd.s)
		let orders = (yield* 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 = {}
		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.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 = yield* this.req('DELETE', "/trading/orders/pending", {Id: orders[i].id})
			if (order && order.pendingOrders && order.pendingOrders.last().order.id === orders[i].id) {
				cancelOrders.push(order.pendingOrders.last().order)
			}
		}
		return cancelOrders
	}

	*positionsClose(cmd, pos, i, ohlc = {}, market) {
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || (yield* this.symbolTicker(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] || ohlc[cmd.pr] || ((cmd.isBid && cmd.t !== "market") || (cmd.isAsk && cmd.t === "market") ? ticker.bid : ticker.ask)
		let price = cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(market.digits)
		if (cmd.fp) {
			price = cmd.fp.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) {
				cmd.slref && cmd.slref !== "price" && this.info(`using custom sl reference: ${ohlc[cmd.slref] || ticker[cmd.slref] || "NOT FOUND!"} (${cmd.slref})`)
				const stop = cmd.slref === "price" ? price : ((cmd.slref && (ohlc[cmd.slref] || ticker[cmd.slref])) || first)
				order.StopLoss = cmd.sl._().relative(stop).resolve(market.digits)
			}
			if (cmd.tp) {
				cmd.tpref && cmd.tpref !== "price" && this.info(`using custom TP reference: ${ohlc[cmd.tpref] || ticker[cmd.tpref] || "NOT FOUND!"} (${cmd.tpref})`)
				const stop = cmd.tpref === "price" ? price : ((cmd.tpref && (ohlc[cmd.tpref] || ticker[cmd.tpref])) || first)
				order.TakeProfit = cmd.tp._().relative(stop).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 = yield* this.req('PUT', "/trading/orders/market", order)
			return result.marketOrders && result.marketOrders.last().order
		}

		ohlc[cmd.qr] && this.info(`using custom quantity multiplier: ${ohlc[cmd.qr]} (${cmd.qr})`)
		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(ohlc[cmd.qr] || 1).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" ? (yield* this.req('DELETE', "/trading/orders/market", order)) : 
			(yield* 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)
	}

	*positionsCloseAll(cmd, ohlc, positions) {
		if (!positions) {
			positions = (yield* 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) && (yield* this.symbolTicker(pos.symbol))) || (cmd.pc && this.getPrice(pos.symbol))
			}
		}

		const balance = this.getBalance(undefined, undefined, true) || {}
		const symbols = yield* this.symbolInfo()
		const match = cmd.cr ? false : true
		yield* 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 = yield* 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
	}
}

Broker.add(new SimpleFX())
