const { normalize, Big, util, request, pg } = require('./scrape/common.js');
const qs = require('querystring');
const argon2 = require('argon2');
const { hash: blake3 } = require('blake3/dist/node');//wasm is faster than neon(node-native) for small inputs (16 bytes)
const b = require('fs').readFileSync('./16byteSecretKey');
const cryptids = new (require('crypt-ids').Cryptids)(new Uint8Array(b.buffer, b.byteOffset, b.byteLength));
const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID);
const pool = new pg.Pool();
const randomBytes = util.promisify(require('crypto').randomBytes);

let clients = 0;
const authenticated = new Map(/*<user_ID, websocket>*/);

setInterval(() => pool.query('select invalidate_stale_carts()'), 1000);

async function is(ws, store, owner) {//ws->user_ID || undefined, store_id integer || undefined, owner boolean || undefined
//https://stackoverflow.com/questions/354070/sql-join-where-clause-vs-on-clause
	return (await pool.query(`select god or (owner is not null and owner >= $1) as t from usr left join user_store on usr.user_id = user_store.user_id and store_id = $2 where usr.user_id = $3`, [Boolean(owner), store, ws.user_ID])).rows?.[0]?.t;
}

function notmissing(parameters, fields, ws, response_ID) {
	let sad;
	if(parameters) {
		sad = fields.filter(field => parameters[field] === "" || parameters[field] === null || parameters[field] === undefined || parameters[field] === NaN).join(', ');
	} else {
		sad = fields;
	}
	if(sad.length) {
		ws.send(JSON.stringify({
			response_ID,
			data: 'missing ' + sad
		}));
		return false;
	} else {
		return true;
	}
}

async function mergeCart(client, carts, ws) {
	let decryptedCarts = [];
	if(Array.isArray(carts)) {
		for(let i = 0; i < carts.length; ++i) {
			try {
				decryptedCarts.push(cryptids.s2i(carts[i]));
			} catch(e) {}
		}
	}
	if(decryptedCarts.length) {
		let diy = false;
		if(!client) {
			diy = true;
			client = await pool.connect();
			await client.query('begin');
		}
		let carts2merge = (await client.query(`
			with distinct_cart_per_store as (
				select distinct on (store_id) cart_id, store_id
				from cart
				where cart_id = any($2) and user_id is null and status = 'active'
			),
			associate_user2cart as (
				update cart
				set user_id = $1
				where
					cart_id in (select cart_id from distinct_cart_per_store) and
					not exists(select from cart as c where c.user_id = $1 and c.store_id = cart.store_id and c.status = 'active')
				returning cart_id, store_id
			)
			select
				t.cart_id as anon_cart_id,
				cart.cart_id as user_cart_id,
				json_agg(json_build_object(
					'line_item_id', line_item_id,
					'menu_item_id', menu_item_id,
					'quantity', quantity
				)) as line_items2associate_user_or_add_qty
			from
				(select * from distinct_cart_per_store except all select * from associate_user2cart) as t inner join
				cart on (t.store_id = cart.store_id and status = 'active' and user_id = $1) inner join
				line_item on (t.cart_id = line_item.cart_id)
			group by
				t.cart_id,
				cart.cart_id
		`, [ws.user_ID, decryptedCarts])).rows;
		//todo: set based merge cart or cursor plpgsql function. this is ugly
		loop1: for(let i = 0; i < carts2merge.length; ++i) {
			let addQty = [];
			let lis = carts2merge[i].line_items2associate_user_or_add_qty;
			for(let j = 0; j < lis.length; ++j) {
				try {
					await client.query('savepoint wow');
					await client.query(`update line_item set cart_id = $1 where line_item_id = $2`, [carts2merge[i].user_cart_id, lis[j].line_item_id]);
				} catch(e) {
					await client.query('rollback to wow');
					if(e.constraint === 'line_item_cart_id_menu_item_id_key') {
						addQty.push(lis[j]);
					} else if(e.message === 'exceeded 30g limit') {
						continue loop1;
					} else {
						console.error(e);
					}
				}
			}
			console.log('sanity');
			await client.query('delete from cart where cart_id = $1', [carts2merge[i].anon_cart_id]);
			for(let j = 0; j < addQty.length; ++j) {
				try {
					await client.query('savepoint wow');
					await client.query('update line_item set quantity = quantity + $1 where cart_id = $2 and menu_item_id = $3', [addQty[j].quantity, carts2merge[i].user_cart_id, addQty[j].menu_item_id]);
				} catch(e) {
					await client.query('rollback to wow');
					if(e.message === 'exceeded 30g limit') {
						break;
					} else {
						console.error(e);
					}
				}
			}
		}
		if(diy) {
			await client.query('commit');
			client.release();
		}
	}
}

const origins = process.env.WS_ACCEPTED_ORIGINS.split(',');
const ssl = Boolean(process.env.SSLKEY && process.env.SSLCERT);
const port = Number(ssl ? process.env.SSLPORT : process.env.PORT);
const app = require('uWebSockets.js')[(ssl ? 'SSL' : '') + 'App']({key_file_name: process.env.SSLKEY, cert_file_name: process.env.SSLCERT})
.ws('/*', {
	/* Options */
	compression: 0,
	maxPayloadLength: 16 * 1024 * 1024,
	idleTimeout: 10000,
	/* Handlers */
	upgrade: (res, req, context) => {
		if(origins.includes(req.getHeader('origin')) || origins.includes('*')) {
			res.upgrade(
				{q: qs.parse(req.getQuery())},
				req.getHeader('sec-websocket-key'),
				req.getHeader('sec-websocket-protocol'),
				req.getHeader('sec-websocket-extensions'),
				context
			);
		} else {
			res.close();
		}
	},
	open: async ws => {
		console.log('WebSocket opened');
		pool.query(`select * from get_stores()`).then(r => {
			for(let i = 0; i < r.rows.length; ++i) {
				for(const prop in r.rows[i]) {
					if(r.rows[i][prop] === null) {
						delete r.rows[i][prop];
					}
				}
			}
			ws.subscribe('store');
			ws.send(JSON.stringify({
				what: 'store',
				how: 'replace',
				data: r.rows
			}));
		});
		let fuck;
		let hack;
		if(ws.q.token) {
			let user = (await pool.query('select user_id, email, god, json_object_agg(store_id, owner) filter (where store_id is not null) as acl from usr left join user_store using (user_id) where token_hash = $1 and now() < token_expiry group by user_id', [blake3(Buffer.from(ws.q.token, 'hex'), {length: 16})])).rows[0];
			if(user === undefined) {
				fuck = JSON.stringify({
					what: 'user',
					how: 'replace',
					data: null,
					why: 'invalid token'
				});
			} else {
				ws.user_ID = user.user_id;
				delete user.user_id;
				hack = user;
				let old = authenticated.get(ws.user_ID);
				authenticated.set(ws.user_ID, ws);
				if(old) {
					old.send(JSON.stringify({
						what: 'user',
						how: 'replace',
						data: null,
						why: 'logged in elsewhere'
					}));
					old.end(1000, 'logged in elsewhere');//this works correctly in firefox onclose, but not in chrome. https://stackoverflow.com/a/53340067 todo: report on chromium bug tracker
				} else {
					ws.publish('user/authenticated', JSON.stringify({
						what: 'user/authenticated',
						how: 'add',
						data: ws.user_ID
					}));
				}
			}
		}
		if(ws.q.otp && (ws.q.token && fuck || !ws.q.token)) {
			//click link && original tab closed(sharedworker dead, so no preexisting connection)
			//good thing this is a fallback to token. magic links won't invalidate remember me :)
			let token = await randomBytes(16);
			let user = (await pool.query(
				`with u as (
					update usr
					set
						otp_hash = null,
						token_hash = $1
					where
						otp_hash = $2 and
						now() < otp_expiry
					returning
						user_id,
						email,
						god
				)
				select u.*, json_object_agg(store_id, owner) filter (where store_id is not null) as acl
				from u left join user_store using (user_id)
				group by user_id, email, god`,
				[
					blake3(token, {length: 16}),
					blake3(Buffer.from(ws.q.otp, 'hex'), {length: 16})
				]
			)).rows[0];
			if(user === undefined) {
				ws.send(JSON.stringify({
					what: 'user',
					how: 'replace',
					data: null,
					why: 'invalid otp'
				}));
				if(fuck) {
					ws.send(fuck);
				}
			} else {
				ws.user_ID = user.user_id;
				delete user.user_id;
				user.token = token.toString('hex');
				hack = user;
				let old = authenticated.get(ws.user_ID);
				authenticated.set(ws.user_ID, ws);
				if(old) {
					old.send(JSON.stringify({
						what: 'user',
						how: 'replace',
						data: null,
						why: 'logged in elsewhere'
					}));
					old.end(1000, 'logged in elsewhere');//this works correctly in firefox onclose, but not in chrome. https://stackoverflow.com/a/53340067 todo: report on chromium bug tracker
				} else {
					ws.publish('user/authenticated', JSON.stringify({
						what: 'user/authenticated',
						how: 'add',
						data: ws.user_ID
					}));
				}
			}
		} else if(fuck) {
			ws.send(fuck);
		}
		++clients;
		ws.publish('user/count', JSON.stringify({
			what: 'user/count',
			how: 'add'
		}));
		if(hack) {
			await mergeCart(null, ws.q.carts, ws);
			ws.send(JSON.stringify({
				what: 'user',
				how: 'replace',
				data: hack
			}));
		}
		delete ws.q;
	},
	message: async (ws, message, isBinary) => {
		let {what, parameters = {}, request_ID} = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(message)));
		switch(what) {
			case 'request_otp':
				if(ws.user_ID) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: "already logged in"
					}));
				} else {
					let token = await randomBytes(16);
					if((await pool.query(`update usr set otp_expiry = now() + interval '5 minute', otp_hash = $2 where email = $1`, [parameters.email, blake3(token, {length: 16})])).rowCount === 1) {
						token = token.toString('hex');
						sgMail.send({
							to: parameters.email,
							from: 'lobojane <support@lobojane.com>',
							subject: `Click this link to finish logging in`,
							html: `<a href="http://localhost:3000?otp=${token}">idk</a><div>Alternatively, copy and paste <code>${token}</code> into the input field</div>if this wasn't you, let us know by replying to this email.`
						}).then(() => ws.send(JSON.stringify({
							response_ID: request_ID
						})), error => {
							console.error(error);
							ws.send(JSON.stringify({
								response_ID: request_ID,
								data: 'failed to send email'
							}));
						});
					} else {
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: 'user does not exist'
						}))
					}
				}
				break;
			case 'login_otp'://if paste in code || click link && original tab still open(sharedworker active, preexisting connection)
				if(ws.user_ID) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: "already logged in"
					}));
				} else {
					let token = await randomBytes(16);
					let user = (await pool.query(
						`with u as (
							update usr
							set
								otp_hash = null,
								token_hash = $1
							where
								otp_hash = $2 and
								now() < otp_expiry
							returning
								user_id,
								email,
								god
						)
						select u.*, json_object_agg(store_id, owner) filter (where store_id is not null) as acl
						from u left join user_store using (user_id)
						group by user_id, email, god`,
						[
							blake3(token, {length: 16}),
							blake3(Buffer.from(parameters.otp, 'hex'), {length: 16})
						]
					)).rows[0];
					if(user === undefined) {
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: "invalid otp"
						}));
					} else {
						ws.user_ID = user.user_id;
						await mergeCart(null, parameters.carts, ws);
						delete user.user_id;
						user.token = token.toString('hex');
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: user
						}));
						let old = authenticated.get(ws.user_ID);
						authenticated.set(ws.user_ID, ws);
						if(old) {
							old.send(JSON.stringify({
								what: 'user',
								how: 'replace',
								data: null,
								why: 'logged in elsewhere'
							}));
							old.end(1000, 'logged in elsewhere');//this works correctly in firefox onclose, but not in chrome. https://stackoverflow.com/a/53340067 todo: report on chromium bug tracker
						} else {
							ws.publish('user/authenticated', JSON.stringify({
								what: 'user/authenticated',
								how: 'add',
								data: ws.user_ID
							}));
						}
					}
				}
				break;
			case 'register':
				if(ws.user_ID) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: "already logged in"
					}));
				} else {
					try {
						let token = await randomBytes(16);
						let decryptedCarts = [];
						if(Array.isArray(parameters.carts)) {
							for(let i = 0; i < parameters.carts.length; ++i) {
								try {
									decryptedCarts.push(cryptids.s2i(parameters.carts[i]));
								} catch(e) {}
							}
						}
						let user = (await pool.query(`
							with i as (
								insert into usr
								(email, passphrase_hash, token_hash)
								values ($1, $2, $3)
								returning user_id
							), distinct_cart_per_store as (
								select distinct on (store_id) cart_id
								from cart
								where cart_id = any($4) and user_id is null and status = 'active'
							), u as (
								update cart
								set user_id = (select user_id from i)
								where cart_id in (select cart_id from distinct_cart_per_store)
							)
							select user_id from i`,
							[
								parameters.email,
								await argon2.hash(parameters.passphrase),
								blake3(token, {length: 16}),
								decryptedCarts
							])).rows[0];
						ws.user_ID = user.user_id;
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: {token: token.toString('hex')}/*sharedworker needs to intercept this and emit virtual event
							what: 'user',
							how: 'replace',
							data: {email: fromforminput, god: false(default), acl: null(default)}*/
						}));
						user.email = parameters.email;
						user.god = false;
						user.acl = null;
						ws.publish('user/s', JSON.stringify({
							what: "users",
							how: 'add',
							data: user
						}));
						//ws.publish('/users/' + id? dunno if needed, JSON.stringify(event.json id, email, add))
						authenticated.set(ws.user_ID, ws);
						ws.publish('user/authenticated', JSON.stringify({
							what: 'user/authenticated',
							how: 'add',
							data: {user_id: ws.user_ID, email: parameters.email, god: false, logged_in: true},
							why: 'register'
						}));
					} catch(e) {
						let error = 'user already exists';
						if(e.constraint !== 'usr_email_key') {
							console.error(e);
							error = 'error';
						}
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: error
						}));
					}
				}
				break;
			case 'unregister':
				if(ws.user_ID) {
					await pool.query(`delete from usr where user_id = $1`, [ws.user_ID]);
					ws.send(JSON.stringify({
						response_ID: request_ID
					}));
					//ws.publish('/users/' + id? dunno if needed, JSON.stringify(event.json id, remove))
					//probably needed, since subscribing to /users/${id} is a given to listen to augment calls or whatever
					authenticated.delete(ws.user_ID);
					ws.publish('user/authenticated', JSON.stringify({
						what: 'user/authenticated',
						how: 'remove',
						data: ws.user_ID,
						why: 'unregister'
					}));
					ws.end(1000, 'jendo');//default status is 0, which translates to 1005
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));
				}
				break;
			case 'login':
				if(ws.user_ID) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: "already logged in"
					}));
				} else {
					//think maybe todo: if remember_me false, don't generate new token. this is so that if user "remember me" at home computer, but they for whatever reason need to log in elsewhere, won't invalidate home "remember me". but then while they're logged in elsewhere, if lose connection or refresh page (and no other tabs so sharedworker will be recreated), they'll need to log in again :(
					let user = (await pool.query('select user_id, passphrase_hash, god, json_object_agg(store_id, owner) filter (where store_id is not null) as acl from usr left join user_store using (user_id) where email = $1 group by user_id', [parameters.email])).rows[0];
					if(user === undefined) {
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: "user does not exist"
						}));
					} else {
						try {
							if(user.passphrase_hash !== null && user.passphrase_hash !== undefined && await argon2.verify(user.passphrase_hash, parameters.passphrase)) {
								ws.user_ID = user.user_id;
								let token = await randomBytes(16);
								const client = await pool.connect();
								await client.query('begin');
								await client.query(`update usr set token_hash = $1 where user_id = $2`, [blake3(token, {length: 16}), ws.user_ID]);
								await mergeCart(client, parameters.carts, ws);
								await client.query('commit');
								client.release();
								//yuck, since json can't send binary, need to hex encode. fyi base64 is smaller than hex(base16), but after compressing, hex is smaller. also base64 has issues with regards to url encoding, and slower than hex
								//if remember me on client side, store in localstorage. else do nothing with token
								//on subsequent new sockets, auto_login with token. token will be invalid(undefined) if remember me was not checked
								ws.send(JSON.stringify({
									response_ID: request_ID,
									data: {token: token.toString('hex'), god: user.god, acl: user.acl}
								}));
								let old = authenticated.get(ws.user_ID);
								authenticated.set(ws.user_ID, ws);
								if(old) {
									old.send(JSON.stringify({
										what: 'user',
										how: 'replace',
										data: null,
										why: 'logged in elsewhere'
									}));
									old.end(1000, 'logged in elsewhere');//this works correctly in firefox onclose, but not in chrome. https://stackoverflow.com/a/53340067 todo: report on chromium bug tracker
								} else {
									ws.publish('user/authenticated', JSON.stringify({
										what: 'user/authenticated',
										how: 'add',
										data: ws.user_ID
									}));
								}
							} else {//https://security.stackexchange.com/questions/62661/generic-error-message-for-wrong-passphrase-or-username-is-this-really-helpful
								ws.send(JSON.stringify({
									response_ID: request_ID,
									data: "wrong passphrase"
								}));
							}
						} catch(e) {
							console.error(e);
							ws.send(JSON.stringify({
								response_ID: request_ID,
								data: 'error'
							}));
						}
					}
				}
				break;
			case 'logout':
				if(ws.user_ID) {
					await pool.query(`update usr set token_expiry = null, token_hash = null where user_id = $1`, [ws.user_ID]);
					ws.send(JSON.stringify({
						response_ID: request_ID
					}));
					authenticated.delete(ws.user_ID);
					ws.publish('user/authenticated', JSON.stringify({
						what: 'user/authenticated',
						how: 'remove',
						data: ws.user_ID
					}));
					ws.end(1000, 'see ya l8er alligator');//default status is 0, which translates to 1005
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));
				}
				break;
			case 'list_users': {
				const auth = await is(ws);
				if(auth) {
					ws.subscribe('user/#');//if user type changes, need to auto unsubscribe somehow
					let users = (await pool.query('select user_id, email, god from usr')).rows;
					for(let i = 0; i < users.length; ++i) {
						if(authenticated.has(users[i].user_id)) {
							users[i].logged_in = true;
						} else {
							users[i].logged_in = false;
						}
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: {
							users,
							count: clients
						}
					}));
				} else if(auth === undefined) {
					if(ws.user_ID) {
						authenticated.delete(ws.user_ID);
						ws.publish('user/authenticated', JSON.stringify({
							what: 'user/authenticated',
							how: 'remove',
							data: ws.user_ID
						}));
						delete ws.user_ID;
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));//client side virtual event user null
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthorized'
					}));//client side virtual event user god false
				}
				break;}
			// case 'update_user' for god bulk edit
			case 'update_profile':
				if(ws.user_ID) {
					let dawg = [];
					let poop = [ws.user_ID];
					if(typeof parameters.email === 'string') {
						dawg.push(`email = $${poop.push(parameters.email)}`);
					}
					if(typeof parameters.passphrase === 'string') {
						dawg.push(`passphrase_hash = $${poop.push(await argon2.hash(parameters.passphrase))}`);
					}
					if(dawg.length) {
						try {
							if((await pool.query(`update usr set ${dawg.join(',')} where user_id = $1`, poop)).rowCount) {
								ws.send(JSON.stringify({
									response_ID: request_ID
								}));
							} else {
								authenticated.delete(ws.user_ID);
								ws.publish('user/authenticated', JSON.stringify({
									what: 'user/authenticated',
									how: 'remove',
									data: ws.user_ID
								}));
								delete ws.user_ID;
								ws.send(JSON.stringify({
									response_ID: request_ID,
									data: 'unauthenticated'
								}));
							}
						} catch(e) {
							let error = 'this email address is in use';
							if(e.constraint !== 'usr_email_key') {
								console.error(e);
								error = 'error';
							}
							ws.send(JSON.stringify({
								response_ID: request_ID,
								data: error
							}));
						}
					} else {
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: 'WHAT DO U WANT ME 2 DO???'
						}));
					}
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));
				}
				break;
			case 'create_store': {
				const auth = await is(ws);
				if(auth) {
					let addr, location, tz, foot, car;
					try {
						[addr, location, tz, foot, car] = await g(`${parameters.address}, ${parameters.address2}, ${parameters.city}, ${parameters.region}, ${parameters.country}, ${parameters.postal_code}`);
					} catch(e) {
						if(e.response) {
							console.error(e.response.data);
						} else {
							console.error(e);
						}
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: 'geocode failure'
						}));
						break;
					}
					let fuck = [];
					if(Array.isArray(parameters.times)) {
						for(let i = 0; i < parameters.times.length; ++i) {
							try {
								let ts = parameters.times[i].time_start.split(':');
								ts = Number(parameters.times[i].day_start) * 1440 + Number(ts[0]) * 60 + Number(ts[1]);
								let te = parameters.times[i].time_end.split(':');
								te = Number(parameters.times[i].day_end) * 1440 + Number(te[0]) * 60 + Number(te[1]);
								if(ts < 0 || ts >= 10080 || te < 0 || te >= 10080) {
									continue;
								}
								if(te <= ts) {
									fuck.push([ts, 10080], [0, te]);
									//oops possibly 0,0 empty range. nvm good job range_agg
								} else {
									fuck.push([ts, te]);
								}
							} catch(e) {}
						}
					}
					let jn = sanitize_name(parameters.name)
					let params = [
						jn,
						jn,
						parameters.URL,
						addr.address,
						addr.address2,
						addr.city,
						addr.region,
						addr.country,
						addr.postal_code,
						location.lng,
						location.lat,
						foot,
						car,
						tz,
						parameters.phone ? parameters.phone : null,
						parameters.phone && parameters.extension ? parameters.extension : null,
						Boolean(parameters.delivery),
						Boolean(parameters.pickup),
						Boolean(parameters.prepayment),
						JSON.stringify(fuck)
					];
					let q = 1;
					let query = '';
					if(Array.isArray(parameters.taxes) && parameters.taxes.length) {
						query += `, insert${++q} as (
							insert into store_tax (store_id, name, rate, types)
							values ${parameters.taxes.map(tax => `((select store_id from insert1), $${params.push(tax.name)}, $${params.push(tax.rate)}, $${params.push(tax.types)})`).join(', ')}
						)`;
					}
					if(Array.isArray(parameters.images) && parameters.images.length) {
						query += `, insert${++q} as (
							insert into store_image (store_id, URL)
							values ${parameters.images.map(image => `((select store_id from insert1), $${params.push(image)})`).join(', ')}
						)`;
					}
					pool.query(
						`with insert1 as (
							insert into store (
								joined_name,
								name,
								url,
								address,
								address2,
								city,
								region,
								country,
								postal_code,
								longitude,
								latitude,
								osrm_hint_foot,
								osrm_hint_car,
								timezone,
								phone,
								extension,
								delivery,
								pickup,
								prepayment,
								open
							) select
								$1,
								$2,
								$3,
								$4,
								$5,
								$6,
								$7,
								$8,
								$9,
								$10,
								$11,
								$12,
								$13,
								$14,
								$15,
								$16,
								$17,
								$18,
								$19,
								(select range_agg(int4range((x->>0)::int, (x->>1)::int), true, true) from json_array_elements($20) as t(x))
							returning store_id
						)${query}
						select * from get_stores((select store_id from insert1))`,
						params
					).then(r => {
						ws.publish('store', JSON.stringify({
							what: 'store',
							how: 'add',
							data: r.rows[0]
						}));
						ws.send(JSON.stringify({
							response_ID: request_ID
						}));
					}).catch(e => {
						/* todo
						Class 23 https://www.postgresql.org/docs/current/errcodes-appendix.html with e.code
						e.table
						https://stackoverflow.com/a/4108266
						use implicit names in initialize.sql. then, can use e.constraint, remove e.table + _ from beginning, remove suffix to see which columns are affected
						e.detail is fine for 23505, but 23514 just complains about the row as a whole. how can we get the specific details? https://stackoverflow.com/a/47972916 for python...
						useful
						  code: '23505',
						  detail: 'Key (store, name)=(21, hst) already exists.',
						useless
						  [message]: 'new row for relation "store_tax" violates check constraint "store_tax_rate_check"',
						  name: 'error',
						  length: 266,
						  severity: 'ERROR',
						  code: '23514',
						  detail: 'Failing row contains (15, 28, hst, 2, 2020-02-11 20:20:54.923496-05, infinity).',
						what I want: rate > 0 and rate < 1
						2 > 0 and 2 < 1
						true and false
						false
						*/
						console.log(e);
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: 'db fuckup'
						}));
					});
				} else if(auth === undefined) {
					if(ws.user_ID) {
						authenticated.delete(ws.user_ID);
						ws.publish('user/authenticated', JSON.stringify({
							what: 'user/authenticated',
							how: 'remove',
							data: ws.user_ID
						}));
						delete ws.user_ID;
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));//client side virtual event user null
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthorized'
					}));//client side virtual event user god false
				}
				break;}
			case 'store_distance': {
				let stores = (await pool.query('select store_id, longitude, latitude, osrm_hint_foot, osrm_hint_car from store where longitude is not null and latitude is not null')).rows;
				let longlats = stores.map(x => `${x.longitude},${x.latitude}`).join(';');
				let [foot, car] = await Promise.all([
					request(`http://127.0.0.1:5000/table/v1/fuck/${parameters.longitude},${parameters.latitude};${longlats}?annotations=duration,distance&skip_waypoints=true&sources=0&hints=;${stores.map(x => x.osrm_hint_foot).join(';')}`).then(JSON.parse),
					request(`http://127.0.0.1:5001/table/v1/fuck/${parameters.longitude},${parameters.latitude};${longlats}?annotations=duration,distance&skip_waypoints=true&sources=0&hints=;${stores.map(x => x.osrm_hint_car).join(';')}`).then(JSON.parse)
				]);
				if(foot.code === 'Ok' && car.code === 'Ok') {
					for(let i = stores.length - 1; i >= 0; --i) {
						delete stores[i].longitude;
						delete stores[i].latitude;
						delete stores[i].osrm_hint_foot;
						delete stores[i].osrm_hint_car;
						if(foot.durations[0][i + 1] === null && car.durations[0][i + 1] === null) {
							stores.splice(i, 1);
						} else if(foot.durations[0][i + 1] === null) {
							stores[i].distance = car.distances[0][i + 1];
						} else if(car.durations[0][i + 1] === null || foot.durations[0][i + 1] <= car.durations[0][i + 1] + 300) {// + 5 minutes to make more likely to choose foot to account for things like parking, traffic
							stores[i].distance = foot.distances[0][i + 1];
						} else {
							stores[i].distance = car.distances[0][i + 1];
						}
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: stores
					}));
				} else {
					console.log(foot, car);
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'could not compute routes'
					}));
				}
				break;}
			case 'cloudinary_preset': {
				//owner is not null if want to accept >= store worker as well. right now just >= store owner
				const auth = (await pool.query(`select god or owner as t from usr left join user_store using (user_id) where user_id = $1 order by owner desc nulls last limit 1`, [ws.user_ID])).rows?.[0]?.t;
				if(auth) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: process.env.CLOUDINARY
					}));
				} else if(auth === undefined) {
					if(ws.user_ID) {
						authenticated.delete(ws.user_ID);
						ws.publish('user/authenticated', JSON.stringify({
							what: 'user/authenticated',
							how: 'remove',
							data: ws.user_ID
						}));
						delete ws.user_ID;
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));//client side virtual event user null
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthorized'
					}));//client side virtual event user god false
					let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
					ws.send(JSON.stringify({
						what: 'user',
						how: 'update',
						data: {acl: r.length ? Object.fromEntries(r) : null}
					}));
				}
				break;}
			case 'product_form': {
				//owner is not null if want to accept >= store worker as well. right now just >= store owner
				const auth = (await pool.query(`select god or owner as t from usr left join user_store using (user_id) where user_id = $1 order by owner desc nulls last limit 1`, [ws.user_ID])).rows?.[0]?.t;
				if(auth) {
					const [strains, pgenum] = await Promise.all([
						pool.query('select strain_id as id, display_name as name from strain').then(x => x.rows),
						pool.query('select array_to_json(enum_range(null::species)) as species, array_to_json(enum_range(null::product_type)) as product_types').then(x => x.rows[0])
					]);
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: {strains, ...pgenum}
					}));
				} else if(auth === undefined) {
					if(ws.user_ID) {
						authenticated.delete(ws.user_ID);
						ws.publish('user/authenticated', JSON.stringify({
							what: 'user/authenticated',
							how: 'remove',
							data: ws.user_ID
						}));
						delete ws.user_ID;
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));//client side virtual event user null
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthorized'
					}));//client side virtual event user god false
					let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
					ws.send(JSON.stringify({
						what: 'user',
						how: 'update',
						data: {acl: r.length ? Object.fromEntries(r) : null}
					}));
				}
				break;}
			case 'create_product': {
				const auth = (await pool.query(`select god, owner from usr left join user_store using (user_id) where user_id = $1 order by owner desc nulls last limit 1`, [ws.user_ID])).rows?.[0];
				if(auth === undefined) {
					if(ws.user_ID) {
						authenticated.delete(ws.user_ID);
						ws.publish('user/authenticated', JSON.stringify({
							what: 'user/authenticated',
							how: 'remove',
							data: ws.user_ID
						}));
						delete ws.user_ID;
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));//client side virtual event user null
				} else if(auth.god || auth.owner) {
					try {
						let q = 1;
						let [producer, brand, joined_name, display_name] = normalize(parameters.brand, parameters.name, parameters.type);
						let params = [
							producer,
							brand,
							joined_name,
							display_name,
							parameters.type,
							parameters.species,
							parameters.strain,
							parameters.description,
							parameters.terpenes,
							auth.god
						];
						let query = `with insert1 as (insert into product (producer, brand, display_name, joined_name, type, species, strain_id, description, terpenes, show) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) on conflict do nothing returning product_id)`;
						if(Array.isArray(parameters.variants) && parameters.variants.length) {
							if(parameters.type === 'flower') {
								let values = parameters.variants.reduce((acc, v) => {
									if(isFinite(v.quantity)) {
										acc.push(`($${params.push(v.quantity)})`);
									}
									return acc;
								}, []);
								if(values.length) {
									query += `, insert${++q} as (insert into variant (product_id, quantity) select product_id, x::numeric from insert1 cross join (values ${values.join(',')}) as t(x) on conflict do nothing)`;
								}
							} else if(parameters.type === 'preroll' || parameters.type === 'tea' || parameters.type === 'gummy' || parameters.type === 'mint' || parameters.type === 'chocolate' || parameters.type === 'baked') {
								let values = parameters.variants.reduce((acc, v) => {
									if(isFinite(v.quantity) && Number.isInteger(Number(v.portions))) {
										acc.push(`($${params.push(v.portions)}, $${params.push(v.quantity)})`);
									}
									return acc;
								}, []);
								if(values.length) {
									query += `, insert${++q} as (insert into variant (product_id, portions, quantity) select product_id, x::smallint, y::numeric from insert1 cross join (values ${values.join(',')}) as t(x, y) on conflict do nothing)`;
								}
							} else if(parameters.type === 'cartridge' || parameters.type === 'disposable') {
								let values = parameters.variants.reduce((acc, v) => {
									if(isFinite(v.quantity)) {
										acc.push(`($${params.push(v.quantity)}, ${v.flavor ? '$' + params.push(v.flavor) : 'null'})`);
									}
									return acc;
								}, []);
								if(values.length) {
									query += `, insert${++q} as (insert into variant (product_id, quantity, flavor) select product_id, x::numeric, y from insert1 cross join (values ${values.join(',')}) as t(x, y) on conflict do nothing)`;
								}
							} else if (parameters.type === 'oil' || parameters.type === 'spray') {
								let values = parameters.variants.reduce((acc, v) => {
									if(isFinite(v.gram_equivalency) && Number.isInteger(Number(v.quantity))) {
										acc.push(`($${params.push(v.quantity)}, $${params.push(v.gram_equivalency)})`);
									}
									return acc;
								}, []);
								if(values.length) {
									query += `, insert${++q} as (insert into variant (product_id, quantity, gram_equivalency) select product_id, y::numeric, z::numeric from insert1 cross join (values ${values.join(',')}) as t(y, z) on conflict do nothing)`;
								}
							} else if(parameters.type === 'capsule') {
								let values = parameters.variants.reduce((acc, v) => {
									if(isFinite(v.gram_equivalency) && Number.isInteger(Number(v.portions))) {
										acc.push(`($${params.push(v.portions)}, ${isFinite(v.quantity) ? '$' + params.push(v.quantity) : 'null'}, $${params.push(v.gram_equivalency)})`);
									}
									return acc;
								}, []);
								if(values.length) {
									query += `, insert${++q} as (insert into variant (product_id, portions, quantity, gram_equivalency) select product_id, x::smallint, y::numeric, z::numeric from insert1 cross join (values ${values.join(',')}) as t(x, y, z) on conflict do nothing)`;
								}
							}
						}
						if(Array.isArray(parameters.images) && parameters.images.length) {
							query += `, insert${++q} as (insert into product_image (product_id, destination) select product_id, x from insert1 cross join (values ${parameters.images.map(i => `(${params.push(i)})`).join(',')}) as t(x) on conflict do nothing)`;
						}
						let product = (await pool.query(query + ' select product_id from insert1', params)).rows;
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: product.length === 1 && Number.isInteger(Number(product[0].product_id)) ? undefined : 'product already exists'
						}));
					} catch(e) {
						console.error(e);
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: 'check log'
						}));
					}
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthorized'
					}));//client side virtual event user god false
					let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
					ws.send(JSON.stringify({
						what: 'user',
						how: 'update',
						data: {acl: r.length ? Object.fromEntries(r) : null}
					}));
				}
				break; }
			case 'productsq':
				const [strains, idk, pgenum] = await Promise.all([
					pool.query('select strain_id as id, display_name as name from strain').then(x => x.rows),
					pool.query('select producer, array_agg(distinct brand) as brands from product group by producer').then(x => x.rows),//takes in a store / set of stores and has number of items per producer / brand in brackets? (10)
					pool.query(`select
						array_to_json(enum_range(null::species)) as species,
						array_to_json(enum_range(null::product_type)) as product_types
					`).then(x => x.rows[0])
				]);
				ws.send(JSON.stringify({
					response_ID: request_ID,
					data: {strains, idk, ...pgenum}
				}));
				break;
			case 'products': {
				let params = [];
				let qs = '';
				let wack = '';
				if(parameters) {
					if(Array.isArray(parameters.stores) && (parameters.stores = parameters.stores.map(Number)).every(Number.isInteger)) {
						qs += `_store_ids => $${params.push(parameters.stores)},`;
					}
					if(Array.isArray(parameters.strain_ids) && (parameters.strain_ids = parameters.strain_ids.map(Number)).every(Number.isInteger)) {
						qs += `_strain_ids => $${params.push(parameters.strain_ids)},`;
					}
					if(Array.isArray(parameters.types) && parameters.types.every(s => typeof s === 'string')) {
						qs += `_types => $${params.push(parameters.types)},`;
					}
					if(Array.isArray(parameters.species) && parameters.species.every(s => typeof s === 'string')) {
						qs += `_species => $${params.push(parameters.species)},`;
					}
					if(typeof parameters.search === 'string') {
						qs += `search => $${params.push(parameters.search)},`;
					}
					if(typeof parameters.in_stock === 'boolean') {
						qs += `in_stock => $${params.push(parameters.in_stock)},`;
					}
					if(Array.isArray(parameters.thc2tc) && parameters.thc2tc.every(isFinite) && parameters.thc2tc.length === 2) {
						qs += `min_thc2tc => $${params.push(parameters.thc2tc[0])},max_thc2tc => $${params.push(parameters.thc2tc[1])},`;
					}
					if(parameters.min_quantity) {
						qs += `min_quantity => $${params.push(parameters.min_quantity)},`;
					}
					if(parameters.max_quantity) {
						qs += `max_quantity => $${params.push(parameters.max_quantity)},`;
					}
					if(Array.isArray(parameters.producers) && parameters.producers.every(p => typeof p === 'object' && (typeof p.producer === 'string' || p.producer === null) && (Array.isArray(p.brands) || p.brands === undefined))) {
						wack += ` where ${parameters.producers.map(p => `producer ${typeof p.producer === 'string' ? `= $${params.push(p.producer)}` : 'is null'}${Array.isArray(p.brands) ? ` and brand = any($${params.push(p.brands)})`: ''}`).join(' or ')}`;
					}
					if(typeof parameters.price === 'boolean') {
						wack += ` order by min_price ${parameters.price ? 'a' : 'de'}sc`;
					} else if(typeof parameters.THC === 'boolean') {
						wack += ` order by avg_range(thc) ${parameters.THC ? 'a' : 'de'}sc, case when type in ('tea', 'gummy', 'mint', 'chocolate', 'baked') then avg_range(thc) / (min_portions + max_portions) else null end ${parameters.THC ? 'a' : 'de'}sc`;//per pack, per piece, do we need per g?
					} else if(typeof parameters.CBD === 'boolean') {
						wack += ` order by avg_range(cbd) ${parameters.CBD ? 'a' : 'de'}sc, case when type in ('tea', 'gummy', 'mint', 'chocolate', 'baked') then avg_range(cbd) / (min_portions + max_portions) else null end ${parameters.CBD ? 'a' : 'de'}sc`;
					} else if(typeof parameters.producer === 'boolean') {//lower bad for performance. index?
						wack += ` order by lower(producer) ${parameters.producer ? 'a' : 'de'}sc nulls last, case when brand = '' then lower(producer) else lower(brand) end ${parameters.producer ? 'a' : 'de'}sc, lower(name) ${parameters.producer ? 'a' : 'de'}sc`;
					} else if(typeof parameters.brand === 'boolean') {
						wack += ` order by case when brand = '' then lower(producer) else lower(brand) end ${parameters.brand ? 'a' : 'de'}sc, lower(name) ${parameters.brand ? 'a' : 'de'}sc`;
					} else if(typeof parameters.name === 'boolean') {
						wack += ` order by lower(name) ${parameters.name ? 'a' : 'de'}sc, lower(producer) ${parameters.name ? 'a' : 'de'}sc nulls last, case when brand = '' then lower(producer) else lower(brand) end ${parameters.name ? 'a' : 'de'}sc`;
					} else if(typeof parameters.price_g === 'boolean') {
						wack += ` order by min_price_g ${parameters.price_g ? 'a' : 'de'}sc`;
					}
				}
				// try {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: (await pool.query(`select * from get_products(${qs.slice(0, -1)})${wack}`, params)).rows
					}));
				// } catch(e) {
				// 	ws.send(JSON.stringify({
				// 		response_ID: request_ID,
				// 		data: e.message//usually "invalid input value for enum product_type: "flo"". 
				// 	}));
				// }
/*
UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
[DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Uncomment when the future finally happens OR if we actually want to handle this. For now, it's harmless / won't happen.
*/
				break;}
			case 'similar_products': {
				let params = [];
				let qs = '';
				if(parameters) {
					if(Array.isArray(parameters.stores) && (parameters.stores = parameters.stores.map(Number)).every(Number.isInteger)) {
						qs += `_store_ids => $${params.push(parameters.stores)},`;
					}
					if(typeof parameters.in_stock === 'boolean') {
						qs += `in_stock => $${params.push(parameters.in_stock)},`;
					}
					let v = Number(parameters.variant_id);
					if(Number.isInteger(v)) {
						qs += `_variant_id => $${params.push(v)},`;
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: (await pool.query(`select * from similar_products(${qs.slice(0, -1)})`, params)).rows
						}));
						break;
					}
				}
				//if you get here, you fucked up.
				break;
			}
			case 'product_static':
				let p = Number(parameters.id);
				let data = 'invalid id';
				if(Number.isInteger(p)) {
					let params = [p];
					let s = Number(parameters.store);
					let response = (await pool.query(`select * from get_product_static($1${Number.isInteger(s) ? ', $' + params.push(s) : ''})`, params)).rows;
					if(response.length === 0) {
						data = 'product not found';
					} else {
						data = response[0];
					}
				}
				ws.send(JSON.stringify({
					response_ID: request_ID,
					data: data
				}));
				break;
			case 'product_dynamic': {
				let v = Number(parameters.variant);
				let data = 'invalid id';
				if(Number.isInteger(v)) {
					let params = [v];
					let s = Number(parameters.store);
					let response;
					if(Number.isInteger(s)) {
						response = (await pool.query(`select * from get_menu_item($1, $${params.push(s)})`, params)).rows;
					} else {
						response = (await pool.query(`select * from get_product_dynamic($1)`, params)).rows;
					}
					if(response.length === 0) {
						data = 'product not found';
					} else {
						data = response[0];
					}
				}
				ws.send(JSON.stringify({
					response_ID: request_ID,
					data: data
				}));
				break;}
			case 'menu_item_loader': {
				const auth = await is(ws, parameters.store);
				if(auth) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: (await pool.query(`select * from menu_item_loader($1)`, [parameters.store])).rows
					}));
				} else if(auth === undefined) {
					if(ws.user_ID) {
						authenticated.delete(ws.user_ID);
						ws.publish('user/authenticated', JSON.stringify({
							what: 'user/authenticated',
							how: 'remove',
							data: ws.user_ID
						}));
						delete ws.user_ID;
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));//client side virtual event user null
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthorized'
					}));//client side virtual event user god false
					let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
					ws.send(JSON.stringify({
						what: 'user',
						how: 'update',
						data: {acl: r.length ? Object.fromEntries(r) : null}
					}));
				}
				break;}
			case 'menu_items_builder': {
				const auth = await is(ws, parameters.store);
				if(auth) {
					let shit;
					if(Array.isArray(parameters.fresh) && parameters.fresh.length) {
						let query = 'insert into menu_item values ';
						let params = [];
						for(let i = 0; i < parameters.fresh.length; ++i) {
							query += `(default, $${i * 7 + 1}, $${i * 7 + 2}, $${i * 7 + 3}, $${i * 7 + 4}, $${i * 7 + 5}, $${i * 7 + 6}, $${i * 7 + 7}),`;
							params.push(
								parameters.fresh[i].variant,
								parameters.store,
								`[${parameters.fresh[i].min_cbd},${parameters.fresh[i].max_cbd}]`,
								`[${parameters.fresh[i].min_thc},${parameters.fresh[i].max_thc}]`,
								parameters.fresh[i].price,
								parameters.fresh[i].stock,
								Boolean(parameters.fresh[i].featured)
							);
						}
						shit = (await pool.query({text: query.slice(0, -1) + ` on conflict (variant_id, store_id) do update set
							cbd = excluded.cbd,
							thc = excluded.thc,
							price = excluded.price,
							stock = excluded.stock,
							featured = excluded.featured
						where (menu_item.cbd, menu_item.thc, menu_item.price, menu_item.stock, menu_item.featured) is distinct from
						(excluded.cbd, excluded.thc, excluded.price, excluded.stock, excluded.featured) returning menu_item_id
							`,
							values: params,
							rowMode: 'array'
						})).rows.flat();//risky: https://stackoverflow.com/questions/5439293
					}
					if(Array.isArray(parameters.updated)) {
						for(let i = 0; i < parameters.updated.length; ++i) {
							if(Number.isInteger(Number(parameters.updated[i].menu_item))) {
								let params = [parameters.store, parameters.updated[i].menu_item];
								let what = [];
								if(isFinite(parameters.updated[i].price)) {
									what.push(`price = $${params.push(parameters.updated[i].price)}`);
								}
								if(Number.isInteger(Number(parameters.updated[i].stock))) {
									what.push(`stock = $${params.push(parameters.updated[i].stock)}`);
								}
								if(typeof parameters.updated[i].featured === 'boolean') {
									what.push(`featured = $${params.push(parameters.updated[i].featured)}`);
								}
								if(isFinite(parameters.updated[i].min_cbd) && isFinite(parameters.updated[i].max_cbd)) {
									what.push(`cbd = $${params.push(`[${parameters.updated[i].min_cbd},${parameters.updated[i].max_cbd}]`)}`);
								} else if(isFinite(parameters.updated[i].min_cbd)) {
									what.push(`cbd = numrange($${params.push(parameters.updated[i].min_cbd)}, upper(cbd), '[]')`);
								} else if(isFinite(parameters.updated[i].max_cbd)) {
									what.push(`cbd = numrange(lower(cbd), $${params.push(parameters.updated[i].max_cbd)}, '[]')`);
								}
								if(isFinite(parameters.updated[i].min_thc) && isFinite(parameters.updated[i].max_thc)) {
									what.push(`thc = $${params.push(`[${parameters.updated[i].min_thc},${parameters.updated[i].max_thc}]`)}`);
								} else if(isFinite(parameters.updated[i].min_thc)) {
									what.push(`thc = numrange($${params.push(parameters.updated[i].min_thc)}, upper(thc), '[]')`);
								} else if(isFinite(parameters.updated[i].max_thc)) {
									what.push(`thc = numrange(lower(thc), $${params.push(parameters.updated[i].max_thc)}, '[]')`);
								}
								if(what.length > 0) {
									await pool.query(`update menu_item set ${what.join(', ')} where store_id = $1 and menu_item_id = $2`, params);
								}
							}
						}
					}
					if(Array.isArray(parameters.deleted) && parameters.deleted.length) {
						await pool.query('delete from menu_item where menu_item_id = any($1) and store_id = $2', [parameters.deleted, parameters.store]);
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: shit
					}));
				} else if(auth === undefined) {
					if(ws.user_ID) {
						authenticated.delete(ws.user_ID);
						ws.publish('user/authenticated', JSON.stringify({
							what: 'user/authenticated',
							how: 'remove',
							data: ws.user_ID
						}));
						delete ws.user_ID;
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));//client side virtual event user null
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthorized'
					}));//client side virtual event user god false
					let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
					ws.send(JSON.stringify({
						what: 'user',
						how: 'update',
						data: {acl: r.length ? Object.fromEntries(r) : null}
					}));
				}
				break;}
			case 'modifycart':
				if(Number.isInteger(Number(parameters.menu_item))) {
					let params = [];
					let qs = [`_menu_item_id => $${params.push(parameters.menu_item)}`];
					if(Number.isInteger(Number(parameters.qty))) {
						qs.push(`qty => $${params.push(parameters.qty)}`);
					}
					if(ws.user_ID) {
						qs.push(`_user_id => $${params.push(ws.user_ID)}`);
					} else if(Array.isArray(parameters.carts)) {
						let decryptedCarts = [];
						for(let i = 0; i < parameters.carts.length; ++i) {
							try {
								decryptedCarts.push(cryptids.s2i(parameters.carts[i]));
							} catch(e) {}
						}
						if(decryptedCarts.length) {
							qs.push(`cart_ids => $${params.push(decryptedCarts)}`);
						}
					}
					let data;
					try {
						data = (await pool.query(`select modifycart(${qs.join(',')})`,
							params
						)).rows[0].modifycart;
						if(data === null) {
							data = undefined;
						} else {
							data = cryptids.i2s(data);
						}
					} catch(e) {
						data = e.message;
						//(race condition) failed to create cart. an active cart for this store and user already exist. try again, and hopefully the preexisting cart will be used
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data
					}));
				}
				break;
			case 'cart': {
				let data = 'invalid cart';
				if(ws.user_ID) {
					data = (await pool.query(`select * from authenticated_carts($1)`, [ws.user_ID])).rows
				} else if(Array.isArray(parameters.carts)) {
					let decryptedCarts = [];
					for(let i = 0; i < parameters.carts.length; ++i) {
						try {
							decryptedCarts.push(cryptids.s2i(parameters.carts[i]));
						} catch(e) {}
					}
					if(decryptedCarts.length) {
						data = (await pool.query(`select * from anonymous_carts($1)`, [decryptedCarts])).rows;
					}
				}
				if(Array.isArray(data)) {
					for(let i = 0; i < data.length; ++i) {
						data[i].cart_id = cryptids.i2s(data[i].cart_id);
					}
				}
				ws.send(JSON.stringify({
					response_ID: request_ID,
					data
				}));
				break;}
			case 'reservation': {
				let kartik;
				try {
					kartik = cryptids.s2i(parameters.cart);
				} catch(e) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'invalid cart'
					}));
					break;
				}
				let data = await pool.query('select * from reservation($1)', [kartik]);
				if(data.rowCount === 1) {
					let out;
					for(let j = 0; j < data.rows[0].statuses.length; ++j) {
						if(j === data.rows[0].statuses.length - 1 && data.rows[0].statuses[j].end !== 'infinity' && (data.rows[0].statuses[j].status === 'placed' || data.rows[0].statuses[j].status === 'ready')) {
							out = {status: 'store_cancelled', as_of: data.rows[0].statuses[j].end};
						}
						delete data.rows[0].statuses[j].end;
					}
					if(out) {
						data.rows[0].statuses.push(out);
					}
					ws.subscribe('reservations/' + parameters.cart);
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: data.rows[0]
					}));
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'invalid cart'
					}));
				}
				break;
			}
			case 'claim': {
				let kartik;
				try {
					kartik = cryptids.s2i(parameters.cart);
				} catch(e) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'invalid cart'
					}));
					break;
				}
				const auth = await is(ws);
				let n = Number.isInteger(Number(parameters.user_ID));
				if(auth === undefined) {
					if(ws.user_ID) {
						authenticated.delete(ws.user_ID);
						ws.publish('user/authenticated', JSON.stringify({
							what: 'user/authenticated',
							how: 'remove',
							data: ws.user_ID
						}));
						delete ws.user_ID;
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));//client side virtual event user null
				} else if(!auth && n) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthorized'
					}));//client side virtual event user god false
				} else {
					const id = auth && n ? parameters.user_ID : ws.user_ID;
					let r = (await pool.query(`
						update cart
						set	user_id = $1
						where
							cart_id = $2 and
							user_id is null and
							status > 'active'
						`, [id, kartik]
					));
					if(r.rowCount === 1) {
						ws.send(JSON.stringify({
							response_ID: request_ID
						}));//redirect to all of user's reservations
						ws.publish('reservations/' + parameters.cart, JSON.stringify({
							what: 'reservations/' + parameters.cart,
							how: 'replace',
							why: 'claimed'
						}));
					} else {
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: 'invalid cart'
						}));
					}
				}
				break;}
			case 'user_reservations': {
				const auth = await is(ws);
				let n = Number.isInteger(Number(parameters.user_ID));
				if(auth === undefined) {
					if(ws.user_ID) {
						authenticated.delete(ws.user_ID);
						ws.publish('user/authenticated', JSON.stringify({
							what: 'user/authenticated',
							how: 'remove',
							data: ws.user_ID
						}));
						delete ws.user_ID;
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthenticated'
					}));//client side virtual event user null
				} else if(!auth && n) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthorized'
					}));//client side virtual event user god false
				} else {
					const id = auth && n ? parameters.user_ID : ws.user_ID;
					ws.subscribe('user/reservations/' + id);
					let data = (await pool.query(`select * from user_reservations($1)`, [id])).rows;
					for(let i = 0; i < data.length; ++i) {
						data[i].cart_id = cryptids.i2s(data[i].cart_id);
						let out;
						for(let j = 0; j < data[i].statuses.length; ++j) {
							if(j === data[i].statuses.length - 1 && data[i].statuses[j].end !== 'infinity' && (data[i].statuses[j].status === 'placed' || data[i].statuses[j].status === 'ready')) {
								out = {status: 'store_cancelled', as_of: data[i].statuses[j].end};
							}
							delete data[i].statuses[j].end;
							/*if(j > 0 && data[i].statuses[j].status === data[i].statuses[j - 1].status) {
								data[i].statuses[j].status = 'claimed'
							}since `where user_id = _user_id`, this is unnecessary. would be cool though*/
						}
						if(out) {
							data[i].statuses.push(out);
						}
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data
					}));
				}
				break;}
			case 'user_update_reservation': {
				let kartik;
				try {
					kartik = cryptids.s2i(parameters.cart);
				} catch(e) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'invalid cart'
					}));
					break;
				}
				const auth = await is(ws);
				let n = Number.isInteger(Number(parameters.user_ID));
				if(auth === undefined) {
					if(ws.user_ID) {
						authenticated.delete(ws.user_ID);
						ws.publish('user/authenticated', JSON.stringify({
							what: 'user/authenticated',
							how: 'remove',
							data: ws.user_ID
						}));
						delete ws.user_ID;
					}
				}
				if(!auth && n) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'unauthorized'
					}));//client side virtual event user god false
				} else {
					const id = auth && n ? parameters.user_ID : ws.user_ID;
					let params = [id, kartik];
					let precondition;
					if(parameters.update === 'cancel') {
						params.push('user_cancelled');
						precondition = "(x.status = 'placed' or x.status = 'ready')";
					} else if(parameters.update === 'reserve') {
						params.push('placed');
						precondition = "x.status = 'active'";
					}
					let data = 'invalid update';
					if(params.length === 3) {
						//https://stackoverflow.com/a/7927957
						let r = await pool.query(`
							update cart as x
							set
								status = $3,
								user_id = case
									when x.user_id is null then $1
									else x.user_id
								end
							from (select cart_id, user_id from cart where cart_id = $2 for update) as y
							where
								x.cart_id = y.cart_id and
								(x.user_id is null or x.user_id = $1) and
								${precondition}
							returning x.system_time_start, x.store_id, y.user_id as old_user`,
							params
						);
						if(r.rowCount === 1) {
							data = undefined;
							if(parameters.update === 'cancel') {
								ws.publish('store/reservations/' + r.rows[0].store_id, JSON.stringify({
									what: 'store/reservations/' + r.rows[0].store_id,
									how: 'update',
									data: {
										cart_id: parameters.cart,
										status: 'user_cancelled',
										as_of: r.rows[0].system_time_start
									}
								}));
								if(id) {//https://github.com/uNetworking/uWebSockets.js/issues/362
									ws.publish('user/reservations/' + id, JSON.stringify({
										what: 'user/reservations/' + id,
										how: 'update',
										data: {
											cart_id: parameters.cart,
											status: 'user_cancelled',
											as_of: r.rows[0].system_time_start
										}
									}));
									if(r.rows[0].old_user === null) {
										ws.publish('reservations/' + parameters.cart, JSON.stringify({
											what: 'reservations/' + parameters.cart,
											how: 'replace',
											why: 'claimed'
										}));
									}
								} else {
									ws.publish('reservations/' + parameters.cart, JSON.stringify({
										what: 'reservations/' + parameters.cart,
										how: 'update',
										data: {
											status: 'user_cancelled',
											as_of: r.rows[0].system_time_start
										}
									}));
								}
							} else {//'reserve'
								//distinct on is because product_image may multiple number of rows
								let line_items = (await pool.query(`
									select distinct on (product_id)
										product_id,
										variant_id,
										menu_item_id as menu_item,
										producer,
										brand,
										product.display_name as name,
										type,
										strain.display_name as strain,
										coalesce(product.species, strain.species) as species,
										blend,
										portions,
										variant.quantity::text as quantity,
										flavor,
										gram_equivalency::text,
										thc,
										cbd,
										price::text,
										line_item.quantity as howmany,
										destination as image
									from
										line_item
										inner join menu_item using (menu_item_id)
										inner join variant using (variant_id)
										inner join product using (product_id)
										left outer join strain using (strain_id)
										left outer join product_image using (product_id)
									where cart_id = $1
								`, [kartik])).rows;
								let types = new Set();
								for(let i = 0; i < line_items.length; ++i) {
									types.add(line_items[i].type);
								}
								let taxes = (await pool.query(`select name, rate::text, types from store_tax where store_id = $1 and (types is null or types && $2)`, [r.rows[0].store_id, [...types]])).rows;
								ws.publish('store/reservations/' + r.rows[0].store_id, JSON.stringify({
									what: 'store/reservations/' + r.rows[0].store_id,
									how: 'add',
									data: {
										cart_id: parameters.cart,
										statuses: [{status: 'placed', as_of: r.rows[0].system_time_start}],
										taxes,
										line_items
									}
								}));
								if(id) {//https://github.com/uNetworking/uWebSockets.js/issues/362
									ws.publish('user/reservations/' + id, JSON.stringify({
										what: 'user/reservations/' + id,
										how: 'add',
										data: {
											cart_id: parameters.cart,
											statuses: [{status: 'placed', as_of: r.rows[0].system_time_start}],
											taxes,
											line_items
										}
									}));
								}
							}
						}
					}
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data
					}));
				}
				break;}
			case 'store_reservations': {
				if(Number.isInteger(Number(parameters.store))) {
					const auth = await is(ws, parameters.store);
					if(auth) {
						ws.subscribe('store/reservations/' + parameters.store);
						let data = (await pool.query(`select * from store_reservations($1)`, [parameters.store])).rows;
						for(let i = 0; i < data.length; ++i) {
							data[i].cart_id = cryptids.i2s(data[i].cart_id);
							let out;
							for(let j = data[i].statuses.length - 1; j >= 0; --j) {
								if(j === data[i].statuses.length - 1 && data[i].statuses[j].end !== 'infinity' && (data[i].statuses[j].status === 'placed' || data[i].statuses[j].status === 'ready')) {
									out = {status: 'user_cancelled', as_of: data[i].statuses[j].end};
								}
								delete data[i].statuses[j].end;
								if(j > 0 && data[i].statuses[j].status === data[i].statuses[j - 1].status) {
									data[i].statuses.splice(j, 1);
								}
							}
							if(out) {
								data[i].statuses.push(out);
							}
						}
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data
						}));
					} else if(auth === undefined) {
						if(ws.user_ID) {
							authenticated.delete(ws.user_ID);
							ws.publish('user/authenticated', JSON.stringify({
								what: 'user/authenticated',
								how: 'remove',
								data: ws.user_ID
							}));
							delete ws.user_ID;
						}
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: 'unauthenticated'
						}));//client side virtual event user null
					} else {
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: 'unauthorized'
						}));//client side virtual event user god false
						let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
						ws.send(JSON.stringify({
							what: 'user',
							how: 'update',
							data: {acl: r.length ? Object.fromEntries(r) : null}
						}));
					}
				} else {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'missing store'
					}));
				}
				break;}
			case 'store_update_reservation': {
				let out = [];
				if(!Number.isInteger(Number(parameters.store))) {
					out.push('store');
				}
				let kartik;
				try {
					kartik = cryptids.s2i(parameters.cart);
				} catch(e) {
					out.push('cart');
				}
				if(out.length) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: 'missing ' + out.join(' and ')
					}));
				} else {
					const auth = await is(ws, parameters.store);
					if(auth) {
						let params = [parameters.store, kartik];
						switch(parameters.update) {//determine precondition
							case 'ready':
								params.push('ready', 'placed');
								break;
							case 'paid':
								params.push('paid', 'ready');
								break;
							case 'cancel':
								params.push('store_cancelled', 'placed');
								break;
							case 'noshow':
								params.push('no_show', 'ready');
								break;
						}
						let data = 'invalid update';
						if(params.length === 4) {
							let query = await pool.query(
								`update cart set status = $3 where store_id = $1 and cart_id = $2 and status = $4 returning system_time_start, user_id`,
								params
							);
							if(query.rowCount === 1) {
								data = undefined;
								ws.publish('store/reservations/' + parameters.store, JSON.stringify({
									what: 'store/reservations/' + parameters.store,
									how: 'update',
									data: {
										cart_id: parameters.cart,
										status: params[2],
										as_of: query.rows[0].system_time_start
									}
								}));
								if(query.rows[0].user_id === null) {
									ws.publish('reservations/' + parameters.cart, JSON.stringify({
										what: 'reservations/' + parameters.cart,
										how: 'update',
										data: {
											status: params[2],
											as_of: query.rows[0].system_time_start
										}
									}));
								} else {
									ws.publish('user/reservations/' + query.rows[0].user_id, JSON.stringify({
										what: 'user/reservations/' + query.rows[0].user_id,
										how: 'update',
										data: {
											cart_id: parameters.cart,
											status: params[2],
											as_of: query.rows[0].system_time_start
										}
									}));
								}
							}
						}
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data
						}));
					} else if(auth === undefined) {
						if(ws.user_ID) {
							authenticated.delete(ws.user_ID);
							ws.publish('user/authenticated', JSON.stringify({
								what: 'user/authenticated',
								how: 'remove',
								data: ws.user_ID
							}));
							delete ws.user_ID;
						}
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: 'unauthenticated'
						}));//client side virtual event user null
					} else {
						ws.send(JSON.stringify({
							response_ID: request_ID,
							data: 'unauthorized'
						}));//client side virtual event user god false
						let r = (await pool.query({text: 'select store_id, owner from user_store where user_id = $1', values: [ws.user_ID], rowMode: 'array'})).rows;
						ws.send(JSON.stringify({
							what: 'user',
							how: 'update',
							data: {acl: r.length ? Object.fromEntries(r) : null}
						}));
					}
				}
				break;}
			case 'unsubscribe':
				if(parameters && typeof parameters.what === 'string' && parameters.what.length) {
					ws.send(JSON.stringify({
						response_ID: request_ID,
						data: parameters.what === 'all' ? ws.unsubscribeAll() : ws.unsubscribe(parameters.what)
					}));
				}
				break;
			default:
				console.log('unknown operation');
		}
		/* Ok is false if backpressure was built up, wait for drain */
		//let ok = ws.send(message, isBinary);
	},
	drain: (ws) => {
		console.log('WebSocket backpressure: ' + ws.getBufferedAmount());
	},
	close: async (ws, code, message) => {
		if(ws.user_ID) {
			await pool.query(`update usr set token_expiry = now() + interval '14 day' where user_id = $1`, [ws.user_ID]);
			authenticated.delete(ws.user_ID);
			app.publish('user/authenticated', JSON.stringify({
				what: 'user/authenticated',
				how: 'remove',
				data: ws.user_ID
			}));
		}
		--clients;
		app.publish('user/count', JSON.stringify({
			what: 'user/count',
			how: 'remove'
		}));
		console.log('WebSocket closed');
	}
}).any('/*', (res, req) => {
	res.end('<div>Nothing to see here!</div><script>const socket = new WebSocket("ws' + (ssl ? 's' : '') + '://" + location.hostname);</script>');
}).listen(port, μSocket => {
	if(μSocket) {
		console.log('Listening to port ' + port);
	} else {
		console.log('Failed to listen to port ' + port);
	}
});