"use strict"

class CoinbaseProSandbox extends Exchange {
	static meta = {
		aliases: [
			"COINBASEPROSANDBOX",
			"COINBASEPRO-SANDBOX",
			"GDAXSANDBOX",
			"GDAX-SANDBOX",
			"COINBASESANDBOX",
			"COINBASE-SANDBOX",
		],
		endpoint: "https://api-public.sandbox.pro.coinbase.com",
		fields: {
			public: {
				label: "API Key",
			},
			private: {
				label: "API Secret",
			},
			password: {
				label: "API Passphrase",
			},
		},
		name: "Coinbase Pro Sandbox",
		permissions: {
			origins: [
				"https://api-public.sandbox.pro.coinbase.com/*",
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		},
		recvWindow: 30 * 1000,
		website: "https://public-sandbox.exchange.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(meta = CoinbaseProSandbox.meta) {
		super(meta)
	}

	async 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
			headers["CB-ACCESS-KEY"] = credentials.public
			headers["CB-ACCESS-TIMESTAMP"] = timestamp
			headers["CB-ACCESS-PASSPHRASE"] = credentials.password
			headers["CB-ACCESS-SIGN"] = CryptoJS.HmacSHA256(payload, CryptoJS.enc.Base64.parse(credentials.private)).toString(CryptoJS.enc.Base64)
		} else this.hasAccess()

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

	async time() {
		return (await request.call(this, 'GET', this.meta.endpoint + "/time", null, null)).epoch * 1000
	}

	async account(currency) {
		const data = await 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
	}

	async symbolInfoImpl(cache) {
		const response = await this.req('GET', "/products")
		response?.forEach(info => {
			info.pricePrecision = decimals(info.quote_increment)
			info.quantityPrecision = decimals(info.base_increment)
			cache[this.sym(info.id)] = info
		})
	}

	async symbolTickerImpl(symbol) {
		const ticker = await 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)
		}
	}

	async trade(cmd, ohlc = {}) {
		if (cmd.isMargin) {
			throw new Error(`${this.getName()} does not support Margin trading!`)
		}
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.id
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.id)) || (await this.symbolTicker(cmd.up, 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 && (await 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] || 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.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.isBid) {
			available /= price
		}

		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(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc)).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 param = cmd.tp && 'tpref' || cmd.sl && 'slref' || cmd.so && 'soref'
			order.stop = cmd.isBid ? "entry" : "loss"
			order.stop_price = (cmd.tp || cmd.sl || cmd.so)._().relative(this.ohlc(cmd, ohlc, param, ticker, price) || first).resolve(market.pricePrecision)
		}

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

	async ordersCancel(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.id
		let orders = await 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 = {}
		await 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 (await this.req('DELETE', "/orders/" + order.id, null, true)) {
				cancelOrders.push(order)
				this.removeId(order.id)
			}
		}
		return cancelOrders
	}

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

class CoinbasePro extends CoinbaseProSandbox {
	static meta = Object.assign({}, super.meta, {
		aliases: [
			"COINBASEPRO",
			"GDAX",
			"COINBASE-PRO",
		],
		endpoint: "https://api.pro.coinbase.com",
		name: "Coinbase Pro",
		permissions: {
			origins: [
				"https://api.pro.coinbase.com/*",
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"coinp1",
				"coinp2",
			],
		},
		website: "https://pro.coinbase.com",
	})
	constructor(meta = CoinbasePro.meta) {
		super(meta)
	}
}

//Broker.add(new CoinbasePro())
Broker.add(new CoinbaseProSandbox())
