import bcrypt from 'bcryptjs';
import { Op, QueryTypes } from 'sequelize';
import { UserAuth } from '#models/UserAuth';
import { UserPasswordHistory } from '#models/UserPasswordHistory';
import { UserSession } from '#models/UserSession';
import sequelize from '#services/database/sequelize/sequelizeService';
import dayjs from 'dayjs';
import Config from '#config';

/**
 * Authentication utility functions
 * These functions provide the logic that would normally be instance methods
 * but are kept separate to avoid conflicts with auto-generated models
 */

/**
 * Maximum number of login attempts before account lockout
 */
const MAX_LOGIN_ATTEMPTS = Config.notifications.rateLimiting.maxLoginAttempts;

/**
 * Account lockout duration in minutes
 */
const LOCKOUT_DURATION_MINUTES = Config.notifications.rateLimiting.lockoutDurationMinutes;

/**
 * Check if user account is locked due to too many failed login attempts
 */
export const isUserLocked = (userAuth: UserAuth): boolean => {
	if (userAuth.locked_until) {
		const now = dayjs();
		const lockedUntil = dayjs(userAuth.locked_until);
		return now.isBefore(lockedUntil);
	}

	return false;
};

/**
 * Atomically increment login attempts using a single SQL UPDATE.
 *
 * This is race-condition-free (S5/TOCTOU fix): the read-modify-write happens
 * inside the DB engine in one round-trip, so concurrent failed logins can
 * never skip the lockout threshold.
 *
 * @param userId - The `user_id` of the row in `user_auth` to update.
 */
export const atomicIncrementLoginAttempts = async (userId: string): Promise<void> => {
	await sequelize.query(
		`UPDATE user_auth
		 SET login_attempts = login_attempts + 1,
		     locked_until   = CASE
		                         WHEN login_attempts + 1 >= :maxAttempts
		                         THEN NOW() + INTERVAL '1 minute' * :lockoutDuration
		                         ELSE locked_until
		                       END
		 WHERE user_id = :userId`,
		{
			replacements: { userId, maxAttempts: MAX_LOGIN_ATTEMPTS, lockoutDuration: LOCKOUT_DURATION_MINUTES },
			type: QueryTypes.UPDATE,
		},
	);
};

/**
 * Reset login attempts for a user (on successful login)
 */
export const resetUserLoginAttempts = async (userAuthInstance: UserAuth): Promise<void> => {
	await userAuthInstance.update({
		login_attempts: 0,
		locked_until: null,
	});
};

/**
 * Unlock user account (reset lockout)
 */
export const unlockUserAccount = async (userAuthInstance: UserAuth): Promise<void> => {
	await userAuthInstance.update({
		locked_until: null,
		login_attempts: 0,
	});
};

/**
 * Check if user account lockout has expired and unlock if needed
 */
export const tryUnlockIfExpired = async (userAuthInstance: UserAuth): Promise<boolean> => {
	if (userAuthInstance.locked_until) {
		const now = dayjs();
		const lockedUntil = dayjs(userAuthInstance.locked_until);

		if (now.isAfter(lockedUntil)) {
			await unlockUserAccount(userAuthInstance);
			return true; // Account was unlocked
		}
	}
	return false; // Account was not locked or still locked
};

/**
 * Update user's last login timestamp
 */
export const updateUserLastLogin = async (userAuthInstance: UserAuth): Promise<void> => {
	await userAuthInstance.update({
		last_login_at: new Date(),
	});
};

/**
 * Check if user session is expired
 */
export const isSessionExpired = (session: UserSession): boolean => {
	const now = dayjs();
	const expiresAt = dayjs(session.expires_at);
	return now.isAfter(expiresAt);
};

/**
 * Check if user session refresh token is expired
 */
export const isSessionRefreshExpired = (session: UserSession): boolean => {
	const now = dayjs();
	const refreshExpiresAt = dayjs(session.refresh_expires_at);
	return now.isAfter(refreshExpiresAt);
};

/**
 * Deactivate a user session
 */
export const deactivateUserSession = async (sessionInstance: UserSession): Promise<void> => {
	await sessionInstance.update({ status: 'logged_out' });
};

/**
 * Update session activity timestamp
 */
export const updateSessionActivity = async (sessionInstance: UserSession): Promise<void> => {
	await sessionInstance.update({ last_activity_at: new Date() });
};

/**
 * Check if user can change password (not recently changed)
 */
export const canUserChangePassword = (userAuth: UserAuth, minDaysBetweenChanges = 1): boolean => {
	if (!userAuth.password_changed_at) return true;

	const lastChange = dayjs(userAuth.password_changed_at);
	const now = dayjs();
	const daysSinceChange = now.diff(lastChange, 'day');

	return daysSinceChange >= minDaysBetweenChanges;
};

/**
 * Update user's password changed timestamp
 */
export const updateUserPasswordChanged = async (userAuthInstance: UserAuth): Promise<void> => {
	await userAuthInstance.update({
		password_changed_at: new Date(),
	});
};

/**
 * SEC-05: Number of previous passwords to check for reuse prevention
 */
const PASSWORD_HISTORY_LIMIT = Config.auth.passwordHistoryLimit;

/**
 * SEC-05: Check if password was used recently (prevent reuse)
 * @param userId - User ID to check password history for
 * @param plainNewPassword - Plain text password to check against history
 * @returns true if password was used recently, false otherwise
 */
export const isPasswordRecentlyUsed = async (userId: string, plainNewPassword: string): Promise<boolean> => {
	try {
		// Get last N password hashes for the user
		const historyEntries = await UserPasswordHistory.findAll({
			where: { user_id: userId },
			order: [['changed_at', 'DESC']],
			limit: PASSWORD_HISTORY_LIMIT,
			attributes: ['password_hash'],
		});

		// Check if new password matches any recent password
		for (const entry of historyEntries) {
			if (await bcrypt.compare(plainNewPassword, entry.password_hash)) {
				return true;
			}
		}
		return false;
	} catch (error) {
		// If table doesn't exist yet (migration not run), allow password change
		console.error('Password history check failed:', error);
		return false;
	}
};

/**
 * SEC-05: Save password hash to history
 * @param userId - User ID
 * @param passwordHash - Hashed password to save
 * @param changedBy - ID of user who changed the password (optional)
 * @param reason - Reason for password change (optional)
 */
export const savePasswordToHistory = async (
	userId: string,
	passwordHash: string,
	changedBy?: string,
	reason?: string,
): Promise<void> => {
	try {
		const createData: any = {
			user_id: userId,
			password_hash: passwordHash,
		};
		if (changedBy !== undefined) createData.changed_by = changedBy;
		if (reason !== undefined) createData.reason = reason;

		await UserPasswordHistory.create(createData);
	} catch (error) {
		// Log error but don't fail the password change if history save fails
		console.error('Failed to save password to history:', error);
	}
};

/**
 * SEC-05: Cleanup old password history entries (keep only last N)
 * @param userId - User ID to cleanup history for
 */
export const cleanupPasswordHistory = async (userId: string): Promise<void> => {
	try {
		const cutoffDate = dayjs().subtract(1, 'year').toDate(); // Keep history for 1 year max

		// PERF-04: Use DELETE query directly instead of loading all entries
		// Delete entries older than 1 year
		await UserPasswordHistory.destroy({
			where: {
				user_id: userId,
				changed_at: { [Op.lt]: cutoffDate },
			},
		});

		// Delete entries beyond the limit (keep only PASSWORD_HISTORY_LIMIT most recent)
		// Use subquery to find IDs to delete
		await sequelize.query(
			`DELETE FROM user_password_history
			 WHERE id IN (
				 SELECT id FROM user_password_history
				 WHERE user_id = :userId
				 ORDER BY changed_at DESC
				 OFFSET :limit
			 )`,
			{
				replacements: { userId, limit: PASSWORD_HISTORY_LIMIT },
				type: QueryTypes.DELETE,
			},
		);
	} catch (error) {
		console.error('Failed to cleanup password history:', error);
	}
};

/**
 * Get role display text
 */
export const getRoleText = (role: string): { vi: string; en: string } => {
	const roleMap: Record<string, { vi: string; en: string }> = {
		user: { vi: 'Người dùng', en: 'User' },
		admin: { vi: 'Quản trị viên', en: 'Administrator' },
		system_admin: { vi: 'Quản trị hệ thống', en: 'System Administrator' },
	};

	return roleMap[role] || { vi: 'Không xác định', en: 'Unknown' };
};

/**
 * Extract roles and permissions from a user_roles association array.
 * Centralises the Set-deduplication logic used in authService and zaloLogin.
 */
export function extractPermissionsFromRoles(userRoles: any[]): { roles: string[]; permissions: string[] } {
	const rolesSet = new Set<string>();
	const permissionsSet = new Set<string>();

	for (const ur of userRoles) {
		if (ur.role?.name) rolesSet.add(ur.role.name);
		for (const rp of ur.role?.role_permissions ?? []) {
			// Only use the machine-readable key for permissions (not the human-readable name)
			if (rp.permission?.key) permissionsSet.add(rp.permission.key);
		}
	}

	return { roles: Array.from(rolesSet), permissions: Array.from(permissionsSet) };
}
