import ProxyAgent from 'https-proxy-agent';
import {isIP} from 'net';
import {Agent as HttpsAgent} from 'https';
import roundRobin from '@derhuerst/round-robin-scheduler';
import {randomBytes} from 'crypto';
import createHash from 'create-hash';
import {Buffer} from 'node:buffer';
import {stringify} from 'qs';
import {Request, fetch} from 'cross-fetch';
import {parse as parseContentType} from 'content-type';
import {HafasError, byErrorCode} from './errors.js';

const proxyAddress = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || null;
const localAddresses = process.env.LOCAL_ADDRESS || null;

if (proxyAddress && localAddresses) {
	console.error('Both env vars HTTPS_PROXY/HTTP_PROXY and LOCAL_ADDRESS are not supported.');
	process.exit(1);
}

const plainAgent = new HttpsAgent({
	keepAlive: true,
});
let getAgent = () => plainAgent;

if (proxyAddress) {
	const agent = new ProxyAgent(proxyAddress, {
		keepAlive: true,
		keepAliveMsecs: 10 * 1000, // 10s
	});
	getAgent = () => agent;
} else if (localAddresses) {
	const agents = process.env.LOCAL_ADDRESS.split(',')
		.map((addr) => {
			const family = isIP(addr);
			if (family === 0) {
				throw new Error('invalid local address:' + addr);
			}
			return new HttpsAgent({
				localAddress: addr, family,
				keepAlive: true,
			});
		});
	const pool = roundRobin(agents);
	getAgent = () => pool.get();
}

const id = randomBytes(3)
	.toString('hex');
const randomizeUserAgent = (userAgent) => {
	let ua = userAgent;
	for (
		let i = Math.round(5 + Math.random() * 5);
		i < ua.length;
		i += Math.round(5 + Math.random() * 5)
	) {
		ua = ua.slice(0, i) + id + ua.slice(i);
		i += id.length;
	}
	return ua;
};

const md5 = input => createHash('md5')
	.update(input)
	.digest();

const checkIfResponseIsOk = (_) => {
	const {
		body,
		errProps: baseErrProps,
	} = _;

	const errProps = {
		...baseErrProps,
	};
	if (body.id) {
		errProps.hafasResponseId = body.id;
	}

	// Because we want more accurate stack traces, we don't construct the error here,
	// but only return the constructor & error message.
	const getError = (_) => {
		// mutating here is ugly but pragmatic
		if (_.errTxt) {
			errProps.hafasMessage = _.errTxt;
		}
		if (_.errTxtOut) {
			errProps.hafasDescription = _.errTxtOut;
		}

		if (_.err in byErrorCode) {
			return byErrorCode[_.err];
		}
		return {
			Error: HafasError,
			message: body.errTxt || 'unknown error',
			props: {},
		};
	};

	if (body.err && body.err !== 'OK') {
		const {Error: HafasError, message, props} = getError(body);
		throw new HafasError(message, body.err, {...errProps, ...props});
	}
	if (!body.svcResL || !body.svcResL[0]) {
		throw new HafasError('invalid/unsupported response structure', null, errProps);
	}
	if (body.svcResL[0].err !== 'OK') {
		const {Error: HafasError, message, props} = getError(body.svcResL[0]);
		throw new HafasError(message, body.svcResL[0].err, {...errProps, ...props});
	}
};

const request = async (ctx, userAgent, reqData) => {
	const {profile, opt} = ctx;

	const rawReqBody = profile.transformReqBody(ctx, {
		// todo: is it `eng` actually?
		// RSAG has `deu` instead of `de`
		lang: opt.language || profile.defaultLanguage || 'en',
		svcReqL: [reqData],

		client: profile.client, // client identification
		ext: profile.ext, // ?
		ver: profile.ver, // HAFAS protocol version
		auth: profile.auth, // static authentication
	});

	const req = profile.transformReq(ctx, {
		agent: getAgent(),
		method: 'post',
		// todo: CORS? referrer policy?
		body: JSON.stringify(rawReqBody),
		headers: {
			'Content-Type': 'application/json',
			'Accept-Encoding': 'gzip, br, deflate',
			'Accept': 'application/json',
			'user-agent': profile.randomizeUserAgent
				? randomizeUserAgent(userAgent)
				: userAgent,
			'connection': 'keep-alive', // prevent excessive re-connecting
		},
		redirect: 'follow',
		query: {},
	});

	if (profile.addChecksum || profile.addMicMac) {
		if (!Buffer.isBuffer(profile.salt) && 'string' !== typeof profile.salt) {
			throw new TypeError('profile.salt must be a Buffer or a string.');
		}
		// Buffer.from(buf, 'hex') just returns buf
		const salt = Buffer.from(profile.salt, 'hex');

		if (profile.addChecksum) {
			const checksum = md5(Buffer.concat([
				Buffer.from(req.body, 'utf8'),
				salt,
			]));
			req.query.checksum = checksum.toString('hex');
		}
		if (profile.addMicMac) {
			const mic = md5(Buffer.from(req.body, 'utf8'));
			req.query.mic = mic.toString('hex');

			const micAsHex = Buffer.from(mic.toString('hex'), 'utf8');
			const mac = md5(Buffer.concat([micAsHex, salt]));
			req.query.mac = mac.toString('hex');
		}
	}

	const reqId = randomBytes(3)
		.toString('hex');
	const url = profile.endpoint + '?' + stringify(req.query);
	const fetchReq = new Request(url, req);
	profile.logRequest(ctx, fetchReq, reqId);

	const res = await fetch(url, req);

	const errProps = {
		// todo [breaking]: assign as non-enumerable property
		request: fetchReq,
		// todo [breaking]: assign as non-enumerable property
		response: res,
		url,
	};

	if (!res.ok) {
		// todo [breaking]: make this a FetchError or a HafasClientError?
		const err = new Error(res.statusText);
		Object.assign(err, errProps);
		throw err;
	}

	let cType = res.headers.get('content-type');
	if (cType) {
		const {type} = parseContentType(cType);
		if (type !== 'application/json') {
			throw new HafasError('invalid/unsupported response content-type: ' + cType, null, errProps);
		}
	}

	const body = await res.text();
	profile.logResponse(ctx, res, body, reqId);

	const b = JSON.parse(body);
	checkIfResponseIsOk({
		body: b,
		errProps,
	});

	const svcRes = b.svcResL[0].res;
	return {
		res: svcRes,
		common: profile.parseCommon({...ctx, res: svcRes}),
	};
};

export {
	checkIfResponseIsOk,
	request,
};
