"use strict"

class CoinbasePro extends Exchange {
	static meta = {
		aliases: [
			"COINBASEPRO",
			"GDAX",
			"COINBASE-PRO",
//			"COINBASE",
		],
		endpoint: "https://api.pro.coinbase.com",
		fields: {
			public: {
				label: "API Key",
			},
			private: {
				label: "API Secret",
			},
			password: {
				label: "API Passphrase",
			},
		},
		name: "Coinbase Pro",
		patterns: [],
		permissions: {
			origins: [
				"https://api.pro.coinbase.com/*",
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"coinp1",
				"coinp2",
			],
		},
		recvWindow: 30 * 1000,
		website: "https://pro.coinbase.com",
		desc: "US based digital asset exchange with trading FIX API and REST API. Easy to deposit funds with Coinbase wallet, bank transfer, wire transfer, or digital currency.",
		testSymbol: "ETHBTC"
	}
	constructor() {
		super(CoinbasePro.meta)
	}

	*req(method, resource, params, sign) {
		if (method === 'GET' && params) {
			resource += '?' + serialize(params)
			params = null
		}
		params = params ? JSON.stringify(params) : ''

		const headers = {"Content-Type": "application/json"}
		if (sign) {
			const credentials = this.getCredentials("private")
			const timestamp = this.getTimestamp() / 1000
			const payload = timestamp + method + resource + params

			let sha = new jsSHA("SHA-256", "TEXT")
			sha.setHMACKey(credentials.private, "B64")
			sha.update(payload)

			headers["CB-ACCESS-KEY"] = credentials.public
			headers["CB-ACCESS-TIMESTAMP"] = timestamp
			headers["CB-ACCESS-PASSPHRASE"] = credentials.password
			headers["CB-ACCESS-SIGN"] = sha.getHMAC("B64")
		} else this.hasAccess()

		let response = yield request.bind(this, method, this.meta.endpoint + resource, params, headers)
		if (!response && method === 'GET') {
			response = yield request.bind(this, method, this.meta.endpoint + resource, params, headers)
		}
		if (response && response.message) {
			throw new Error(response.message)
		}
		return response
	}

	*time() {
		const result = yield* this.req('GET', "/time")
		return result.epoch * 1000
	}

	*account(currency) {
		const data = yield* this.req('GET', "/accounts", null, true)
		let balances = {}

		data.forEach(item => {
			const balance = Number(item.balance)

			if (balance || item.currency === currency) {
				balances[item.currency] = {
					available: Number(item.available),
					balance: balance
				}
			}
		})
		return balances
	}

	*symbolInfo(symbol) {
		const symbolCache = this.getSymbolCache()
		if (symbolCache._refresh) {
			const response = yield* this.req('GET', "/products")

			response.forEach(info => {
				info.pricePrecision = decimals(info.quote_increment)
				info.quantityPrecision = decimals(info.base_increment)
				symbolCache[info.id.replace('-', '')] = 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]
	}

	*symbolTicker(symbol) {
		const ticker = yield* this.req('GET', `/products/${symbol}/ticker`)
		return ticker && {
			ask: Number(ticker.ask),
			bid: Number(ticker.bid),
			mid: (Number(ticker.ask) + Number(ticker.bid)) / 2,
			last: Number(ticker.price)
		}
	}

	*trade(cmd, ohlc = {}) {
		if (cmd.isMargin) {
			throw new Error(`${this.getName()} does not support Margin trading!`)
		}
		const market = yield* this.symbolInfo(cmd.s)
		cmd._sym = market.id
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.id)) || (yield* this.symbolTicker(market.id))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.id} is not available!`)
		}
		this.checkSlip(cmd, ohlc, market.id, ticker)
		if (cmd.up) {
			this.updatePrice(market.id, ticker)
			if (!cmd.b && !cmd.ub) {
				return ticker
			}
		}

		const currency = cmd._currency = cmd.isBid ? market.quote_currency : market.base_currency
		let balance = {available: 0, balance: 0}
		if (cmd.q.wasPercent() || cmd.ub) {
			balance = cmd.bc && !cmd.ub && this.getBalance(currency)
			const balances = !balance && (yield* this.account(currency))
			balance = balance || balances[currency]
			this.checkBalance(cmd, balance, currency)
			if (!cmd.bc || cmd.ub) {
				this.updateBalance(balances, 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.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 (cmd.t !== "post") {
			available *= (1 - 0.0050)
		}
		if (cmd.isBid) {
			available /= price
		}

		ohlc[cmd.qr] && this.info(`using custom quantity multiplier: ${ohlc[cmd.qr]} (${cmd.qr})`)
		let order = {
			product_id: market.id,
			side: cmd.isBid ? "buy" : "sell",
			size: cmd.q._().div(cmd.u === "currency" && !cmd.q.wasPercent() ? price : 1)
				.reference(available).mul(ohlc[cmd.qr] || 1).minmax(cmd.minq, cmd.maxq).resolve(market.quantityPrecision),
			type: cmd.t === "market" ? "market" : "limit",
			time_in_force: ({fok: "FOK", ioc: "IOC"})[cmd.t] || "GTC"
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.size = ohlc.left.toFixed(market.quantityPrecision)
		}
		if (market.base_min_size && order.size < market.base_min_size) {
			this.warn(`order quantity below instrument minimum of ${market.base_min_size} – use minq= to set minimum!`)
		} else if (market.base_max_size && order.size > market.base_max_size) {
			this.warn(`order quantity above instrument maximum of ${market.base_max_size} – use maxq= to set maximum!`)
		} else if (order.size <= 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.post_only = true
		}
		if (cmd.tp || cmd.sl || cmd.so) {
			const ref = (cmd.tp && cmd.tpref) || (cmd.sl && cmd.slref) || (cmd.so && cmd.soref)
			ref && ref !== "price" && this.info(`using custom stop reference: ${ohlc[ref] || ticker[ref] || "NOT FOUND!"} (${ref})`)
			ref !== "price" && (price = (ref && (ohlc[ref] || ticker[ref])) || first)
			order.stop = cmd.isBid ? "entry" : "loss"
			order.stop_price = (cmd.tp || cmd.sl || cmd.so)._().relative(price).resolve(market.pricePrecision)
		}

		this.info("placing order: "+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		const result = yield* this.req('POST', "/orders", order, true)
		cmd.id && result && result.id && this.cacheId(result.id, cmd.id)
		return result
	}

	*ordersCancel(cmd, ohlc) {
		const market = yield* this.symbolInfo(cmd.s)
		cmd._sym = market.id
		let orders = yield* this.req('GET', "/orders", {product_id: market.id}, true)
		if (!orders || !orders.filter) {
			this.msgOrderErr()
			return false
		} else if (!orders.length) {
			this.msgOrdersNone()
			return false
		}

		const ids = (cmd.id || "").split(',')
		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.b && ((cmd.isAsk && order.side !== "sell") || (cmd.isBid && order.side !== "buy"))) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if (hasType && !order.type.includes(cmd.t)) {
				return !match
			}
			if ((cmd.t === "open" && order.stop.length) || (cmd.t === "close" && !order.stop.length)) {
				return !match
			}
			if (cmd.id && !this.checkId(order.id, 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": 	["created_at", true],
				"oldest": 	["created_at", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["size", true],
				"smallest": ["size", 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) {
			if (yield* this.req('DELETE', "/orders/" + order.id, null, true)) {
				cancelOrders.push(order)
				this.removeId(order.id)
			}
		}
		return cancelOrders
	}

	*positionsCloseAll(cmd) {
		throw new ReferenceError(`${this.getName()} does not support Margin trading!`)
	}
}

Broker.add(new CoinbasePro())
