const dns = require('dns');
const ipaddr = require('ipaddr.js');
const { isIPv6, isIPv4 } = require('net');
const cfg = require('../../../lib/config');
const jwt = require('../../../lib/jwt');
const user = require('../../../lib/db/user');
const ServerUser = require('../../../lib/db/user/User');
const { ERROR, WARNING, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);

function parseIP(ip) {
	if (!ip || typeof ip !== 'string') {
		WARNING('Invalid IP input:', ip);
		return null;
	}
	// Handle comma-separated X-Forwarded-For (e.g., "87.90.254.127,")
	const cleanIp = ip.split(',')[0].trim();
	if (!cleanIp) {
		WARNING('Empty IP after parsing:', ip);
		return null;
	}
	// Convert IPv6-mapped IPv4 (e.g., ::ffff:127.0.0.1 -> 127.0.0.1)
	if (cleanIp.startsWith('::ffff:') && isIPv4(cleanIp.split('::ffff:')[1])) {
		return cleanIp.split('::ffff:')[1];
	}
	return cleanIp;
}

function normalizeCIDR(range) {
	if (!range || typeof range !== 'string') {
		WARNING('Invalid CIDR range:', range);
		return null;
	}
	// If no /prefix, assume /32 for IPv4 or /128 for IPv6
	if (!range.includes('/')) {
		try {
			const parsed = ipaddr.parse(range);
			const prefix = parsed.kind() === 'ipv4' ? 32 : 128;
			return `${range}/${prefix}`;
		} catch (err) {
			WARNING(`Failed to parse bare IP ${range}:`, err.message);
			return null;
		}
	}
	return range;
}

async function authorisedIP(req, res) {
	const ip = parseIP(req.ip || req.headers['x-forwarded-for'] || req.headers['x-real-ip']);
	DEBUG('authorisedIP:', { ip, headers: req.headers }); // Debug
	if (!ip) {
		WARNING('No valid IP provided:', { ip, headers: req.headers });
		return false;
	}

	let addr;
	try {
		addr = ipaddr.parse(ip);
	} catch (err) {
		WARNING('Invalid IP:', ip, err.message);
		return false;
	}

	const validIPs = await user.ip({ active: true }); // Get active IP logins
	// Attach parsed CIDR to each IP entry
	validIPs.forEach(i => {
		const normalized = normalizeCIDR(i.ip);
		if (!normalized) {
			i.$range = null;
			return;
		}
		try {
			const [rangeAddr, prefix] = ipaddr.parseCIDR(normalized);
			i.$range = { addr: rangeAddr, prefix };
		} catch (err) {
			WARNING(`Invalid CIDR range ${i.ip}:`, err.message);
			i.$range = null; // Skip invalid ranges
		}
	});
	// Filter out invalid ranges and sort by specificity (descending prefix length)
	const validRanges = validIPs.filter(i => i.$range).sort((a, b) => b.$range.prefix - a.$range.prefix);

	for (const ipEntry of validRanges) {
		const { addr: rangeAddr, prefix } = ipEntry.$range;
		try {
			if (addr.match(rangeAddr, prefix)) {
				const payload = {
					...ipEntry,
					ip,
					autologin: true
				};
				delete payload.$range;
				delete payload.hash;
				delete payload.active;
				jwt.issue(payload, req, res);
				return true;
			}
		} catch (err) {
			WARNING(`Error checking range ${ipEntry.ip}:`, err.message);
			continue;
		}
	}
	return false;
}

async function authorisedHost(req, res) {
	const ip = parseIP(req.ip || req.headers['x-forwarded-for'] || req.headers['x-real-ip']);
	DEBUG('authorisedHost:', { ip, headers: req.headers }); // Debug
	if (!ip) {
		WARNING('No valid IP for host check:', { ip, headers: req.headers });
		return false;
	}

	const validHosts = await user.host({ active: true });
	for (const key in validHosts) {
		try {
			const resolvedIPs = await dns.promises.resolve(key);
			if (resolvedIPs.includes(ip)) {
				const payload = {
					...validHosts[key],
					ip,
					autologin: true
				};
				delete payload.$block;
				delete payload.hash;
				delete payload.active;
				jwt.issue(payload, req, res);
				return true;
			}
		} catch (err) {
			if (err.code !== 'ENODATA') {
				ERROR(`DNS error for host ${key}:`, err);
			}
		}
	}
	return false;
}

async function auth(req, res, next) {
	if (res.headersSent) {
		return; // Handled by another middleware
	}

	// Check for valid JWT
	if (req.user) {
		if (!req.user.autologin && req.user.exp) {
			const ttl = req.user.exp - Date.now() / 1000;
			if (ttl < cfg.jwt.options.expiresIn / 2) {
				const credentials = await ServerUser.fromSQL(null, req.user.id);
				if (credentials) {
					const payload = Object.assign({}, credentials.toJSON());
					jwt.issue(payload, req, res);
				}
			}
		}
		next();
		return;
	}

	// Check IP and host
	if (await authorisedIP(req, res)) {
		next();
		return;
	}
	if (await authorisedHost(req, res)) {
		next();
		return;
	}

	// If *all* else fails, check if the user came with a cookie
	// (see https://gitlab.com/wgp/dougal/software/-/issues/335)
	if (req.cookies.JWT) {
		const token = req.cookies.JWT;
		delete req.cookies.JWT;
		DEBUG("falling back to cookie-based authentication");
		req.user = await jwt.checkValidCredentials({jwt: token});
		return await auth(req, res, next);
	}

	next({ status: 401, message: 'Not authorised' });
}

module.exports = auth;
