import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import util from 'node:util';
import { shapes } from '@dicebear/collection';
import { createAvatar } from '@dicebear/core';
import { faker } from '@faker-js/faker';
import config from 'config';
import { verify } from 'hcaptcha';

import slugify from '../utils/slugify.js';
import { HttpError } from './errors.js';
import { knexOwner as knex } from './knex.js';
import initLogger, { initAccessLogger } from './logger.js';
import redis from './redis.js';
import { curateUser, fetchUser } from './users.js';

const logger = initLogger();
const accessLogger = initAccessLogger();
const scrypt = util.promisify(crypto.scrypt);

async function verifyPassword(password, storedPassword) {
	const [salt, hash] = storedPassword.split('/');
	const hashedPassword = (await scrypt(password, salt, 64)).toString('hex');

	if (hashedPassword === hash) {
		return true;
	}

	throw new HttpError('Username or password incorrect', 401);
}

async function generateAvatar(user) {
	const avatar = createAvatar(shapes, {
		seed: user.username,
		backgroundColor: ['f65596', '9b004b', '006b68', '5abab6'],
		shape1Color: ['f65596', 'ff6d7e', 'ff8d69', 'ffb15b', 'ffd55c', 'f9f871'],
		shape2Color: ['c162c6', '6074dd', '007dd2', '007ba9', '007170'],
		shape3Color: ['f65596', 'ff6d7e', 'ff8d69', 'ffb15b', 'ffd55c', 'f9f871'],
	});

	await fs.mkdir('media/avatars', { recursive: true });
	await avatar.png().toFile(`media/avatars/${user.id}_${user.username}.png`);

	logger.verbose(`Generated avatar for '${user.username}' (${user.id})`);
}

export async function login(credentials, userIp) {
	if (!config.auth.login) {
		throw new HttpError('Logins are currently disabled', 405);
	}

	const { user } = await fetchUser(credentials.username.trim(), {
		email: true,
		raw: true,
	}).catch(() => {
		throw new HttpError('Username or password incorrect', 401);
	});

	await verifyPassword(credentials.password, user.password);

	await knex('users')
		.update('last_login', 'NOW()')
		.update('last_ip', userIp)
		.where('id', user.id);

	logger.verbose(`Login from '${user.username}' (${user.id}, ${userIp})`);

	try {
		await fs.access(`media/avatars/${user.id}_${user.username}.png`);
	}
	catch (error) {
		await generateAvatar(user);
	}

	// fetched the raw user for password verification, don't return directly to user
	return curateUser(user, user);
}

export async function signup(credentials, userIp) {
	if (!config.auth.signup) {
		throw new HttpError('Sign-ups are currently disabled', 405);
	}

	const curatedUsername = credentials.username.trim();

	if (!curatedUsername) {
		throw new HttpError('Username required', 400);
	}

	if (curatedUsername.length < config.auth.usernameLength[0]) {
		throw new HttpError('Username is too short', 400);
	}

	if (curatedUsername.length > config.auth.usernameLength[1]) {
		throw new HttpError('Username is too long', 400);
	}

	if (!config.auth.usernamePattern.test(curatedUsername)) {
		throw new HttpError('Username contains invalid characters', 400);
	}

	if (!credentials.email) {
		throw new HttpError('E-mail required', 400);
	}

	if (credentials.password?.length < 3) {
		throw new HttpError('Password must be 3 characters or longer', 400);
	}

	if (config.auth.captcha.enabled) {
		const captchaVerification = await verify(config.auth.captcha.secretKey, credentials.captcha);

		if (!captchaVerification.success) {
			logger.warn(`Invalid sign-up CAPTCHA from '${curatedUsername}' (${credentials.email}, ${userIp})`);
			throw new HttpError('Invalid CAPTCHA', 400);
		}
	}

	const existingUser = await knex('users')
		.where(knex.raw('lower(username)'), curatedUsername.toLowerCase())
		.orWhere(knex.raw('lower(email)'), credentials.email.toLowerCase())
		.first();

	if (existingUser) {
		throw new HttpError('Username or e-mail already in use', 409);
	}

	const salt = crypto.randomBytes(16).toString('hex');
	const hashedPassword = (await scrypt(credentials.password, salt, 64)).toString('hex');
	const storedPassword = `${salt}/${hashedPassword}`;

	const [{ id: userId }] = await knex('users')
		.insert({
			username: curatedUsername,
			email: credentials.email,
			password: storedPassword,
		})
		.returning('id');

	await knex('stashes').insert({
		user_id: userId,
		name: 'Favorites',
		slug: 'favorites',
		public: false,
		primary: true,
	});

	logger.info(`Signup from '${curatedUsername}' (${userId}, ${credentials.email}, ${userIp})`);

	await generateAvatar({
		id: userId,
		username: curatedUsername,
	});

	return fetchUser(userId);
}

function curateKey(key) {
	return {
		id: key.id,
		identifier: key.identifier,
		lastUsedAt: key.last_used_at,
		lastUsedIp: key.last_used_ip,
		createdAt: key.created_at,
	};
}

export async function fetchUserKeys(reqUser) {
	const keys = await knex('users_keys')
		.where('user_id', reqUser.id)
		.orderBy('created_at', 'asc');

	return keys.map((key) => curateKey(key));
}

export async function verifyKey(userId, key, req) {
	if (!key || !userId) {
		throw new HttpError('The API credentials are not provided.', 401);
	}

	const hashedKey = (await scrypt(key, '', 64)).toString('hex'); // salt redundant for randomly generated key

	const storedKey = await knex('users_keys')
		.where('user_id', userId)
		.where('key', hashedKey)
		.first();

	if (!storedKey) {
		throw new HttpError('The API credentials are invalid.', 401);
	}

	accessLogger.access({
		userId,
		identifier: storedKey.identifier,
		ip: req.userIp,
		path: req.path,
		userAgent: req.headers['user-agent'],
	});

	knex('users_keys')
		.where('id', storedKey.id)
		.update('last_used_at', knex.raw('now()'))
		.update('last_used_ip', req.userIp)
		.then(() => {
			// no need to wait for this
		});

	return fetchUser(storedKey.user_id);
}

export async function createKey(reqUser) {
	const cooldownKey = `traxxx:key_create_cooldown:${reqUser.id}`;

	if (reqUser.role !== 'admin' && await redis.exists(cooldownKey)) {
		throw new HttpError(`You can only create a new API key once every ${config.apiAccess.keyCooldown} minutes.`, 429);
	}

	const keys = await fetchUserKeys(reqUser);

	if (keys.length >= config.apiAccess.keyLimit) {
		throw new HttpError(`You can only hold ${config.apiAccess.keyLimit} API keys at one time. Please remove a key.`, 400);
	}

	const key = crypto.randomBytes(config.apiAccess.keySize).toString('base64url');
	const hashedKey = (await scrypt(key, '', 64)).toString('hex'); // salt redundant for randomly generated key

	const identifier = slugify([faker.word.adjective(), faker.animal[faker.animal.type()]()]);

	const [newKey] = await knex('users_keys')
		.insert({
			user_id: reqUser.id,
			key: hashedKey,
			identifier,
		})
		.returning('*');

	await redis.set(cooldownKey, identifier);
	await redis.expire(cooldownKey, config.apiAccess.keyCooldown * 60);

	return {
		...curateKey(newKey),
		key,
	};
}

export async function removeUserKey(reqUser, identifier) {
	await knex('users_keys')
		.where('user_id', reqUser.id)
		.where('identifier', identifier)
		.delete();
}

export async function flushUserKeys(reqUser) {
	await knex('users_keys')
		.where('user_id', reqUser.id)
		.delete();
}
