import { Router } from 'express';
import IPCIDR from 'ip-cidr';

import runtimeId from '../../utils/runtime-id.js';
import argv from '../argv.js';

import {
	createKey,
	fetchUserKeys,
	flushUserKeys,
	login,
	removeUserKey,
	resetPassword,
	sendEmailVerification,
	sendPasswordReset,
	sendSignup,
	signup,
	updateSettings,
	updateUser,
	verifyEmail,
} from '../auth.js';

import {
	fetchUser,
} from '../users.js';

// spread into a new object rather than mutating the user we're about to hand back to the
// client (loginApi/signupApi send this straight to the browser) — runtimeId/refreshedAt are
// session bookkeeping only, they shouldn't leak into the API response
function toSession(user) {
	if (!user) {
		return null;
	}

	return {
		...user,
		runtimeId,
		refreshedAt: Date.now(),
	};
}
// concurrent requests for the same user (e.g. the burst of asset/API requests a single page
// load fires) share one in-flight refresh instead of each independently hitting the DB
const pendingSessionRefreshes = new Map();

async function refreshSessionUser(sessionUser) {
	const pending = pendingSessionRefreshes.get(sessionUser.id);

	if (pending) {
		return pending;
	}

	const refresh = fetchUser(sessionUser.id, {}, sessionUser)
		.then((user) => toSession(user))
		.finally(() => pendingSessionRefreshes.delete(sessionUser.id));

	pendingSessionRefreshes.set(sessionUser.id, refresh);

	return refresh;
}

function getIp(req) {
	if (argv.ip) {
		return argv.ip;
	}

	const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.connection.remoteAddress;

	const unmappedIp = ip?.includes('.')
		? ip.slice(ip.lastIndexOf(':') + 1)
		: ip;

	if (!unmappedIp) {
		console.log('failed unmapped ip', ip, unmappedIp);
		return null;
	}

	// ensure IP is in expanded notation for consistency and matching
	const expandedIp = unmappedIp.includes(':')
		? new IPCIDR(`${ip}/128`) // IPv6
		: new IPCIDR(`${ip}/32`); // IPv4

	if (!expandedIp.addressStart?.addressMinusSuffix) {
		throw new Error(`Could not determine user IP from ${ip}`);
	}

	return expandedIp.addressStart?.addressMinusSuffix || null;
}

// changes every time the process restarts, so a deploy invalidates all cached session users
// even if they're within the staleness window below
const sessionStaleThreshold = 5 * 60 * 1000;

function isSessionStale(sessionUser) {
	return !!sessionUser && (sessionUser.runtimeId !== runtimeId || Date.now() - sessionUser.refreshedAt > sessionStaleThreshold);
}

export async function setUserApi(req, res, next) {
	const ip = getIp(req);

	req.userIp = ip;

	if (isSessionStale(req.session.user)) {
		req.session.user = await refreshSessionUser(req.session.user);
	}

	if (req.session.user) {
		req.user = req.session.user;
		req.user.ip = ip;
	}

	next();
}

export async function updateSessionUser(req) {
	const user = toSession(await fetchUser(req.session.user.id, {}, req.session.user));

	if (user) {
		req.session.user = user;

		req.user = user;
		req.user.ip = req.userIp;
	}
}

export async function loginApi(req, res) {
	const user = await login(req.body, req.userIp);

	req.session.user = toSession(user);
	res.send(user);
}

export async function logoutApi(req, res) {
	req.session.destroy((error) => {
		if (error) {
			res.status(500).send();
		}

		res.status(204).send();
	});
}

async function sendSignupApi(req, res) {
	await sendSignup(req.body.email, req.userIp);

	res.status(204).send();
}

export async function signupApi(req, res) {
	const user = await signup(req.body, req.userIp);

	req.session.user = toSession(user);
	res.send(user);
}

async function updateUserApi(req, res) {
	const updatedUser = await updateUser(req.body, req.user, req.session);
	const sessions = await req.sessionStore.all();

	req.session.user.refreshedAt = 0;

	await Promise.all(sessions.map(async (session) => {
		if (session.user?.id === req.user.id && session.id !== req.sessionID) {
			if (req.body.credentials?.password) {
				// we changed the password, destroy all user sessions except our own (we already provided all the credentials)
				await req.sessionStore.destroy(session.id);
			}
			else {
				// mark our sessions as stale
				session.user.refreshedAt = 0; // eslint-disable-line no-param-reassign
				await req.sessionStore.set(session.id, session);
			}
		}
	}));

	res.send(updatedUser);
}

async function sendEmailVerificationApi(req, res) {
	await sendEmailVerification(req.user, null, 'verify');

	res.status(204).send();
}

async function verifyEmailApi(req, res) {
	const userId = Number(req.body.userId) || null;

	await verifyEmail(userId, req.body.token);

	const sessions = await req.sessionStore.all();

	await Promise.all(sessions.map(async (session) => {
		if (session.user?.id === userId) {
			session.user.refreshedAt = 0; // eslint-disable-line no-param-reassign
			await req.sessionStore.set(session.id, session);
		}
	}));

	res.status(204).send();
}

async function sendPasswordResetApi(req, res) {
	await sendPasswordReset(req.body.credential);

	res.status(204).send();
}

async function resetPasswordApi(req, res) {
	const userId = Number(req.body.userId) || null;

	await resetPassword(userId, req.body.token, req.body.password);

	res.status(204).send();
}

async function updateSettingsApi(req, res) {
	const updatedSettings = await updateSettings(req.body, req.user);

	req.session.user.settings = updatedSettings;

	res.send(updatedSettings);
}

export async function fetchUserKeysApi(req, res) {
	const keys = await fetchUserKeys(req.user);

	res.send(keys);
}

export async function createKeyApi(req, res) {
	const key = await createKey(req.user);

	res.send(key);
}

export async function removeUserKeyApi(req, res) {
	await removeUserKey(req.user, req.params.keyIdentifier);

	res.status(204).send();
}

export async function flushUserKeysApi(req, res) {
	await flushUserKeys(req.user);

	res.status(204).send();
}

export const authRouter = Router();

// SESSION
// authRouter.use(setUserApi);

authRouter.post('/api/session', loginApi);
authRouter.delete('/api/session', logoutApi);

// USERS
authRouter.post('/api/signups', sendSignupApi);
authRouter.post('/api/users', signupApi);

// SETTINGS
authRouter.patch('/api/me', updateUserApi);
authRouter.patch('/api/me/settings', updateSettingsApi);

// EMAIL VERIFICATION
authRouter.post('/api/me/email_verification', sendEmailVerificationApi);
authRouter.post('/api/emails/verify', verifyEmailApi); // usable without session
authRouter.post('/api/passwords/request', sendPasswordResetApi);
authRouter.post('/api/passwords/reset', resetPasswordApi);

// API KEYS
authRouter.get('/api/me/keys', fetchUserKeysApi);
authRouter.post('/api/me/keys', createKeyApi);
authRouter.delete('/api/me/keys/:keyIdentifier', removeUserKeyApi);
authRouter.delete('/api/me/keys', flushUserKeysApi);
