'use strict';

const ipaddr = require('ipaddr.js');
const winston = require('winston');
const _ = require('lodash');
const validator = require('validator');

const db = require('../database');
const pubsub = require('../pubsub');
const plugins = require('../plugins');
const analytics = require('../analytics');

const Blacklist = module.exports;
Blacklist._rules = {};

Blacklist.load = async function () {
	let rules = await Blacklist.get();
	rules = Blacklist.validate(rules);

	winston.verbose(`[meta/blacklist] Loading ${rules.valid.length} blacklist rule(s)${rules.duplicateCount > 0 ? `, ignored ${rules.duplicateCount} duplicate(s)` : ''}`);
	if (rules.invalid.length) {
		winston.warn(`[meta/blacklist] ${rules.invalid.length} invalid blacklist rule(s) were ignored.`);
	}

	Blacklist._rules = {
		ipv4: rules.ipv4,
		ipv6: rules.ipv6,
		cidr: rules.cidr,
		cidr6: rules.cidr6,
	};
};

pubsub.on('blacklist:reload', Blacklist.load);

Blacklist.save = async function (rules) {
	await db.setObject('ip-blacklist-rules', { rules: rules });
	await Blacklist.load();
	pubsub.publish('blacklist:reload');
};

Blacklist.get = async function () {
	const data = await db.getObject('ip-blacklist-rules');
	return data && data.rules;
};

Blacklist.test = async function (clientIp) {
	// Some handy test addresses
	// clientIp = '2001:db8:85a3:0:0:8a2e:370:7334';	// IPv6
	// clientIp = '127.0.15.1';	// IPv4
	// clientIp = '127.0.15.1:3443'; // IPv4 with port strip port to not fail
	if (!clientIp) {
		return;
	}
	clientIp = clientIp.split(':').length === 2 ? clientIp.split(':')[0] : clientIp;

	let addr;
	try {
		addr = ipaddr.parse(clientIp);
	} catch (err) {
		winston.error(`[meta/blacklist] Error parsing client IP : ${clientIp}`);
		throw err;
	}

	if (
		!Blacklist._rules.ipv4.includes(clientIp) &&	// not explicitly specified in ipv4 list
		!Blacklist._rules.ipv6.includes(clientIp) &&	// not explicitly specified in ipv6 list
		!Blacklist._rules.cidr.some((subnet) => {
			const cidr = ipaddr.parseCIDR(subnet);
			if (addr.kind() !== cidr[0].kind()) {
				return false;
			}
			return addr.match(cidr);
		})	// not in a blacklisted IPv4 or IPv6 cidr range
	) {
		try {
			// To return test failure, pass back an error in callback
			await plugins.hooks.fire('filter:blacklist.test', { ip: clientIp });
		} catch (err) {
			analytics.increment('blacklist');
			throw err;
		}
	} else {
		const err = new Error('[[error:blacklisted-ip]]');
		err.code = 'blacklisted-ip';

		analytics.increment('blacklist');
		throw err;
	}
};

Blacklist.validate = function (rules) {
	rules = (rules || '').split('\n');
	const ipv4 = [];
	const ipv6 = [];
	const cidr = [];
	const invalid = [];
	let duplicateCount = 0;

	const inlineCommentMatch = /#.*$/;
	const whitelist = ['127.0.0.1', '::1', '::ffff:0:127.0.0.1'];

	// Filter out blank lines and lines starting with the hash character (comments)
	// Also trim inputs and remove inline comments
	rules = rules.map((rule) => {
		rule = rule.replace(inlineCommentMatch, '').trim();
		return rule.length && !rule.startsWith('#') ? rule : null;
	}).filter(Boolean);

	// Filter out duplicates
	const uniqRules = _.uniq(rules);
	duplicateCount += rules.length - uniqRules.length;
	rules = uniqRules;

	// Filter out invalid rules
	rules = rules.filter((rule) => {
		let addr;
		let isRange = false;
		try {
			addr = ipaddr.parse(rule);
		} catch (e) {
			// Do nothing
		}

		try {
			addr = ipaddr.parseCIDR(rule);
			isRange = true;
		} catch (e) {
			// Do nothing
		}

		if (!addr || whitelist.includes(rule)) {
			invalid.push(validator.escape(rule));
			return false;
		}

		if (!isRange) {
			if (addr.kind() === 'ipv4' && ipaddr.IPv4.isValid(rule)) {
				ipv4.push(rule);
				return true;
			}
			if (addr.kind() === 'ipv6' && ipaddr.IPv6.isValid(rule)) {
				ipv6.push(rule);
				return true;
			}
		} else {
			cidr.push(rule);
			return true;
		}
		return false;
	});

	return {
		numRules: rules.length + invalid.length,
		ipv4: ipv4,
		ipv6: ipv6,
		cidr: cidr,
		valid: rules,
		invalid: invalid,
		duplicateCount: duplicateCount,
	};
};

Blacklist.addRule = async function (rule) {
	const { valid } = Blacklist.validate(rule);
	if (!valid.length) {
		throw new Error('[[error:invalid-rule]]');
	}
	let rules = await Blacklist.get();
	rules = `${rules}\n${valid[0]}`;
	await Blacklist.save(rules);
};
