'use strict';

const config = require('config');
const { setTimeout: delay } = require('timers/promises');
const bhttp = require('bhttp');
const WebSocket = require('ws');
const logger = require('simple-node-logger').createSimpleLogger();
const { argv } = require('yargs');

const {
	onMessage,
	getGames,
} = require('./play');

const instance = process.env.NODE_APP_INSTANCE || 'main';

logger.setLevel(argv.level || 'info');

async function auth() {
	const httpSession = bhttp.session();
	const username = config.uniqueUsername ? `${config.user.username}-${new Date().getTime().toString().slice(-5)}` : config.user.username;

	const res = await httpSession.post(`${config.api}/session`, {
		...config.user,
		username,
	}, {
		encodeJSON: true,
	});

	if (res.statusCode !== 200) {
		throw new Error(`Failed to authenticate: ${res.body.toString()}`);
	}

	logger.info(`Authenticated as '${username}' with ${config.api}`);

	return {
		user: res.body,
		httpSession,
		sessionCookie: res.headers['set-cookie'][0],
	};
}

async function getWsId(httpSession) {
	const res = await httpSession.get(`${config.api}/socket`);

	if (res.statusCode !== 200) {
		throw new Error(`Failed to retrieve WebSocket ID: ${res.body.toString()}`);
	}

	return res.body;
}

function onConnect(data, bot) {
	bot.socket.transmit('joinRooms', { rooms: config.channels });
}

function onRooms({ rooms, users }, bot) {
	logger.info(`Joined ${rooms.map((room) => room.name).join(', ')}`);

	/* eslint-disable no-param-reassign */
	bot.rooms = rooms.reduce((acc, room) => ({ ...acc, [room.id]: room }), {});
	bot.users = { ...bot.users, ...users };
	/* eslint-enable no-param-reassign */

	rooms.forEach((room) => {
		bot.socket.transmit('message', {
			roomId: room.id,
			body: `Hi, I am ${config.user.username}, your game host!`,
			style: config.style,
		});
	});
}

/* eslint-disable no-param-reassign */
function onJoin(data, bot) {
	if (bot.rooms[data.roomId] && !bot.rooms[data.roomId].userIds?.includes(data.user.id)) {
		bot.users[data.user.id] = data.user;
		bot.rooms[data.roomId].userIds.push(data.user.id);
	}
}

function onLeave(data, bot) {
	if (bot.rooms[data.roomId]) {
		bot.rooms[data.roomId].userIds = bot.rooms[data.roomId].userIds.filter((userId) => userId !== data.userId);
	}
}

const messageHandlers = {
	connect: onConnect,
	rooms: onRooms,
	message: onMessage,
	join: onJoin,
	leave: onLeave,
};

function handleError(error, socket, domain, data) {
	logger.error(`${domain} '${JSON.stringify(data)}' triggered error: ${error.message} ${error.stack}`);

	if (data?.roomId) {
		socket.transmit('message', {
			body: ':zap::robot::zap: Many fragments! Some large, some small.',
			type: 'message',
			roomId: data.roomId,
		});
	}
}

async function connect(bot, games) {
	const socket = { ws: { readyState: 0 } };

	socket.connect = async () => {
		try {
			const { user, httpSession, sessionCookie } = await auth();
			const wsCreds = await getWsId(httpSession);

			bot.user = user;
			bot.httpSession = httpSession;

			logger.info(`Attempting to connect to ${config.socket}`);

			socket.ws = new WebSocket(`${config.socket}?${new URLSearchParams({ v: wsCreds.wsId, t: wsCreds.timestamp }).toString()}`, [], {
				headers: {
					cookie: sessionCookie,
				},
			});

			socket.ws.on('message', async (msg) => {
				const [domain, data] = JSON.parse(msg);

				logger.debug(`Received ${domain}: ${JSON.stringify(data)}`);

				if (messageHandlers[domain]) {
					try {
						await messageHandlers[domain](data, bot, games);
					} catch (error) {
						handleError(error, socket, domain, data);
					}
				}
			});

			socket.ws.on('close', async (info) => {
				logger.error(`WebSocket closed, reconnecting in ${config.reconnectDelay} seconds: ${info}`);

				await delay(config.reconnectDelay * 1000);
				socket.connect();
			});

			socket.ws.on('error', async (error) => {
				logger.error(`WebSocket error: ${error.message}`);
			});

			logger.info(`Connected to ${config.socket}`);
		} catch (error) {
			logger.error(`Failed to connect, retrying in ${config.reconnectDelay} seconds: ${error.message}`);

			await delay(config.reconnectDelay * 1000);
			socket.connect();
		}
	};

	socket.transmit = (domain, data) => {
		socket.ws.send(JSON.stringify([domain, data]));
	};

	socket.connect();

	return socket;
}

async function init() {
	const bot = {
		rooms: [],
		users: [],
	};

	const games = await getGames(bot, instance);

	bot.socket = await connect(bot, games);
}

module.exports = init;
