import { randomBytes, randomUUID } from 'crypto';
import { QueryTypes, Op as _Op } from 'sequelize';
import { User } from '#models/User';
import { UserAuth } from '#models/UserAuth';
import { Role } from '#models/Role';
import { UserRole as UserRoleModel } from '#models/UserRole';
import { RolePermission } from '#models/RolePermission';
import { Permission } from '#models/Permission';
import { Resident } from '#models/Resident';
import { GenericError } from '#interfaces/error/generic';
import {
	isUserLocked,
	atomicIncrementLoginAttempts,
	tryUnlockIfExpired,
	extractPermissionsFromRoles,
} from '#utils/authUtils';
import { TokenService } from './tokenService';
import { PasswordService } from './passwordService';
import { PasswordValidationService } from './passwordValidationService';
import { SessionService, SessionData } from './sessionService';
import sequelize from '#services/database/sequelize/sequelizeService';
import { UserStatus, UserRoleEnum } from '#constants/index';
import { invalidatePermissionsCache } from '#middlewares/auth';
import Logger from '#utils/logger';
import Config from '#config';
import { AuditLogService } from '#services/audit/auditLogService';

// Re-export for backward compatibility
export { UserStatus };
export { UserRoleEnum as UserRole };

export interface LoginCredentials {
	email?: string;
	phone?: string;
	password: string;
	device_info?: Record<string, unknown>;
	ip_address?: string;
	user_agent?: string;
	platform?: 'web' | 'mobile' | 'zalo_miniapp' | 'internal';
	device_id?: string;
	device_name?: string;
}

export interface RegisterData {
	email: string;
	password: string;
	username?: string;
	first_name?: string;
	last_name?: string;
	skipPasswordValidation?: boolean; // For OAuth-only accounts where password is never used
	phone?: string;
	role?: UserRoleEnum;
}

interface PermissionWithKey extends Permission {
	key: string;
}

interface RolePermissionWithPermission extends RolePermission {
	permission: PermissionWithKey;
}

interface RoleWithPermissions extends Role {
	role_permissions: RolePermissionWithPermission[];
}

interface UserRoleWithRole extends UserRoleModel {
	role: RoleWithPermissions;
}

interface UserWithRolesAndPermissions extends User {
	user_auth: UserAuth;
	user_roles: UserRoleWithRole[];
}

export interface LoginResult {
	user: User;
	accessToken: string;
	refreshToken: string;
	permissions: string[];
	roles: string[];
	residentId?: string | null;
	session: {
		id: string;
		expires_at: string;
		refresh_expires_at: string;
	};
	expires_in: number;
	refresh_expires_in: number;
}

export class AuthService {
	// PERF-02: In-memory cache for user permissions (TTL: 5 minutes)
	private static permissionCache = new Map<
		string,
		{ data: { roles: string[]; permissions: string[] }; expiry: number }
	>();
	private static readonly PERMISSION_CACHE_TTL = 5 * 60 * 1000; // 5 minutes

	/**
	 * SEC-04: Validate email format using RFC 5322 compliant regex
	 * @param email - Email address to validate
	 * @returns true if email format is valid, false otherwise
	 */
	private static isValidEmailFormat(email: string): boolean {
		const emailRegex =
			/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
		return emailRegex.test(email);
	}

	/**
	 * Get cached permissions for a user
	 */
	private static getCachedPermissions(userId: string): { roles: string[]; permissions: string[] } | null {
		const cached = this.permissionCache.get(userId);
		if (!cached) return null;

		if (Date.now() > cached.expiry) {
			this.permissionCache.delete(userId);
			return null;
		}

		return cached.data;
	}

	/**
	 * Cache permissions for a user
	 */
	private static setCachedPermissions(userId: string, roles: string[], permissions: string[]): void {
		this.permissionCache.set(userId, {
			data: { roles, permissions },
			expiry: Date.now() + this.PERMISSION_CACHE_TTL,
		});

		// Passive cleanup: evict expired entries when cache grows large
		if (this.permissionCache.size > 1000) {
			const now = Date.now();
			for (const [key, value] of this.permissionCache.entries()) {
				if (now > value.expiry) {
					this.permissionCache.delete(key);
				}
			}
		}
	}

	/**
	 * Invalidate cached permissions for a user (call when roles/permissions change)
	 */
	static invalidatePermissionCache(userId: string): void {
		this.permissionCache.delete(userId);
	}

	/**
	 * Helper method to fetch user with roles and permissions
	 * PERF-03: Split into separate queries to avoid complex nested joins
	 * Uses caching to reduce database queries
	 */
	private static async getUserWithRolesAndPermissions(
		identifier: string,
		requireAuth = true,
	): Promise<UserWithRolesAndPermissions | null> {
		// Query 1: Get user with auth (simple)
		// Try email first, then phone if email lookup fails
		const user = (await User.findOne({
			where: { email: identifier },
			include: [{ model: UserAuth, as: 'user_auth', required: requireAuth }],
		})) as UserWithRolesAndPermissions | null;

		if (!user) {
			// Try phone lookup if email not found
			const userByPhone = (await User.findOne({
				where: { phone: identifier },
				include: [{ model: UserAuth, as: 'user_auth', required: requireAuth }],
			})) as UserWithRolesAndPermissions | null;
			if (!userByPhone) return null;
			return userByPhone;
		}

		// Query 2: Get user roles separately
		const userRoles = await UserRoleModel.findAll({
			where: { user_id: user.id },
			include: [
				{
					model: Role,
					as: 'role',
					include: [
						{
							model: RolePermission,
							as: 'role_permissions',
							include: [{ model: Permission, as: 'permission' }],
						},
					],
				},
			],
		});

		// Attach roles to user (manual hydration)
		(user as any).user_user_roles = userRoles;

		// Cache permissions after fetching
		const { roles, permissions } = extractPermissionsFromRoles(userRoles ?? []);
		this.setCachedPermissions(user.id, roles, permissions);

		return user;
	}

	/**
	 * Helper method to create session and generate tokens after successful authentication
	 */
	private static async createAuthSession(
		user: UserWithRolesAndPermissions,
		platform: 'web' | 'mobile' | 'zalo_miniapp' | 'internal',
		deviceInfo?: Record<string, unknown>,
		ipAddress?: string,
		userAgent?: string,
	): Promise<LoginResult> {
		// Extract roles and permissions using utility function
		const { roles, permissions } = extractPermissionsFromRoles(user.user_user_roles ?? []);

		// Determine resident_id associated with this user (if any)
		let residentId: string | null = null;
		try {
			const resident = await Resident.findOne({ where: { user_id: user.id } });
			if (resident) residentId = (resident as any).id;
		} catch {
			// Ignore resident lookup errors - not critical for auth
		}

		// Generate session ID first for standard JWT claims (sid)
		const sessionId = randomUUID();

		// Generate tokens with sessionId
		const tokenInput = {
			id: user.id,
			email: user.email,
			...(user.username != null ? { username: user.username } : {}),
			roles,
			sessionId,
			residentId,
		};
		const accessToken = TokenService.generateAccessToken(tokenInput);
		const refreshToken = TokenService.generateRefreshToken(tokenInput);

		// Create session with sessionId
		const sessionData: SessionData = {
			userId: user.id,
			sessionId,
			accessToken,
			refreshToken,
			deviceInfo: deviceInfo || {},
			platform,
		};

		// Only include optional properties when they have defined values
		if (ipAddress !== undefined) sessionData.ipAddress = ipAddress;
		if (userAgent !== undefined) sessionData.userAgent = userAgent;

		const session = await SessionService.createSession(sessionData);

		return {
			user,
			accessToken,
			refreshToken,
			permissions,
			roles,
			residentId,
			session: {
				id: session.id,
				expires_at: session.expires_at.toISOString(),
				refresh_expires_at: session.refresh_expires_at.toISOString(),
			},
			expires_in: TokenService.parseTimeToSeconds(Config.auth.jwtExpiresIn),
			refresh_expires_in: TokenService.parseTimeToSeconds(Config.auth.jwtRefreshExpiresIn),
		};
	}

	/**
	 * Register new user
	 */
	static async register(data: RegisterData): Promise<User> {
		return sequelize.transaction(async (transaction) => {
			const { email, password, username, first_name, last_name, phone, skipPasswordValidation } = data;

			// SEC-04: Validate email format before database operations
			if (!AuthService.isValidEmailFormat(email)) {
				throw new GenericError('INVALID_EMAIL_FORMAT');
			}

			// Check if user already exists
			const existingUser = await User.findOne({
				where: { email },
				transaction,
			});

			if (existingUser) {
				throw new GenericError('USER_EXISTS');
			}

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

			// Hash password
			const passwordHash = await PasswordService.hashPassword(password);

			// Create user
			const user = await User.create(
				{
					id: crypto.randomUUID(),
					email,
					username: username ?? null,
					first_name: first_name ?? null,
					last_name: last_name ?? null,
					phone: phone ?? null,
					status: UserStatus.ACTIVE,
					created_at: new Date(),
					updated_at: new Date(),
				},
				{ transaction },
			);

			// Create user auth record
			await UserAuth.create(
				{
					id: crypto.randomUUID(),
					user_id: user.id,
					password_hash: passwordHash,
					password_changed_at: new Date(),
					login_attempts: 0,
					created_at: new Date(),
					updated_at: new Date(),
				},
				{ transaction },
			);

			// Assign default role
			const defaultRole = await Role.findOne({
				where: { name: UserRoleEnum.USER },
				transaction,
			});

			if (defaultRole) {
				await UserRoleModel.create(
					{
						id: crypto.randomUUID(),
						user_id: user.id,
						role_id: defaultRole.id,
						assigned_at: new Date(),
						updated_at: new Date(),
					},
					{ transaction },
				);
			} else {
				Logger.warn(
					`[AuthService] Default role '${UserRoleEnum.USER}' not found in database. User ${user.id} will have no role assigned.`,
				);
			}

			// Invalidate permissions cache (fire-and-forget)
			await invalidatePermissionsCache(user.id).catch((error) => {
				Logger.error('Failed to invalidate permissions cache', error);
			});

			// Log registration event
			await AuditLogService.enqueueSystemAudit({
				requestId: randomUUID(),
				traceId: randomUUID(),
				actorId: user.id,
				actorName: `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email,
				actorEmail: user.email,
				actorRole: data.role || UserRoleEnum.USER,
				action: 'USER_REGISTERED',
				module: 'AUTH',
				entityId: user.id,
				entityType: 'User',
				description: `User registered with email ${user.email}`,
				severity: 'MEDIUM',
				payload: {
					email: user.email,
					username: user.username,
					phone: user.phone,
					role: data.role || UserRoleEnum.USER,
				},
			});

			return user;
		});
	}

	/**
	 * Authenticate user login
	 */
	static async login(credentials: LoginCredentials): Promise<LoginResult> {
		const {
			email,
			phone,
			password,
			device_info,
			ip_address,
			user_agent,
			platform,
			device_id: _device_id,
			device_name: _device_name,
		} = credentials;

		const requestId = randomUUID();
		const traceId = randomUUID();

		// Determine lookup field: phone for mobile, email for web
		const lookupField = phone ? 'phone' : 'email';
		const lookupValue = phone || email;

		if (!lookupValue) {
			// Fire-and-forget audit log (non-blocking)
			AuditLogService.enqueueSystemAudit({
				requestId,
				traceId,
				actorId: 'anonymous',
				actorName: 'Anonymous',
				actorEmail: 'unknown',
				actorRole: 'ANONYMOUS',
				action: 'LOGIN_FAILED',
				module: 'AUTH',
				description: 'Login attempt with no credentials provided',
				severity: 'LOW',
				ipAddress: ip_address || undefined,
				userAgent: user_agent || undefined,
				payload: { platform },
			} as any).catch(() => {});
			throw new GenericError('INVALID_CREDENTIALS');
		}

		// Use optimized getUserWithRolesAndPermissions to avoid complex nested joins
		const user = await this.getUserWithRolesAndPermissions(lookupValue, true);

		if (!user) {
			// Fire-and-forget audit log (non-blocking)
			AuditLogService.enqueueSystemAudit({
				requestId,
				traceId,
				actorId: 'anonymous',
				actorName: 'Anonymous',
				actorEmail: lookupValue,
				actorRole: 'ANONYMOUS',
				action: 'LOGIN_FAILED',
				module: 'AUTH',
				description: `Login attempt with invalid credentials for ${lookupField}: ${lookupValue}`,
				severity: 'MEDIUM',
				ipAddress: ip_address || undefined,
				userAgent: user_agent || undefined,
				payload: { lookupField, lookupValue, platform },
			} as any).catch(() => {});
			throw new GenericError('INVALID_CREDENTIALS');
		}

		// Check user status
		if (user.status !== UserStatus.ACTIVE) {
			// Fire-and-forget audit log (non-blocking)
			AuditLogService.enqueueSystemAudit({
				requestId,
				traceId,
				actorId: user.id,
				actorName: `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email,
				actorEmail: user.email,
				actorRole: 'UNKNOWN',
				action: 'LOGIN_FAILED',
				module: 'AUTH',
				entityId: user.id,
				entityType: 'User',
				description: `Login attempt for inactive account ${user.email}`,
				severity: 'HIGH',
				ipAddress: ip_address || undefined,
				userAgent: user_agent || undefined,
				payload: { status: user.status, platform },
			} as any).catch(() => {});
			throw new GenericError('ACCOUNT_INACTIVE');
		}

		// Check if user is locked
		const isLocked = await isUserLocked(user.user_auth);
		if (isLocked) {
			const canUnlock = await tryUnlockIfExpired(user.user_auth);
			if (!canUnlock) {
				// Fire-and-forget audit log (non-blocking)
				AuditLogService.enqueueSystemAudit({
					requestId,
					traceId,
					actorId: user.id,
					actorName: `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email,
					actorEmail: user.email,
					actorRole: 'UNKNOWN',
					action: 'LOGIN_FAILED',
					module: 'AUTH',
					entityId: user.id,
					entityType: 'User',
					description: `Login attempt for locked account ${user.email}`,
					severity: 'HIGH',
					ipAddress: ip_address || undefined,
					userAgent: user_agent || undefined,
					payload: { platform },
				} as any).catch(() => {});
				throw new GenericError('ACCOUNT_LOCKED');
			}
		}

		// Verify password
		const isPasswordValid = await PasswordService.verifyPassword(password, user.user_auth.password_hash);
		if (!isPasswordValid) {
			await atomicIncrementLoginAttempts(user.user_auth.user_id);
			// Fire-and-forget audit log (non-blocking)
			AuditLogService.enqueueSystemAudit({
				requestId,
				traceId,
				actorId: user.id,
				actorName: `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email,
				actorEmail: user.email,
				actorRole: 'UNKNOWN',
				action: 'LOGIN_FAILED',
				module: 'AUTH',
				entityId: user.id,
				entityType: 'User',
				description: `Failed password attempt for ${user.email}`,
				severity: 'MEDIUM',
				ipAddress: ip_address || undefined,
				userAgent: user_agent || undefined,
				payload: { lookupField, lookupValue, platform },
			} as any).catch(() => {});
			throw new GenericError('INVALID_CREDENTIALS');
		}

		// Prevent oauth-only users from logging in via password
		if (user.user_auth.is_oauth_only) {
			// Fire-and-forget audit log (non-blocking)
			AuditLogService.enqueueSystemAudit({
				requestId,
				traceId,
				actorId: user.id,
				actorName: `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email,
				actorEmail: user.email,
				actorRole: 'UNKNOWN',
				action: 'LOGIN_FAILED',
				module: 'AUTH',
				entityId: user.id,
				entityType: 'User',
				description: `Password login attempt for OAuth-only account ${user.email}`,
				severity: 'HIGH',
				ipAddress: ip_address || undefined,
				userAgent: user_agent || undefined,
				payload: { platform },
			} as any).catch(() => {});
			throw new GenericError('OAUTH_ONLY_ACCOUNT');
		}

		// Update last login and reset attempts in single query
		await user.user_auth.update({
			last_login_at: new Date(),
			login_attempts: 0,
			locked_until: null,
		});

		// Log successful login (fire-and-forget)
		const { roles: auditRoles } = extractPermissionsFromRoles(user.user_user_roles ?? []);
		AuditLogService.enqueueSystemAudit({
			requestId,
			traceId,
			actorId: user.id,
			actorName: `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email,
			actorEmail: user.email,
			actorRole: (Array.isArray(auditRoles) ? auditRoles[0] : auditRoles) || 'UNKNOWN',
			action: 'LOGIN_SUCCESS',
			module: 'AUTH',
			entityId: user.id,
			entityType: 'User',
			description: `Successful login for ${user.email}`,
			severity: 'LOW',
			ipAddress: ip_address || undefined,
			userAgent: user_agent || undefined,
			payload: { platform, device_info },
		} as any).catch(() => {});

		// Session creation is now sync (indexes optimized)
		// Generate tokens and create session
		const result = await this.createAuthSession(user, platform || 'web', device_info, ip_address, user_agent);

		return result;
	}

	/**
	 * Authenticate or register user via Zalo Mini App
	 */
	static async zaloLogin(
		zaloUserInfo: {
			id: string;
			name: string;
			picture?: string;
		},
		deviceInfo?: Record<string, unknown>,
		ipAddress?: string,
		userAgent?: string,
	): Promise<LoginResult> {
		const requestId = randomUUID();
		const traceId = randomUUID();
		const zaloId = `zalo_${zaloUserInfo.id}`;

		// Find existing user by Zalo ID
		let user = await this.getUserWithRolesAndPermissions(zaloId, false);

		// Create user if not exists, then re-fetch with full associations
		if (!user) {
			// Zalo users get a placeholder password - they can only login via Zalo OAuth
			// The password format indicates this is an oauth-only account
			const newUser = await this.register({
				email: zaloId,
				password: 'ZALO_OAUTH_ONLY_' + randomBytes(32).toString('hex'),
				username: zaloUserInfo.name,
				role: UserRoleEnum.USER,
				skipPasswordValidation: true, // Skip validation for OAuth-only accounts
			});
			// Set is_oauth_only flag for Zalo users
			await UserAuth.update({ is_oauth_only: true }, { where: { user_id: newUser.id } });
			user = await this.getUserWithRolesAndPermissions(zaloId, false);
			if (!user) {
				throw new GenericError('INTERNAL_SERVER_ERROR');
			}
		}

		// Check user status
		if (user.status !== UserStatus.ACTIVE) {
			await AuditLogService.enqueueSystemAudit({
				requestId,
				traceId,
				actorId: user.id,
				actorName: user.username || user.email,
				actorEmail: user.email,
				actorRole: 'UNKNOWN',
				action: 'LOGIN_FAILED',
				module: 'AUTH',
				entityId: user.id,
				entityType: 'User',
				description: `Zalo login attempt for inactive account ${user.email}`,
				severity: 'HIGH',
				ipAddress: ipAddress || undefined,
				userAgent: userAgent || undefined,
				payload: { zaloId: zaloUserInfo.id, platform: 'zalo_miniapp' },
			} as any);
			throw new GenericError('ACCOUNT_INACTIVE');
		}

		// Log successful Zalo login
		const { roles } = extractPermissionsFromRoles(user.user_user_roles ?? []);
		await AuditLogService.enqueueSystemAudit({
			requestId,
			traceId,
			actorId: user.id,
			actorName: user.username || user.email,
			actorEmail: user.email,
			actorRole: (Array.isArray(roles) ? roles[0] : roles) || 'UNKNOWN',
			action: 'LOGIN_SUCCESS',
			module: 'AUTH',
			entityId: user.id,
			entityType: 'User',
			description: `Successful Zalo login for ${user.email}`,
			severity: 'LOW',
			ipAddress: ipAddress || undefined,
			userAgent: userAgent || undefined,
			payload: { zaloId: zaloUserInfo.id, platform: 'zalo_miniapp', device_info: deviceInfo },
		} as any);

		// Create session and generate tokens using helper method
		return this.createAuthSession(user, 'zalo_miniapp', deviceInfo, ipAddress, userAgent);
	}

	/**
	 * Get user permissions
	 * Optimized to use raw SQL with JOINs instead of nested associations
	 */
	static async getUserPermissions(userId: string): Promise<string[]> {
		const rows = await sequelize.query<{
			perm_name: string | null;
			perm_key: string | null;
		}>(
			`SELECT p.name AS perm_name, p.key AS perm_key
			 FROM user_roles ur
			 JOIN roles r ON r.id = ur.role_id
			 LEFT JOIN role_permissions rp ON rp.role_id = r.id
			 LEFT JOIN permissions p ON p.id = rp.permission_id
			 WHERE ur.user_id = :userId`,
			{ replacements: { userId }, type: QueryTypes.SELECT },
		);

		const permissionsSet = new Set<string>();
		for (const row of rows) {
			if (row.perm_key) permissionsSet.add(row.perm_key);
		}

		return Array.from(permissionsSet);
	}
}
