import * as bcrypt from 'bcryptjs';
import { User } from '#models/User';
import { UserAuth } from '#models/UserAuth';
import sequelize from '#services/database/sequelize/sequelizeService';
import { canUserChangePassword } from '#utils/authUtils';
import { UserProvider } from '#providers/UserProvider';
import { PasswordResetTokenProvider } from '#providers/PasswordResetTokenProvider';
import { Errors } from '#interfaces/error';
import { PasswordValidationService } from './passwordValidationService';
import RedisService from '#services/storage/redisService';
import Config from '#config';

export class PasswordService {
	private static readonly BCRYPT_ROUNDS = Config.auth.bcryptRounds;

	/**
	 * Hash password using bcrypt
	 */
	static async hashPassword(password: string): Promise<string> {
		return bcrypt.hash(password, this.BCRYPT_ROUNDS);
	}

	/**
	 * Verify password against hash
	 */
	static async verifyPassword(password: string, hash: string): Promise<boolean> {
		return bcrypt.compare(password, hash);
	}

	/**
	 * Change user password
	 */
	static async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void> {
		const user = await User.findByPk(userId, {
			include: [{ model: UserAuth, as: 'user_auth' }],
		});

		if (!user) {
			throw Errors.userNotFound();
		}

		if (!user.user_auth) {
			throw Errors.notFound('Dữ liệu xác thực');
		}

		// Verify old password
		const isOldPasswordValid = await this.verifyPassword(oldPassword, user.user_auth.password_hash);
		if (!isOldPasswordValid) {
			throw Errors.invalidCredentials();
		}

		// Check if user can change password (not changed too recently)
		const canChange = canUserChangePassword(user.user_auth);
		if (!canChange) {
			throw Errors.validation([
				{
					field: 'password',
					code: 'VALIDATION_ERROR',
					message: {
						vi: 'Vui lòng đợi trước khi thay đổi mật khẩu lại',
						en: 'Password cannot be changed yet. Please wait before changing password again.',
					},
				},
			]);
		}

		// Validate new password strength using NIST-compliant validation
		const userInfo: { email: string; username?: string; firstName?: string; lastName?: string } = {
			email: user.email,
		};
		if (user.username != null) userInfo.username = user.username;
		if (user.first_name != null) userInfo.firstName = user.first_name;
		if (user.last_name != null) userInfo.lastName = user.last_name;
		const passwordValidation = await PasswordValidationService.validatePassword(newPassword, userInfo);
		if (!passwordValidation.valid) {
			const suggestionEn = passwordValidation.suggestions?.[0] || '';
			const suggestionVi = passwordValidation.suggestions?.[1] || suggestionEn;
			const errorCode = (passwordValidation.reason as any) || 'VALIDATION_ERROR';
			throw Errors.validation([
				{
					field: 'new_password',
					code: errorCode,
					message: { vi: suggestionVi || 'Mật khẩu không hợp lệ', en: suggestionEn || 'Invalid password' },
				},
			]);
		}

		// Hash new password (outside transaction — CPU-bound, no DB lock held)
		const newPasswordHash = await this.hashPassword(newPassword);

		// Single atomic update inside a transaction
		const transaction = await sequelize.transaction();
		try {
			await UserAuth.update(
				{
					password_hash: newPasswordHash,
					password_changed_at: new Date(),
				},
				{ where: { user_id: userId }, transaction },
			);
			await transaction.commit();
		} catch (err) {
			await transaction.rollback();
			throw err;
		}
	}

	/**
	 * Reset password to default value (admin function)
	 * Uses DEFAULT_PASSWORD from environment configuration
	 */
	static async resetPasswordToDefault(userId: string, _adminUserId?: string): Promise<string> {
		const defaultPassword = process.env.DEFAULT_PASSWORD || 'VietPro@2026';
		await this.resetPassword(userId, defaultPassword);
		return defaultPassword;
	}

	/**
	 * Reset password (admin function)
	 */
	static async resetPassword(userId: string, newPassword: string, _adminUserId?: string): Promise<void> {
		const user = await User.findByPk(userId, {
			include: [{ model: UserAuth, as: 'user_auth' }],
		});

		if (!user) {
			throw Errors.userNotFound();
		}

		if (!user.user_auth) {
			throw Errors.notFound('Dữ liệu xác thực');
		}

		// Validate new password strength using NIST-compliant validation
		const userInfo: { email: string; username?: string; firstName?: string; lastName?: string } = {
			email: user.email,
		};
		if (user.username != null) userInfo.username = user.username;
		if (user.first_name != null) userInfo.firstName = user.first_name;
		if (user.last_name != null) userInfo.lastName = user.last_name;
		const passwordValidation = await PasswordValidationService.validatePassword(newPassword, userInfo);
		if (!passwordValidation.valid) {
			const suggestionEn = passwordValidation.suggestions?.[0] || '';
			const suggestionVi = passwordValidation.suggestions?.[1] || suggestionEn;
			const errorCode = (passwordValidation.reason as any) || 'VALIDATION_ERROR';
			throw Errors.validation([
				{
					field: 'new_password',
					code: errorCode,
					message: { vi: suggestionVi || 'Mật khẩu không hợp lệ', en: suggestionEn || 'Invalid password' },
				},
			]);
		}

		// Hash new password (outside transaction — CPU-bound, no DB lock held)
		const newPasswordHash = await this.hashPassword(newPassword);

		const transaction = await sequelize.transaction();
		try {
			await UserAuth.update(
				{
					password_hash: newPasswordHash,
					password_changed_at: new Date(),
				},
				{ where: { user_id: userId }, transaction },
			);
			await transaction.commit();
		} catch (err) {
			await transaction.rollback();
			throw err;
		}
	}

	/**
	 * Validate a password-reset OTP for a given email.
	 * Performs cheap checks (used/expired) before the expensive bcrypt comparison.
	 * Returns the matched user and resetCode on success; throws GenericError on failure.
	 */
	static async validatePasswordResetOtp(
		email: string,
		code: string,
	): Promise<{
		user: Awaited<ReturnType<UserProvider['getAll']>>['rows'][number];
		resetCode: Awaited<ReturnType<PasswordResetTokenProvider['getAll']>>['rows'][number];
	}> {
		const userProvider = new UserProvider();
		const codeProvider = new PasswordResetTokenProvider();
		const redis = RedisService.getInstance();

		// SEC-HIGH-02: Check brute force protection using Redis
		const otpAttemptKey = `otp_attempts:${email.toLowerCase()}`;
		const maxAttempts = Config.notifications.rateLimiting.maxLoginAttempts;
		const attemptWindow = Config.notifications.rateLimiting.lockoutDurationMinutes * 60; // Convert to seconds

		const currentAttempts = await redis.get(otpAttemptKey);
		if (currentAttempts && parseInt(currentAttempts as string, 10) >= maxAttempts) {
			throw Errors.validation([
				{
					field: 'code',
					code: 'VALIDATION_ERROR',
					message: {
						vi: 'Quá nhiều lần thử sai, vui lòng thử lại sau 1 giờ',
						en: 'Too many failed attempts, please try again in 1 hour',
					},
				},
			]);
		}

		const users = await userProvider.getAll({ where: { email } });
		if (!users.rows.length) {
			// SEC-HIGH-03: Generic error to prevent email enumeration - same message as invalid code
			throw Errors.validation([
				{
					field: 'code',
					code: 'INVALID_CODE',
					message: { vi: 'Mã xác thực không hợp lệ hoặc đã hết hạn', en: 'Invalid or expired verification code' },
				},
			]);
		}
		const user = users.rows[0]!;

		// Check if user account is active - prevent suspended users from resetting password
		if (user.status !== 'active') {
			// SEC-HIGH-03: Return same generic error to prevent account status enumeration
			throw Errors.validation([
				{
					field: 'code',
					code: 'INVALID_CODE',
					message: { vi: 'Mã xác thực không hợp lệ hoặc đã hết hạn', en: 'Invalid or expired verification code' },
				},
			]);
		}

		const codes = await codeProvider.getAll({
			where: { user_id: user.id },
			sortField: 'created_at',
			sortOrder: 'DESC',
			pageSize: 1,
			page: 1,
		});
		if (!codes.rows.length) {
			// SEC-HIGH-03: Same generic error message to prevent email enumeration
			throw Errors.validation([
				{
					field: 'code',
					code: 'INVALID_CODE',
					message: { vi: 'Mã xác thực không hợp lệ hoặc đã hết hạn', en: 'Invalid or expired verification code' },
				},
			]);
		}
		const resetCode = codes.rows[0]!;

		// Cheap checks first — before the expensive bcrypt.compare
		if (resetCode.used_at != null) {
			// SEC-HIGH-03: Same generic error to prevent code status enumeration
			throw Errors.validation([
				{
					field: 'code',
					code: 'INVALID_CODE',
					message: { vi: 'Mã xác thực không hợp lệ hoặc đã hết hạn', en: 'Invalid or expired verification code' },
				},
			]);
		}
		if (new Date() > new Date(resetCode.expires_at)) {
			// SEC-HIGH-03: Same generic error to prevent code status enumeration
			throw Errors.validation([
				{
					field: 'code',
					code: 'INVALID_CODE',
					message: { vi: 'Mã xác thực không hợp lệ hoặc đã hết hạn', en: 'Invalid or expired verification code' },
				},
			]);
		}

		const isValid = await bcrypt.compare(String(code), resetCode.token_hash);
		if (!isValid) {
			// Increment failed attempt counter in Redis
			await redis.increment(otpAttemptKey);
			await redis.expire(otpAttemptKey, attemptWindow);
			throw Errors.validation([
				{
					field: 'code',
					code: 'INVALID_CODE',
					message: { vi: 'Mã xác thực không hợp lệ', en: 'Invalid verification code' },
				},
			]);
		}

		// Clear attempt counter on successful validation
		await redis.delete(otpAttemptKey).catch(() => {});

		return { user, resetCode };
	}
}
