const EventEmitter = require('events');
const Discord = require('discord.js');
const Status = require('./status');
const isValidDomain = require('is-valid-domain');

class Bot extends EventEmitter {

	constructor(token, database, error, login) {

		super();

		this.lCache = {};
		this.sCache = {};

		this.database = database;
		this.discord = new Discord.Client();

		this.discord.on('message', message => this.message(message));
		this.discord.on('guildCreate', guild => this.join(guild));
		this.discord.on('guildDelete', guild => this.leave(guild));

		this.discord.on('ready', () => {
			this.link = `https://discordapp.com/oauth2/authorize?&client_id=${this.discord.user.id}&scope=bot&permissions=19456`;
			login(this.discord.user);
		});

		this.discord.login(token).catch(e => error(e));

	}

	join(guild) {
		this.emit('join', guild);
	}

	leave(guild) {
		this.emit('leave', guild);
	}

	message(message) {

		// Messages produced by the bot.
		if (message.author === this.discord.user) {
			this.emit('message', message);
			return;
		}

		if (message.author.bot) {
			return;
		}

		const direct = message.channel.type === 'dm' ||
			message.mentions.has(this.discord.user) && !message.mentions.everyone;

		if (direct) {
			this.command(message);
		} else {

			// Status command is only one without need of being mentioned or DMed.
			const matches = message.content
				.replace(/<[^@]*@[^>]+>/g, '')
				.replace(/\s+/g, ' ')
				.trim()
				.match(/^!([a-z]+)/i);

			if (matches) {
				this.database.isGuildStatusCommand(message.guild.id, matches[1], matched => {
					if (matched) {
						this.command(message);
					}
				});
			}

		}

	}

	command(message) {

		this.emit('message', message);

		let content = message.content
			.replace(/<[^@]*@[^>]+>/g, '')
			.replace(/\s+/g, ' ')
			.replace(/^[! ]*/, '')
			.trim();

		const argv = content.split(' ');
		const command = argv[0].toLowerCase();

		if (command === 'help' && (message.channel.type === 'dm' || message.mentions.has(this.discord.user))) {
			this.help(message);
			return;
		}

		if (command === 'ip' || command === 'set' || command === 'reset' || command === 'get') {

			if (message.channel.type !== 'dm') {
				message.reply('Pssh! Send me that via direct message, someone could see us. :open_mouth:');
				return;
			}

			const guilds = this.getAdminGuilds(message.author);

			if (guilds.length === 0) {
				message.reply('You are not an owner on any servers we share.');
				return;
			}

			let guild = null;

			if (guilds.length > 1) {

				if (argv.length < 2) {
					message.reply('Specify Discord server as second argument, e.g.: `' + command + ' <server ID>`');
					return;
				}

				for (let aGuild of guilds) {
					if (aGuild.id === argv[1]) {
						guild = aGuild;
					}
				}

				if (guild === null) {
					message.reply('Invalid Discord server ID specified in second argument, usage: `' + command + ' <server ID>`');
					return;
				}

				argv.splice(1, 1);

			} else {
				guild = guilds[0];
			}

			this[command](message, guild, argv.slice(1));
			return;

		}

		if (message.channel.type === 'dm') {

			if (command === 'status' || command === 'getstatus' || command === 'serverstatus') {
				this.status(message, argv.slice(1));
				return;
			}

			message.react('πŸ™„').catch(() => {});
			return;

		}

		if (message.guild && (message.mentions.has(this.discord.user) || message.content
			.replace(/<[^@]*@[^>]+>/g, '')
			.replace(/\s+/, ' ')
			.startsWith('!'))) {

			this.database.isGuildStatusCommand(message.guild.id, command, matched => {

				if (matched) {
					this.status(message, argv.slice(1));
				} else if (message.mentions.has(this.discord.user)) {
					message.react('πŸ™„').catch(() => {});
				}

			});

		}

	}

	ip(message, guild, argv) {

		switch (argv.length > 0 ? argv[0] : null) {

			case 'get':
				this.ipGet(message, guild, argv.slice(1));
				break;

			case 'set':
				this.ipSet(message, guild, argv.slice(1));
				break;

			case 'reset':
				this.ipReset(message, guild, argv.slice(1));
				break;

			default:
				message.reply('Usage: **ip** `<get|set|reset> <alias>`');
				break;

		}

	}

	ipGet(message, guild, argv) {

		if (argv.length === 0) {

			this.database.ipGetAliases(guild.id, aliases => {
				if (aliases.length > 0) {
					message.reply('Usage: **ip get** `<' + aliases.join('|') + '>`');
				} else {
					message.reply('No servers added so far. Fix that by `ip set <alias>`.');
				}
			});

			return;

		}

		if (!argv[0].match(/^[a-z0-9_]+$/i)) {
			message.reply('Alias can only contain alphanumeric characters and underscores.');
			return;
		}

		this.database.ipGet(guild.id, argv[0], ip => {
			if (ip.length > 0) {
				message.reply(ip.map(address => {
					return address.password ? address.password + '@' + address.address : address.address;
				}).join(' '));
			} else {
				message.reply('This alias is not defined. Fix that by `ip set ' + argv[0] + '`.');
			}
		});

	}

	ipSet(message, guild, argv) {

		if (argv.length === 0) {
			message.reply('Usage: **ip set** `<alias>`');
			return;
		}

		if (!argv[0].match(/^[a-z0-9_]+$/i)) {
			message.reply('Alias can only contain alphanumeric characters and underscores.');
			return;
		}

		if (argv.length < 2) {
			message.reply('Usage: **ip set ' + argv[0] + '** `ip.or.hostname:port [password@another.ip:port, ...]`');
			return;
		}

		const ip = [];

		for (let i = 1; i < argv.length; i++) {

			let password = null;
			let addr = argv[i];

			if (addr.indexOf('@') > 0) {
				password = addr.substring(0, addr.indexOf('@'));
				addr = addr.substring(addr.indexOf('@') + 1);
			}

			let address = Bot.parseAddress(addr);

			if (address === null) {
				message.reply('Address `' + addr + '` seems to be invalid.');
				return;
			}

			if (ip.indexOf(address) === -1) {
				ip.push({
					address: address,
					password: password,
				});
			}

		}

		if (ip.length > 12) {
			message.reply(`That's too much, buddy! You can have up to 12 servers per alias, consider using multiple aliases instead.`);
			return;
		}

		if (ip.length > 0) {

			this.database.ipGetAliases(guild.id, aliases => {

				if (aliases.length >= 10 && aliases.indexOf(argv[0].toLowerCase()) === -1) {
					message.reply(`You have reached aliases limit (10).`);
					return;
				}

				this.database.ipSet(guild.id, argv[0], ip);
				message.reply('OK, saved.');

			});

		}

	}

	ipReset(message, guild, argv) {

		if (argv.length === 0) {

			this.database.ipGetAliases(guild.id, aliases => {
				if (aliases.length > 0) {
					message.reply('Usage: **ip reset** `<' + aliases.join('|') + '>`');
				} else {
					message.reply('No servers added so far.');
				}
			});

			return;

		}

		if (!argv[0].match(/^[a-z0-9_]+$/i)) {
			message.reply('Alias can only contain alphanumeric characters and underscores.');
			return;
		}

		this.database.ipGetAliases(guild.id, aliases => {
			if (aliases.indexOf(argv[0].toLowerCase()) > -1) {
				this.database.ipReset(guild.id, argv[0]);
				message.reply('OK, reset.');
			} else {
				message.reply('That alias doesn\' exist. Usage: **ip reset** `<' + aliases.join('|') + '>`');
			}
		});

	}

	get(message, guild, argv) {

		if (argv.length === 0) {
			message.reply('Usage: **get** `<enabled|command>`');
			return;
		}

		switch (argv[0].toLowerCase()) {

			case 'enabled':
				this.database.directiveGet(guild.id, 'enabled', value => {
					if (value === 0) {
						message.reply(`It's disabled.`);
					} else {
						message.reply(`It's enabled.`);
					}
				});
				break;

			case 'command':
				this.database.directiveGet(guild.id, 'command', value => {
					if (value !== null) {
						message.reply('The command is `!' + value + '`.');
					} else {
						message.reply('No command defined, so it\' the default `!status`, `!getstatus` or `!serverstatus`.');
					}
				});
				break;

			default:
				message.reply('Unknown directive `' + argv[0] + '`.');
				break;

		}

	}

	set(message, guild, argv) {

		if (argv.length === 0) {
			message.reply('Usage: **set** `<enabled|command>`');
			return;
		}

		switch (argv[0].toLowerCase()) {

			case 'enabled':

				if (argv.length < 2 || !argv[1].match(/^[01]$/)) {
					message.reply('Usage: **set enabled** `<0|1>`.');
					return;
				}

				this.database.directiveSet(guild.id, 'enabled', parseInt(argv[1]));
				message.reply('OK, bot is **' + (argv[1] === '1' ? 'enabled' : 'disabled') + '**. Max Hass.');

				break;

			case 'command':

				if (argv.length < 2) {
					message.reply('Usage: **set command** `<command>`.');
					return;
				}

				if (!argv[1].match(/^[a-z]+$/)) {
					message.reply('Command must only contain lowercase letters.');
					return;
				}

				this.database.directiveSet(guild.id, 'command', argv[1]);
				message.reply('OK, command has been changed.');

				break;

			default:
				message.reply('Unknown directive `' + argv[0] + '`.');
				break;

		}

	}

	reset(message, guild, argv) {

		if (argv.length === 0) {
			message.reply('Usage: **reset** `<enabled|command>`');
			return;
		}

		switch (argv[0].toLowerCase()) {

			case 'enabled':
				this.database.directiveSet(guild.id, 'enabled', 1);
				message.reply('OK, bot is **enabled**. Max Hass.');
				break;

			case 'command':
				this.database.directiveSet(guild.id, 'command', null);
				message.reply('OK, default command was restored.');
				break;

			default:
				message.reply('Unknown directive `' + argv[0] + '`.');
				break;

		}

	}

	status(message, argv) {

		if (argv.length > 0) {

			const address = Bot.parseAddress(argv[0]);

			if (address) {
				this.sendStatusFor(message, [{address: address}]);
			} else {

				if (!message.guild) {
					message.react('πŸ™„').catch(() => {});
					return;
				}

				this.database.ipGet(message.guild.id, argv[0], ip => {

					if (ip.length === 0) {
						message.react('πŸ™„').catch(() => {});
					} else {
						this.sendStatusFor(message, ip);
					}

				});

			}

			return;

		}

		if (message.guild) {

			this.database.ipGet(message.guild.id, 'default', ip => {

				if (ip.length === 0) {
					message.react('πŸ™„').catch(() => {});
				} else {
					this.sendStatusFor(message, ip);
				}

			});

			return;

		}

		const common = [];

		for (let shared of this.discord.guilds.cache.array()) {
			for (let member of shared.members.cache.array()) {
				if (member.user === message.author) {
					common.push(shared.id);
					break;
				}
			}
		}

		this.database.getSharedIP(common, ip => {

			if (ip.length === 0) {
				message.react('πŸ™„').catch(() => {});
			} else {
				this.sendStatusFor(message, ip.map(i => {
					return {address: i};
				}));
			}

		});

	}

	sendStatusFor(message, addresses) {

		if (addresses.length === 0) {
			return;
		}

		this.buildResponseMessage(addresses, (text, success) => {

			message.reply(text).then(sent => {

				if (!success && sent.editable) {

					setTimeout(() => {

						this.buildResponseMessage(addresses, (textRetry, successRetry) => {

							if (text === textRetry) {
								return;
							}

							if (message.channel.type !== 'dm') {
								textRetry = '<@' + message.author.id + '>,' + textRetry;
							}

							sent.edit(textRetry).catch(e => {});

						}, 3000);

					}, 8000);

				}

			}).catch(e => {

				message.author
					.send(text)
					.catch(e => {});

				message.react('πŸ€”').catch(e => {});

			});

		});

	}

	buildResponseMessage(addresses, callback, timeout = 1000) {

		const responses = {};
		let counter = addresses.length;

		const create = () => {

			const lines = [];
			let online = true;

			for (let address of addresses) {

				const response = responses[address.address] || this.lCache[address.address];

				if (!responses[address.address]) {
					online = false;
				}

				let line = '';

				if (response && ('g_needpass' in response.info) && parseInt(response.info.g_needpass) > 0 || address.password) {
					line += ':lock: ';
				}

				if (response) {

					line += '**' + Discord.Util.escapeMarkdown(response.info.sv_hostname.trim()) + '**, ';

					if ('mapname' in response.info) {
						line += 'map *' + Discord.Util.escapeMarkdown(response.info.mapname) + '*, ';
					}

					const privateClients = ('sv_privateclients' in response.info) && response.info.sv_privateclients.match(/^\d+$/)
						? parseInt(response.info.sv_privateclients)
						: 0;

					const maxClients = ('sv_maxclients' in response.info) && response.info.sv_maxclients.match(/^\d+$/)
						? parseInt(response.info.sv_maxclients) - privateClients
						: 0;

					const activeClients = response.clients
						.filter(client => client.ping > 0)
						.length;

					if (activeClients > 0) {
						line += `**${activeClients}/${maxClients}**`;
					} else {
						line += `${activeClients}/${maxClients}`;
					}

					if (privateClients > 0) {
						line += '+' + privateClients;
					}

					if (!responses[address.address]) {
						line = ':x: ' + line;
					}

				}

				if (!response) {
					line += `**${address.address}**`;
					line = ':x: ' + line;
				}

				const p = address.address.split(':');
				line += ' `connect ';

				if (p[1] === '27960') {
					line += p[0];
				} else {
					line += address.address;
				}

				if (address.password) {
					line += ' ; password ' + Discord.Util.escapeMarkdown(address.password);
				}

				line += '`';
				lines.push(line);

			}

			callback('\n' + lines.join('\n'), online);

		};

		for (let address of addresses) {

			if (address.address in this.sCache) {
				responses[address.address] = this.sCache[address.address];
				counter--;
				continue;
			}

			const p = address.address.split(':');

			Status.status(p[0], parseInt(p[1]), (error, response) => {

				responses[address.address] = response;

				if (response !== null) {

					this.sCache[address.address] = response;
					this.lCache[address.address] = response;

					setTimeout(() => delete this.sCache[address.address], 30000);

				}

				counter--;

				if (counter === 0) {
					create();
				}

			}, timeout);

		}

		if (counter === 0) {
			create();
		}

	}

	help(message) {

		if (message.channel.type !== 'dm') {

			this.database.getGuildStatusCommand(message.guild.id, command => {

				this.database.ipGetAliases(message.guild.id, aliases => {

					if (aliases.length === 0) {
						return;
					}

					const ndAliases = aliases.filter(a => a !== 'default');
					let append = '';

					if (ndAliases.length > 0) {
						append = ndAliases.join('|');
					}

					if (aliases.indexOf('default') > -1 && append !== '') {
						append = '[' + append + ']';
					}

					if (append !== '') {
						append = ' ' + append;
					}

					message.reply('`!' + command + append + '`');

				});

			});

			return;

		}

		if (this.getAdminGuilds(message.author).length === 0) {
			message.reply('`!status <ip:port>`');
			return;
		}

		message.reply(
			'**Servers configuration**\n' +
			'`ip set default xx.xx.xx.xx:27xxx [password@xx.xx.xx.xx.xx:27xxy ...]`\n' +
			'This will list status of servers (you can add more addresses delimited by a space) on `!status` command without any arguments.\n' +
			'You can define other aliases, such as `tj`, just replace the `default` alias:\n' +
			'`ip set tj xx.xx.xx.xx:27xxx`\n' +
			'That will make `!status tj` command working. Again, you can list multiple addresses.\n' +
			'Call the `ip set <alias>` again for change. You can also list addresses of an alias with `ip get <alias>` or remove an alias completely with `ip reset <alias>`.\n\n' +
			'**Directives**\n' +
			'`set command whatzup` - replaces `!status` with `!whatzup`\n' +
			'`set enabled 0` - disables me, use that as a kill switch\n' +
			'`reset <directive>` - resets original directive value\n' +
			'`get <directive>` - shows current directive value\n' +
			'\nSupport: AΠΌΠΊ#6633 (https://klva.cz/code/amk/etwolf-maxhass)'
		);

	}

	getAdminGuilds(user) {
		return this.discord.guilds
			.cache
			.filter(guild => guild.ownerID === user.id || guild.members.cache.some(member => member.user === user && member.hasPermission("ADMINISTRATOR")))
			.array();
	}

	static parseAddress(address) {

		address += ':27960';

		const parts = address.split(':');

		if (!Status.isIP(parts[0]) && !isValidDomain(parts[0])) {
			return null;
		}

		if (!parts[1].match(/^\d+$/)) {
			return null;
		}

		return parts[0] + ':' + parts[1];

	}

}

module.exports = Bot;