import { createHash as _createHash, randomBytes as _randomBytes, randomUUID as _randomUUID } from 'crypto';
import { User } from '#models/User';
import { UserRole } from '#models/UserRole';
import { Role } from '#models/Role';
import { UserSession } from '#models/UserSession';
import { RefreshTokenAuditLog } from '#models/RefreshTokenAuditLog';
import { TokenService } from './tokenService';
import { isSessionExpired, deactivateUserSession, updateSessionActivity } from '#utils/authUtils';
import { GenericError } from '#interfaces/error/generic';
import NotificationService from '#services/notification/notificationService';
import RedisService from '#services/storage/redisService';
import Logger from '#utils/logger';
import { Op } from 'sequelize';
import Config from '#config';

export interface SessionData {
	userId: string;
	sessionId: string; // Session ID for standard JWT claims (sid)
	accessToken: string;
	refreshToken: string;
	deviceInfo?: Record<string, unknown>;
	ipAddress?: string;
	userAgent?: string;
	platform?: 'web' | 'mobile' | 'zalo_miniapp' | 'internal';
	deviceId?: string;
	deviceName?: string;
}

// TypeScript interfaces to avoid 'as any' casts
interface _SessionWithUser extends UserSession {
	user: User;
}

interface SessionWithUserAndRoles extends UserSession {
	user: User & { user_roles?: Array<{ role?: { name?: string } }> };
}

export class SessionService {
	private static redis = RedisService.getInstance();
	private static readonly REUSE_WINDOW_SECONDS = Config.notifications.cacheTtl?.reuseWindowSeconds ?? 30;
	private static readonly SESSION_CACHE_TTL = Config.notifications.cacheTtl?.session ?? 900;
	private static SESSION_CACHE_KEY = (tokenHash: string) => `session:${tokenHash}`;

	/**
	 * Create new user session
	 */
	static async createSession(sessionData: SessionData): Promise<UserSession> {
		const {
			userId,
			sessionId,
			accessToken,
			refreshToken,
			deviceInfo,
			ipAddress,
			userAgent,
			platform,
			deviceId,
			deviceName,
		} = sessionData;

		const now = new Date();
		const accessTokenTtlSeconds = TokenService.parseTimeToSeconds(Config.auth.jwtExpiresIn);
		const refreshTokenTtlSeconds = TokenService.parseTimeToSeconds(Config.auth.jwtRefreshExpiresIn);

		return UserSession.create({
			id: sessionId, // Use the provided sessionId for standard JWT claims (sid)
			user_id: userId,
			access_token_hash: TokenService.hashToken(accessToken),
			refresh_token_hash: TokenService.hashToken(refreshToken),
			device_info: deviceInfo || {},
			ip_address: ipAddress ?? null,
			user_agent: userAgent ?? null,
			platform: platform ?? null,
			device_id: deviceId ?? null,
			device_name: deviceName ?? null,
			status: 'active',
			created_at: now,
			last_activity_at: now,
			expires_at: new Date(now.getTime() + accessTokenTtlSeconds * 1000),
			refresh_expires_at: new Date(now.getTime() + refreshTokenTtlSeconds * 1000),
		});
	}

	/**
	 * Get user by ID with sessions
	 */
	static async getUserWithSessions(userId: string): Promise<User | null> {
		return User.findByPk(userId, {
			include: [
				{
					model: UserSession,
					as: 'user_sessions',
					where: { status: 'active' },
					required: false,
				},
				{
					model: UserRole,
					as: 'user_roles',
					include: [{ model: Role, as: 'role', attributes: ['name'] }],
					required: false,
				},
			],
		});
	}

	/**
	 * Validate session token with Redis caching strategy
	 * - Check Redis cache first (O(1))
	 * - Cache miss → Query DB (O(1) via hash index) → Populate cache
	 * - DB only as fallback, not primary path
	 */
	static async validateSession(sessionToken: string): Promise<User | null> {
		try {
			const tokenHash = TokenService.hashToken(sessionToken);
			const cacheKey = this.SESSION_CACHE_KEY(tokenHash);

			// Step 1: Check Redis cache first
			const cachedUser = await this.redis.get<User>(cacheKey);
			if (cachedUser) {
				// Cache hit - verify user is still active
				if (cachedUser.status !== 'active') {
					// User inactive - invalidate cache and return null
					await this.redis.delete(cacheKey);
					return null;
				}
				return cachedUser;
			}

			// Step 2: Cache miss - Query DB
			const session = (await UserSession.findOne({
				where: { access_token_hash: tokenHash, status: 'active' },
				include: [
					{
						model: User,
						as: 'user',
						include: [
							{
								model: UserRole,
								as: 'user_roles',
							},
						],
					},
				],
			})) as SessionWithUserAndRoles | null;

			if (!session) {
				return null;
			}

			// Check if session is expired using expires_at
			if (isSessionExpired(session)) {
				await deactivateUserSession(session);
				return null;
			}

			const sessionUser = session.user;
			if (sessionUser.status !== 'active') {
				// User inactive - return null
				return null;
			}

			// Step 3: Populate cache (fire-and-forget)
			this.redis.set(cacheKey, sessionUser, this.SESSION_CACHE_TTL).catch((error) => {
				Logger.error('[SessionService] Failed to cache session data', error);
			});

			// Update session activity (fire-and-forget)
			updateSessionActivity(session).catch((error) => {
				Logger.error('[SessionService] Failed to update session activity', error);
			});

			return sessionUser;
		} catch {
			return null;
		}
	}

	/**
	 * Logout user session with cache invalidation
	 */
	static async logout(sessionToken: string): Promise<void> {
		const tokenHash = TokenService.hashToken(sessionToken);
		const cacheKey = this.SESSION_CACHE_KEY(tokenHash);

		// Invalidate cache (fire-and-forget)
		this.redis.delete(cacheKey).catch((error) => {
			Logger.error('[SessionService] Failed to delete session cache', error);
		});

		const session = await UserSession.findOne({
			where: { access_token_hash: tokenHash, status: 'active' },
		});

		if (session) {
			await deactivateUserSession(session);
		}
	}

	/**
	 * Logout from all devices — revoke all active sessions and write audit log
	 */
	static async logoutAll(userId: string, _ipAddress?: string, _userAgent?: string): Promise<void> {
		const now = new Date();

		const activeSessions = await UserSession.findAll({
			where: { user_id: userId, status: 'active' },
			attributes: ['id', 'platform', 'access_token_hash'],
		});

		if (activeSessions.length === 0) return;

		const sessionIds = activeSessions.map((s) => s.id);

		// LOGIC-HIGH-01: Invalidate all session caches with retry mechanism before DB update
		const sessionHashes = activeSessions
			.map((s) => s.access_token_hash)
			.filter((hash): hash is string => hash !== null);

		// Retry cache deletion up to 3 times with exponential backoff
		const MAX_RETRIES = 3;
		for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
			const cacheDeletionResults = await Promise.allSettled(
				sessionHashes.map((hash) => this.redis.delete(this.SESSION_CACHE_KEY(hash))),
			);

			const failures = cacheDeletionResults.filter((r) => r.status === 'rejected');
			if (failures.length === 0) {
				// All cache deletions succeeded
				break;
			}

			if (attempt === MAX_RETRIES) {
				Logger.error(
					`Failed to delete ${failures.length} session caches after ${MAX_RETRIES} retries during logoutAll`,
				);
			} else {
				// Wait before retry (exponential backoff: 100ms, 200ms, 400ms)
				await new Promise((resolve) => setTimeout(resolve, 100 * Math.pow(2, attempt - 1)));
			}
		}

		await UserSession.update({ status: 'revoked', revoked_at: now }, { where: { id: sessionIds } });

		// Write audit log for each revoked session
		await Promise.all(
			activeSessions.map((s) =>
				this.logRefreshTokenEvent({
					user_id: userId,
					session_id: s.id,
					action: 'revoked',
					reason: 'User requested logout from all devices',
				}).catch((error) => {
					Logger.error('[SessionService] Failed to log refresh token event', error);
				}),
			),
		);

		// Security notification (fire-and-forget)
		NotificationService.getInstance()
			.createAndSend({
				userId,
				title: 'Phiên đăng nhập đã bị thu hồi',
				body: 'Tất cả phiên đăng nhập đã bị thu hồi.',
				type: 'security_alert',
				channel: 'in_app',
				data: { action: 'session_revoked' },
			})
			.catch((error) => {
				Logger.error('[SessionService] Failed to send security notification for logoutAll', error);
			});
	}

	/**
	 * Get current session ID — O(1) via access_token_hash index
	 */
	static async getCurrentSessionId(accessToken: string): Promise<string | null> {
		try {
			const tokenHash = TokenService.hashToken(accessToken);
			const session = await UserSession.findOne({
				where: { access_token_hash: tokenHash, status: 'active' },
				attributes: ['id'],
			});
			return session?.id ?? null;
		} catch {
			return null;
		}
	}

	/**
	 * List active sessions for a user (paginated, with optional sort/filter)
	 */
	static async listSessions(
		userId: string,
		currentAccessToken: string,
		page: number = 1,
		pageSize: number = 10,
		sortField: string = 'last_activity_at',
		sortOrder: string = 'DESC',
		platformFilter?: string,
	): Promise<{ rows: object[]; count: number; page: number; pageSize: number }> {
		const currentSessionId = await this.getCurrentSessionId(currentAccessToken);

		// Allowlist for sort fields
		const allowedSortFields: Record<string, string> = {
			last_activity_at: 'last_activity_at',
			created_at: 'created_at',
			expires_at: 'expires_at',
		};
		const safeSortField = allowedSortFields[sortField] ?? 'last_activity_at';
		const safeSortOrder = sortOrder?.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';

		const where: Record<string, unknown> = { user_id: userId, status: 'active' };
		const VALID_PLATFORMS = ['web', 'mobile', 'zalo_miniapp', 'internal'];
		if (platformFilter && VALID_PLATFORMS.includes(platformFilter)) {
			where['platform'] = platformFilter;
		}

		const offset = (page - 1) * pageSize;

		const { rows: rawRows, count } = await UserSession.findAndCountAll({
			where,
			order: [[safeSortField, safeSortOrder]],
			limit: pageSize,
			offset,
			attributes: [
				'id',
				'platform',
				'device_id',
				'device_name',
				'ip_address',
				'user_agent',
				'last_activity_at',
				'expires_at',
				'refresh_expires_at',
				'created_at',
			],
		});

		const rows = rawRows.map((s) => ({
			id: s.id,
			platform: s.platform ?? null,
			device_id: s.device_id ?? null,
			device_name: s.device_name ?? null,
			ip_address: s.ip_address ?? null,
			user_agent: s.user_agent ?? null,
			is_current: s.id === currentSessionId,
			last_used_at: s.last_activity_at ?? null,
			expires_at: s.expires_at,
			refresh_expires_at: s.refresh_expires_at,
			created_at: s.created_at,
		}));

		return { rows, count, page, pageSize };
	}

	/**
	 * Revoke a specific session (must belong to userId) with cache invalidation
	 */
	static async revokeSession(sessionId: string, userId: string, ipAddress?: string, userAgent?: string): Promise<void> {
		const session = await UserSession.findOne({
			where: { id: sessionId, user_id: userId, status: 'active' },
		});

		if (!session) {
			throw new GenericError('NOT_FOUND');
		}

		// Invalidate cache if session has access_token_hash
		if (session.access_token_hash) {
			const cacheKey = this.SESSION_CACHE_KEY(session.access_token_hash);
			await this.redis.delete(cacheKey).catch((error) => {
				Logger.error('[SessionService] Failed to delete session cache during revoke', error);
			});
		}

		await session.update({
			status: 'revoked',
			revoked_at: new Date(),
		});

		await this.logRefreshTokenEvent({
			user_id: userId,
			session_id: sessionId,
			action: 'revoked',
			reason: 'user_revoked',
			platform: session.platform ?? undefined,
			ip_address: ipAddress,
			user_agent: userAgent,
		});
	}

	static async refreshToken(
		refreshToken: string,
		deviceInfo?: Record<string, unknown>,
		ipAddress?: string,
		userAgent?: string,
	): Promise<{ accessToken: string; refreshToken: string; user: User }> {
		// Verify refresh token
		const payload = TokenService.verifyRefreshToken(refreshToken);

		// Find user with roles only (sessions are queried separately)
		const user = await User.findByPk(payload.id, {
			include: [
				{
					model: UserRole,
					as: 'user_roles',
					include: [{ model: Role, as: 'role', attributes: ['name'] }],
					required: false,
				},
			],
		});
		if (!user) {
			throw new GenericError('NOT_FOUND');
		}

		// Find active session with matching refresh token — O(1) via refresh_token_hash index
		const refreshTokenHash = TokenService.hashToken(refreshToken);
		const matchingSession = await UserSession.findOne({
			where: { user_id: payload.id, refresh_token_hash: refreshTokenHash, status: 'active' },
			attributes: ['id', 'refresh_expires_at', 'platform'],
		});

		if (!matchingSession) {
			// SOFT MODE: Check device fingerprint/IP in short window to avoid false positives from race conditions
			const windowStart = new Date(Date.now() - this.REUSE_WINDOW_SECONDS * 1000);

			// Find recent sessions within the window
			const recentSessions = await UserSession.findAll({
				where: {
					user_id: payload.id,
					status: 'active',
					last_activity_at: { [Op.gte]: windowStart },
				},
				attributes: ['id', 'ip_address', 'user_agent', 'device_id', 'platform'],
				order: [['last_activity_at', 'DESC']],
				limit: 5,
			});

			// Extract deviceId and platform from incoming deviceInfo
			const incomingDeviceId = deviceInfo?.deviceId as string | undefined;
			const incomingPlatform = deviceInfo?.platform as string | undefined;

			// Device fingerprint check
			const isSameDevice = recentSessions.some((session) => {
				const sameIp = session.ip_address === ipAddress;
				const sameUserAgent = session.user_agent === userAgent;
				const sameDeviceId = incomingDeviceId && session.device_id === incomingDeviceId;
				const samePlatform = incomingPlatform && session.platform === incomingPlatform;

				// Consider it the same device if IP and User-Agent match, or Device ID matches
				return (sameIp && sameUserAgent) || (sameDeviceId && samePlatform);
			});

			if (isSameDevice) {
				// Same device within window - likely race condition from 2 tabs, ignore
				// Log the event but don't revoke sessions
				await this.logRefreshTokenEvent({
					user_id: payload.id,
					session_id: null,
					action: 'reused',
					reason: 'token_reuse_ignored_same_device_race_condition',
					previous_token_hash: TokenService.hashToken(refreshToken),
					ip_address: ipAddress,
					user_agent: userAgent,
					platform: recentSessions[0]?.platform ?? undefined,
				});

				throw new GenericError('UNAUTHORIZED');
			}

			// Different device or outside window - potential attack, revoke ALL active sessions
			const allActiveSessions = await UserSession.findAll({
				where: { user_id: payload.id, status: 'active' },
				attributes: ['id', 'platform'],
			});

			if (allActiveSessions.length > 0) {
				const sessionIds = allActiveSessions.map((s) => s.id);
				await UserSession.update({ status: 'revoked', revoked_at: new Date() }, { where: { id: sessionIds } });
			}

			await this.logRefreshTokenEvent({
				user_id: payload.id,
				session_id: null,
				action: 'reused',
				reason: 'token_reuse_all_sessions_revoked_different_device',
				previous_token_hash: TokenService.hashToken(refreshToken),
				ip_address: ipAddress,
				user_agent: userAgent,
				platform: recentSessions[0]?.platform ?? undefined,
			});

			// Security notification (fire-and-forget)
			NotificationService.getInstance()
				.createAndSend({
					userId: payload.id,
					title: 'Phát hiện hoạt động bất thường',
					body: 'Phát hiện token bị sử dụng lại từ thiết bị khác. Tất cả phiên đăng nhập đã bị thu hồi.',
					type: 'security_alert',
					channel: 'in_app',
				})
				.catch((error) => {
					Logger.error('[SessionService] Failed to send security notification for token reuse', error);
				});

			throw new GenericError('UNAUTHORIZED');
		}

		// Check if refresh token is expired using refresh_expires_at
		if (matchingSession.refresh_expires_at && new Date() > new Date(matchingSession.refresh_expires_at)) {
			const expiredSession = await UserSession.findByPk(matchingSession.id);
			if (expiredSession) await deactivateUserSession(expiredSession);
			throw new GenericError('TOKEN_EXPIRED');
		}

		// Generate new tokens — extract roles from user_roles association
		const roles = ((user as any).user_roles ?? []).map((ur: any) => ur.role?.name).filter(Boolean);
		const tokenInput = {
			id: user.id,
			email: user.email,
			...(user.username != null ? { username: user.username } : {}),
			roles,
			sessionId: matchingSession.id, // Use existing session ID for sid claim
		};
		const newAccessToken = TokenService.generateAccessToken(tokenInput);
		const newRefreshToken = TokenService.generateRefreshToken(tokenInput);

		// Invalidate old session cache based on the old access token hash (fire-and-forget)
		if (matchingSession.access_token_hash) {
			const oldCacheKey = this.SESSION_CACHE_KEY(matchingSession.access_token_hash);
			this.redis.delete(oldCacheKey).catch((error) => {
				Logger.error('[SessionService] Failed to delete old session cache during refresh', error);
			});
		}

		// Update session with new tokens and both token hashes
		await UserSession.update(
			{
				access_token_hash: TokenService.hashToken(newAccessToken),
				refresh_token_hash: TokenService.hashToken(newRefreshToken),
				last_activity_at: new Date(),
				...(deviceInfo && { device_info: deviceInfo }),
				...(ipAddress && { ip_address: ipAddress }),
				...(userAgent && { user_agent: userAgent }),
			},
			{
				where: { id: matchingSession.id },
			},
		);

		// Log token rotation
		await this.logRefreshTokenEvent({
			user_id: payload.id,
			session_id: matchingSession.id,
			action: 'rotated',
			new_token_hash: TokenService.hashToken(newRefreshToken),
			platform: matchingSession.platform ?? undefined,
			ip_address: ipAddress,
			user_agent: userAgent,
		});

		return {
			accessToken: newAccessToken,
			refreshToken: newRefreshToken,
			user,
		};
	}

	/**
	 * Log refresh token event to audit table
	 */
	private static async logRefreshTokenEvent(params: {
		user_id: string;
		session_id: string | null;
		action: 'issued' | 'rotated' | 'reused' | 'revoked' | 'expired';
		previous_token_hash?: string;
		new_token_hash?: string;
		ip_address?: string | undefined;
		user_agent?: string | undefined;
		platform?: 'web' | 'mobile' | 'zalo_miniapp' | 'internal' | undefined;
		reason?: string | undefined;
	}): Promise<void> {
		try {
			await RefreshTokenAuditLog.create({
				id: crypto.randomUUID(),
				user_id: params.user_id,
				session_id: params.session_id,
				action: params.action,
				previous_token_hash: params.previous_token_hash ?? null,
				new_token_hash: params.new_token_hash ?? null,
				ip_address: params.ip_address ?? null,
				user_agent: params.user_agent ?? null,
				platform: params.platform ?? null,
				reason: params.reason ?? null,
				event_version: 1,
				created_at: new Date(),
			});
		} catch {
			// Non-critical audit log — silently swallow
		}
	}
}
