const jwt = require('jsonwebtoken');
const passport = require('passport');
const {
	Users,
	sequelize,
	Listings,
	Discounts,
	HostReviews,
	CustomerReviews,
	Bookings,
	HostsRegistrations,
	ListingImages,
	HostRegistrationFiles,
	DeletedAccounts,
	PasswordResetTokens,
	FavoriteFolderListings,
	FavoriteFolders,
	HostNotifications,
	UsersFlags,
	UsersPrivacyFlags,
} = require('../../models');
const { errorHandler, formatTimestamp } = require('../../util');
const { deleteFile, updateOrCreateToken } = require('../../helpers');
const { QueryTypes, Op } = require('sequelize');
const { sendEmail, forgotPasswordEmail } = require('../../services/emailService');

exports.accountLogin = (req, res, next) => {
	passport.authenticate('login', async (err, user, info) => {
		if (err) return next(err);
		if (!user) return res.status(401).json({ success: false, message: info.message });

		try {
			const { token, refreshToken } = await updateOrCreateToken(user.id, req, {
				isHost: user.is_host,
				hasPendingApplication: user.has_pending_host_application,
			});

			res.json({
				success: true,
				token,
				refreshToken,
				user: {
					firstname: user.firstname,
					lastname: user.lastname,
					email: user.email_address,
					phoneNo: user.phone_no,
					isHost: user.is_host,
				},
			});
		} catch (error) {
			next(error);
		}
	})(req, res, next);
};

exports.accountCreate = (req, res, next) => {
	passport.authenticate('signup', { session: false }, (err, user, info) => {
		if (err) {
			return next(err);
		}
		if (!user) {
			return res.status(400).json({ success: false, message: info.message });
		}

		const token = jwt.sign({ userId: user.id }, process.env.SECRETKEY, {
			expiresIn: '2h',
		});

		res.json({ success: true, token });
	})(req, res, next);
};

exports.getUserById = (req, res, next) => {
	const { userId } = req;

	Users.findOne({
		where: {
			id: userId,
		},
	})
		.then((user) => {
			if (!user) {
				return errorHandler('User not found!', 404);
			}

			const profile = {
				id: user.id,
				firstName: user.firstname,
				lastName: user.lastname,
				email: user.email_address,
				phoneNo: user.phone_no,
				birthdate: user.birthdate,
				isNumVerified: user.is_phone_verified,
				isHost: user.is_host,
				address: user.address,
				addressLat: user.address_lat,
				addressLong: user.address_long,
				image: (imageUrl = user.image ? `${process.env.BASE_URL}/public/user-uploads/${user.image}` : null),
				addressCountry: user.address_country,
				addressStreet: user.address_street,
				addressApt: user.address_apt,
				addressCity: user.address_city,
				addressState: user.address_state,
				addressZip: user.address_zip,
				validIdType: user.valid_id_type,
				validIdImage: user.valid_id_image ? `${process.env.BASE_URL}/public/user-uploads/${user.valid_id_image}` : null,
				status: user.status,
				isFb: user.is_facebook,
				isGoogle: user.is_google,
				isApple: user.is_apple,
			};

			return res.status(200).json({ success: true, profile });
		})
		.catch((err) => {
			next(err);
		});
};

exports.editUserProfile = (req, res, next) => {
	const { userId } = req;
	const { data } = req.body;
	const { validIdType } = data;
	const { profileImg, validIdImg } = req.files || {};

	Users.findOne({
		where: {
			id: userId,
		},
	})
		.then((user) => {
			if (!user) {
				return res.status(404).json({ success: false, message: 'User not found!' });
			}

			if (validIdImg?.length > 0 || validIdType) {
				if (!(validIdImg?.length > 0 && validIdType)) {
					if (validIdImg && validIdImg.length > 0) {
						deleteFile(validIdImg[0].filename, 'user-uploads');
					}
					return res.status(400).json({
						success: false,
						message: 'Valid ID Type and Image must be passed both if it needs to be updated!',
					});
				}
			}

			const columnsData = {
				firstName: 'firstname',
				lastName: 'lastname',
				phoneNo: 'phone_no',
				emailAddress: 'email_address',
				birthdate: 'birthdate',
				addressLong: 'address_long',
				addressLat: 'address_lat',
				addressCountry: 'address_country',
				addressStreet: 'address_street',
				addressApt: 'address_apt',
				addressCity: 'address_city',
				addressState: 'address_state',
				addressZip: 'address_zip',
				aboutMe: 'about_me',
				school: 'school',
				work: 'work',
				music: 'music',
				livedIn: 'lived_in',
				languages: 'languages',
				pets: 'pets',
				hangouts: 'hangouts',
				birthYear: 'birth_year',
				likes: 'likes',
				dislikes: 'dislikes',
				interests: 'interests',
				showTravels: 'show_travels',
			};

			for (const key in columnsData) {
				if (key in data) {
					let value = data[key];
					if (['interests', 'languages'].includes(key)) {
						value = JSON.stringify(value);
					}
					const columnKey = columnsData[key];
					user[columnKey] = value;
				}
			}

			if (validIdImg && validIdType && validIdImg.length > 0) {
				const previousValidIdImage = user.valid_id_image || null;

				user.valid_id_type = validIdType;
				user.valid_id_image = validIdImg[0].filename || null;

				if (previousValidIdImage) {
					deleteFile(previousValidIdImage, 'user-uploads');
				}
			}

			if (profileImg && profileImg.length > 0) {
				user.image = profileImg[0].filename || null;
			}

			user.save();

			return res.status(200).json({ success: true, message: 'Profile updated successfully' });
		})

		.catch((err) => {
			next(err);
			if (validIdImg?.length > 0) {
				deleteFile(req.files.validIdImg[0].filename, 'user-uploads');
			}
		});
};

exports.changePassword = (req, res, next) => {
	const { userId } = req;
	const { currentPass, newPass, confirmPass } = req.body;

	if (newPass !== confirmPass) {
		return res.status(400).json({ error: 'New password and confirm password do not match!' });
	}

	Users.findOne({
		where: {
			id: userId,
		},
	})
		.then(async (user) => {
			if (!user) {
				throw errorHandler('User not found!', 404);
			}

			const isMatch = await user.validatePassword(currentPass);
			if (!isMatch) {
				throw errorHandler('Current password is incorrect!', 400);
			}

			user.password = newPass;
			return user.save();
		})
		.then(() => {
			return res.status(200).json({ success: true, message: 'Password updated successfully!' });
		})
		.catch((err) => {
			next(err);
		});
};

exports.deactivation = async (req, res, next) => {
	try {
		const { userId } = req;

		const user = await Users.findByPk(userId);

		if (!user) {
			return res.status(404).json({ success: false, message: 'User not found' });
		}

		if (user.status) {
			const activeBookings = await Bookings.findOne({
				where: {
					[Op.and]: [
						{ [Op.or]: [{ host_id: userId }, { customer_id: userId }] },
						{ [Op.or]: [{ status: 'Upcoming' }, { status: 'Pending' }] },
					],
				},
			});

			if (activeBookings) {
				return res.status(400).json({
					success: false,
					message: 'You have upcoming bookings. Please cancel them before deactivating your account',
				});
			}
		}

		user.status = !user.status;
		await user.save();

		return res
			.status(200)
			.json({ success: true, message: `Account ${user.status ? 'Activated' : 'Deactivated'} successfully` });
	} catch (error) {
		return next(error);
	}
};

exports.deleteAccount = async (req, res, next) => {
	const { userId } = req;
	const t = await sequelize.transaction();

	try {
		const user = await Users.findOne({
			where: {
				id: userId,
			},
		});

		const deleteAccountData = {
			firstname: user.firstname,
			lastname: user.lastname,
			email_address: user.email_address,
			reason: req.body.reason,
		};

		const activeBookings = await Bookings.findOne({
			where: {
				[Op.and]: [
					{ [Op.or]: [{ host_id: userId }, { customer_id: userId }] },
					{ [Op.or]: [{ status: 'Upcoming' }, { status: 'Pending' }] },
				],
			},
		});

		if (activeBookings) {
			return res.status(400).json({
				success: false,
				message: 'You have upcoming bookings. Please cancel them before deactivating your account',
			});
		}

		const files = {
			userUploads: [],
			hostUploads: [],
		};

		if (user.valid_id_image) {
			files.userUploads.push(user.valid_id_image);
		}

		if (user.image) {
			files.userUploads.push(user.image);
		}

		await sequelize.query(
			`DELETE listing_add_ons FROM listing_add_ons 
			INNER JOIN listings ON listings.id = listing_add_ons.listing_id
			WHERE listings.host_id = ?`,
			{
				transaction: t,
				type: QueryTypes.DELETE,
				replacements: [userId],
			},
		);

		await sequelize.query(
			`DELETE listing_reviews FROM listing_reviews 
			INNER JOIN listings ON listings.id = listing_reviews.listing_id
			WHERE listings.host_id = ?`,
			{
				transaction: t,
				type: QueryTypes.DELETE,
				replacements: [userId],
			},
		);

		const listingImages = await ListingImages.findAll({
			include: [
				{
					model: Listings,
					where: { host_id: userId },
				},
			],
			transaction: t,
		});

		for (const image of listingImages) {
			files.hostUploads.push(image.image);
			await image.destroy({ transaction: t });
		}

		await Listings.destroy({
			where: {
				host_id: userId,
			},
			transaction: t,
		});

		await CustomerReviews.destroy({
			where: {
				host_id: userId,
			},
			transaction: t,
		});

		await HostReviews.destroy({
			where: {
				host_id: userId,
			},
			transaction: t,
		});

		await Discounts.destroy({
			where: {
				host_id: userId,
			},
			transaction: t,
		});

		await sequelize.query(
			`DELETE host_registration_add_ons FROM host_registration_add_ons
			INNER JOIN host_registrations ON host_registrations.id = host_registration_add_ons.host_registration_id
			WHERE host_registrations.user_id = ?`,
			{
				transaction: t,
				type: QueryTypes.DELETE,
				replacements: [userId],
			},
		);

		const hostRegistrationFiles = await HostRegistrationFiles.findAll({
			include: [
				{
					model: HostsRegistrations,
					where: { user_id: userId },
				},
			],
			transaction: t,
		});

		for (const file of hostRegistrationFiles) {
			files.hostUploads.push(file.file);
			await file.destroy({ transaction: t });
		}

		await HostsRegistrations.destroy({
			where: {
				user_id: userId,
			},
			transaction: t,
		});

		await FavoriteFolderListings.destroy({
			where: {
				user_id: userId,
			},
			transaction: t,
		});

		await FavoriteFolders.destroy({
			where: {
				user_id: userId,
			},
			transaction: t,
		});

		await HostNotifications.destroy({
			where: {
				host_id: userId,
			},
			transaction: t,
		});

		await UsersFlags.destroy({
			where: {
				user_id: userId,
			},
			transaction: t,
		});

		await UsersPrivacyFlags.destroy({
			where: {
				user_id: userId,
			},
			transaction: t,
		});

		await user.destroy({
			transaction: t,
		});

		await DeletedAccounts.create(deleteAccountData, { transaction: t });

		for (const file of files.userUploads) {
			await deleteFile(file, 'user-uploads');
		}
		for (const file of files.hostUploads) {
			await deleteFile(file, 'host-uploads');
		}

		await t.commit();
		return res.status(200).json({ success: true, message: 'Account deleted successfully!' });
	} catch (err) {
		await t.rollback();
		next(err);
	}
};

exports.getLanguage = async (req, res, next) => {
	const { userId } = req;

	try {
		const user = await Users.findOne({
			where: {
				id: userId,
			},
			attributes: ['language'],
		});

		if (!user) {
			return res.status(404).json({ success: false, message: 'User not found' });
		}

		return res.status(200).json({ success: true, language: user.language ?? null });
	} catch (error) {
		return next(error);
	}
};

exports.updateLanguage = async (req, res, next) => {
	const { userId } = req;
	const { language } = req.body;

	try {
		const user = await Users.findByPk(userId);

		if (!user) {
			return res.status(404).json({ success: false, message: 'User not found' });
		}

		user.language = language;
		await user.save();

		return res.status(200).json({ success: true, message: 'Language updated successfully' });
	} catch (error) {
		return next(error);
	}
};

exports.requestPasswordReset = async (req, res, next) => {
	try {
		const { email } = req.body;

		const user = await Users.findOne({
			where: {
				email_address: email,
			},
		});

		if (!user) {
			return res.status(404).json({ success: false, message: 'User not found' });
		}

		const existingToken = await PasswordResetTokens.findOne({
			where: {
				email: email,
			},
		});

		if (existingToken && new Date() < existingToken.expiry) {
			const resendAvailable = !existingToken.resent;
			let resendAvailableAt = null;

			if (resendAvailable) {
				resendAvailableAt = getResentAvaibleAt(existingToken.expiry);
			}

			return res.status(400).json({
				success: false,
				message: 'Password reset already sent and is still valid',
				resendAvailableAt,
			});
		}

		if (existingToken && new Date() > existingToken.expiry) {
			await existingToken.destroy();
		}

		const token = jwt.sign({ userId: user.id }, process.env.RESETSECRETKEY, { expiresIn: '15m' });
		const newRecord = await PasswordResetTokens.create({
			email: email,
			token,
			expiry: new Date(new Date().getTime() + 15 * 60 * 1000),
		});

		const username = user.firstname + ' ' + user.lastname;

		const success = await sendEmail(forgotPasswordEmail(username, user.email_address, token));

		if (!success) {
			newRecord.destroy();
			return res.status(500).json({ success: false, message: 'Failed to send password reset link' });
		}

		return res.status(200).json({ success: true, message: 'Password reset link sent successfully' });
	} catch (error) {
		next(error);
	}
};

exports.resetPassword = async (req, res, next) => {
	const { token } = req.query;
	const { newPass, confirmPass } = req.body;

	try {
		if (newPass !== confirmPass) {
			return res.status(400).json({ error: 'New password and confirm password do not match!' });
		}

		if (!token) {
			return res.status(400).json({ success: false, message: 'Token is required' });
		}

		const tokenRecord = await PasswordResetTokens.findOne({
			where: {
				token,
			},
		});

		if (!tokenRecord) {
			return res.status(404).json({ success: false, message: 'Token not found or expired' });
		}

		const expired = new Date() > tokenRecord.expiry;
		if (expired) {
			await tokenRecord.destroy();
			return res.status(404).json({ success: false, message: 'Token expired' });
		}

		const decodedToken = jwt.verify(token, process.env.RESETSECRETKEY);

		if (!decodedToken) {
			await tokenRecord.destroy();
			return res.status(401).json({ success: false, message: 'Invalid token' });
		}

		const user = await Users.findOne({
			where: {
				email_address: tokenRecord.email,
			},
		});

		user.password = newPass;
		await user.save();

		await tokenRecord.destroy();

		return res.status(200).json({ success: true, message: 'Password reset successfully' });
	} catch (error) {
		next(error);
	}
};

exports.resendPasswordResetEmail = async (req, res, next) => {
	try {
		const { email } = req.body;

		const tokenRecord = await PasswordResetTokens.findOne({
			where: {
				email,
			},
		});

		if (!tokenRecord) {
			return res
				.status(404)
				.json({ success: false, message: 'No email record found or is invalid, please request a new one' });
		}

		if (new Date() > tokenRecord.expiry) {
			await tokenRecord.destroy();
			return res.status(404).json({ success: false, message: 'Token expired, Please request a new one' });
		}

		if (tokenRecord.resent) {
			return res.status(400).json({ success: false, message: 'Password reset link already resent' });
		}

		const resendAvailableAt = getResentAvaibleAt(tokenRecord.expiry);
		if (resendAvailableAt) {
			const waitUntil = new Date(resendAvailableAt).toLocaleTimeString();

			return res.status(400).json({
				success: false,
				message: `Please wait until ${waitUntil} requesting before resending.`,
				resendAvailableAt,
			});
		}

		const user = await Users.findOne({
			where: {
				email_address: email,
			},
		});

		await sendEmail(forgotPasswordEmail(`${user.firstname} ${user.lastname}`, user.email_address, tokenRecord.token));

		tokenRecord.resent = true;
		await tokenRecord.save();

		return res.status(200).json({ success: true, message: 'Password reset link sent successfully' });
	} catch (error) {
		next(error);
	}
};

function getResentAvaibleAt(expiry) {
	const requestTime = new Date(expiry.getTime() - 15 * 60 * 1000);
	const resendAvailableAt = new Date(requestTime.getTime() + 3 * 60 * 1000);

	return formatTimestamp(resendAvailableAt);
}
