"use strict"

class CoinbaseAdvancedSandbox extends Exchange {
	static meta = {
		aliases: [
			"COINBASEADV-SANDBOX",
			"COINBASEADVSANDBOX",
		],
		endpoint: "https://api-sandbox.coinbase.com/api/",
		fields: {
			public: {
				label: "API Key Name",
			},
			private: {
				label: "Private Key",
			},
		},
		name: "Coinbase Adv. Sandbox",
		permissions: {
			origins: [
				"https://api-sandbox.coinbase.com/api/*",
				"https://api.coinbase.com/api/*",
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		},
		recvWindow: 30 * 1000,
		website: "https://www.coinbase.com/advanced-trade/",
		desc: "Offers a secure and easy way to buy, sell, and trade digital assets online across 550+ markets.",
		testSymbol: "ETHBTC",
		symbolAuth: true,
		configTypes: ["market_market_ioc", "sor_limit_ioc", "limit_limit_gtc", "limit_limit_gtd", "limit_limit_fok", "stop_limit_stop_limit_gtc", "stop_limit_stop_limit_gtd", "trigger_bracket_gtc", "trigger_bracket_gtd"]
	}
	constructor(meta = CoinbaseAdvancedSandbox.meta) {
		super(meta)
	}

	async req(method, resource, params, endpoint = this.meta.endpoint) {
		resource = "v3/brokerage/"+resource
		let uri = resource
		if (method === 'GET' && params) {
			uri += '?' + serialize(params)
			params = null
		}
		params = params ? JSON.stringify(params) : ''

		const credentials = this.getCredentials("private")
		const ts = Math.floor(this.getTimestamp() / 1000)
		const headers = {
			"Content-Type": "application/json",
			"Authorization": "Bearer " + KJUR.jws.JWS.sign("ES256", {
				alg: "ES256",
				kid: credentials.public,
				nonce: KJUR.crypto.Util.getRandomHexOfNbytes(16)
			}, {
				iss: "cdp", nbf: ts, exp: ts + 120,
				sub: credentials.public,
				uri: method + " api.coinbase.com/api/" + resource
			}, credentials.private.replaceAll('\\n', '\n'))
		}

		let response = await request.call(this, method, endpoint + uri, params, headers)
		if (!response && method === 'GET') {
			response = await request.call(this, method, endpoint + uri, params, headers)
		}
		if (response && (response.error || response.error_response)) {
			response = response.error_response || response
			throw new Error((response.error_details || response.message || response.error || "Unknown connection error with API endpoint!") + (response.code ? ` (Code ${response.code})`:''))
		}
		return response
	}

	async time() {
		return (await request.call(this, 'GET', "https://api.coinbase.com/api/"/*this.meta.endpoint*/ + "v3/brokerage/time", null, null)).epochMillis
	}

	async account(currency) {
		let balances = {}
		const params = {limit: 250}
		do {
			const data = await this.req('GET', "accounts", params)
			const accounts = data?.accounts
			accounts.forEach(item => {
				const available = Number(item.available_balance?.value) || 0
				const balance = available + (Number(item.hold?.value) || 0)

				if (balance || item.currency === currency) {
					balances[item.currency] = {available, balance}
				}
			})
			params.cursor = data.cursor && accounts.length >= params.limit && data.cursor
		} while (params.cursor)
		return balances
	}

	async symbolInfoImpl(cache) {
		const response = await this.req('GET', "products", null, "https://api.coinbase.com/api/")
		response?.products?.forEach(info => {
			if (info.is_disabled) return
			info.basePrecision = decimals(info.base_increment) + (!info.base_increment.endsWith('1') ? info.base_increment % 1 : 0)
			info.quotePrecision = decimals(info.quote_increment) + (!info.quote_increment.endsWith('1') ? info.quote_increment % 1 : 0)
			info.pricePrecision = decimals(info.price_increment) + (!info.price_increment.endsWith('1') ? info.price_increment % 1 : 0)
			info.base_min_size = Number(info.base_min_size), info.base_max_size = Number(info.base_max_size)
			info.quote_min_size = Number(info.quote_min_size), info.quote_max_size = Number(info.quote_max_size)
			cache[this.sym(info.product_id)] = info
		})
	}

	async symbolTickerImpl(symbol) {
		const ticker = (await this.req('GET', "best_bid_ask", {product_ids: symbol}, "https://api.coinbase.com/api/")).pricebooks
		const ask = Number(ticker[0]?.asks[0]?.price) || 0
		const bid = Number(ticker[0]?.bids[0]?.price) || 0
		return ticker && {
			ask, bid, mid: (ask + bid) / 2
		}
	}

	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.product_id
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.product_id)) || (await this.symbolTicker(cmd.up, market.product_id))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.product_id} is not available!`)
		}
		this.checkSlip(cmd, ohlc, market.product_id, ticker)
		if (cmd.up) {
			this.updatePrice(market.product_id, ticker)
			if (!cmd.b && !cmd.ub) {
				return ticker
			}
		}

		const currency = cmd._currency = cmd.isBid ? market.quote_currency_id : market.base_currency_id
		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)")
		}

		const useQuote = cmd.isBid && cmd.t === "market"
		let available = (cmd.y === "equity" ? balance.balance : balance.available) * (useQuote ? price : 1)

		const base = useQuote ? "quote" : "base"
		const config = {}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			config[base+'_size'] = ohlc.left.toFixed(market[base+'Precision'])
		} else {
			config[base+'_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[base+'Precision'])
		}
		if (config[base+'_size'] < market[base+'_min_size']) {
			this.warn(`order quantity below instrument minimum of ${market[base+'_min_size']} – use minq= to set minimum!`)
		} else if (config[base+'_size'] > market[base+'_max_size']) {
			this.warn(`order quantity above instrument maximum of ${market[base+'_max_size']} – use maxq= to set maximum!`)
		} else if (config[base+'_size'] <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated)")
		}
/*
		trigger_bracket_gtc			OCO / gtc			stop_loss_price, stop_trigger_price, take_profit_price
		trigger_bracket_gtd			OCO / gtd			
*/
		let type = "market_market_ioc"
		if (cmd.t !== "market") {
			config.limit_price = price
			type = "limit_limit_"+(cmd.t === "fok" ? cmd.t : "gtc")
		}
		if (cmd.tp || cmd.sl || cmd.so) {
			const param = cmd.tp && 'tpref' || cmd.sl && 'slref' || cmd.so && 'soref'
			config.stop_price = (cmd.tp || cmd.sl || cmd.so)._().relative(this.ohlc(cmd, ohlc, param, ticker, price) || first).resolve(market.pricePrecision)
			config.stop_direction = config.stop_price > first ? "STOP_DIRECTION_STOP_UP" : "STOP_DIRECTION_STOP_DOWN"
			type = "stop_limit_stop_limit_gtc"
		} else if (cmd.t === "post") {
			config.post_only = true
		}
		let order = {
			product_id: market.product_id,
			side: cmd.isBid ? "BUY" : "SELL",
			order_configuration: {[type]: config}
		}
		order.client_order_id = this.uniqueID(cmd.id)

		this.info("placing order: "+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		let result = await this.req('POST', "orders", order)
		if (result.success_response) {
			Object.assign(result.success_response, result.order_configuration?.getAny(this.meta.configTypes))
			result = result.success_response
		}
		return result
	}

	async ordersCancel(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.product_id
		let orders = []
		const params = {order_status: "OPEN", product_id: market.product_id}
		do {
			const data = await this.req('GET', "orders/historical/batch", params)
			const result = data.orders || []
			if (!data || !result.filter) {
				this.msgOrderErr()
				return false
			}
			orders.push(...result)
			params.cursor = result.length >= params.limit && data?.cursor
		} while (params.cursor)
		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
			}
			Object.assign(order, order.order_configuration?.getAny(this.meta.configTypes))
			order.size = Number(order.base_size)
			order.price = Number(order.limit_price)
			if (cmd.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if (hasType && !order.order_type.includesNoCase(cmd.t)) {
				return !match
			}
			if ((cmd.t === "open" && (order.stop_loss_price || order.stop_trigger_price || order.take_profit_price)) || 
				(cmd.t === "close" && !(order.stop_loss_price || order.stop_trigger_price || order.take_profit_price))) {
				return !match
			}
			if ((cmd.so && !order.stop_trigger_price) || (cmd.sl && !order.stop_loss_price) || (cmd.tp && !order.take_profit_price)) {
				return !match
			}
			if (cmd.id && (!order.client_order_id || !order.client_order_id.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": 	["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
		}
		orders = orders.reduce((prev, order) => ({...prev, [order.order_id]: order}), {})
		let results = await this.req('POST', "orders/batch_cancel", {order_ids: Object.keys(orders)})
		results = results.results
		if (!results || !results.filter) return false

		let cancelOrders = []
		for (const result of results) {
			result.success && cancelOrders.push(orders[result.order_id])
		}
		return cancelOrders
	}

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

class CoinbaseAdvanced extends CoinbaseAdvancedSandbox {
	static meta = Object.assign({}, super.meta, {
		aliases: [
			"COINBASEADVANCED",
			"COINBASE-ADVANCED",
			"COINBASE",
		],
		endpoint: "https://api.coinbase.com/api/",
		name: "Coinbase Advanced",
		permissions: {
			origins: [
				"https://api.coinbase.com/api/*",
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"coina1",
				"coina2",
			],
		},
		website: "https://www.coinbase.com/advanced-trade/",
	})
	constructor(meta = CoinbaseAdvanced.meta) {
		super(meta)
	}
}

Broker.add(new CoinbaseAdvanced())
Broker.add(new CoinbaseAdvancedSandbox())
