const jwt = require("jsonwebtoken");
const crypto = require("node:crypto");
const { getPrismaClient } = require("../config/prisma");

const prisma = getPrismaClient();

/**
 * Session Manager - Handles secure session management with inactivity timeout
 */

// Configuration
if (!process.env.JWT_SECRET) {
	throw new Error("JWT_SECRET environment variable is required");
}
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "1h";
const JWT_REFRESH_EXPIRES_IN = process.env.JWT_REFRESH_EXPIRES_IN || "7d";
const TFA_REMEMBER_ME_EXPIRES_IN =
	process.env.TFA_REMEMBER_ME_EXPIRES_IN || "30d";
const TFA_MAX_REMEMBER_SESSIONS = parseInt(
	process.env.TFA_MAX_REMEMBER_SESSIONS || "5",
	10,
);
const TFA_SUSPICIOUS_ACTIVITY_THRESHOLD = parseInt(
	process.env.TFA_SUSPICIOUS_ACTIVITY_THRESHOLD || "3",
	10,
);
const INACTIVITY_TIMEOUT_MINUTES = parseInt(
	process.env.SESSION_INACTIVITY_TIMEOUT_MINUTES || "30",
	10,
);

/**
 * Generate access token (short-lived)
 */
function generate_access_token(user_id, session_id) {
	return jwt.sign({ userId: user_id, sessionId: session_id }, JWT_SECRET, {
		expiresIn: JWT_EXPIRES_IN,
	});
}

/**
 * Generate refresh token (long-lived)
 */
function generate_refresh_token() {
	return crypto.randomBytes(64).toString("hex");
}

/**
 * Hash token for storage
 */
function hash_token(token) {
	return crypto.createHash("sha256").update(token).digest("hex");
}

/**
 * Parse expiration string to Date
 */
function parse_expiration(expiration_string) {
	const match = expiration_string.match(/^(\d+)([smhd])$/);
	if (!match) {
		throw new Error("Invalid expiration format");
	}

	const value = parseInt(match[1], 10);
	const unit = match[2];

	const now = new Date();
	switch (unit) {
		case "s":
			return new Date(now.getTime() + value * 1000);
		case "m":
			return new Date(now.getTime() + value * 60 * 1000);
		case "h":
			return new Date(now.getTime() + value * 60 * 60 * 1000);
		case "d":
			return new Date(now.getTime() + value * 24 * 60 * 60 * 1000);
		default:
			throw new Error("Invalid time unit");
	}
}

/**
 * Generate device fingerprint from request data
 */
function generate_device_fingerprint(req) {
	// Use the X-Device-ID header from frontend (unique per browser profile/localStorage)
	const deviceId = req.get("x-device-id");

	if (deviceId) {
		// Hash the device ID for consistent storage format
		return crypto
			.createHash("sha256")
			.update(deviceId)
			.digest("hex")
			.substring(0, 32);
	}

	// No device ID - return null (user needs to provide device ID for remember-me)
	return null;
}

/**
 * Check for suspicious activity patterns
 */
async function check_suspicious_activity(
	user_id,
	_ip_address,
	_device_fingerprint,
) {
	try {
		// Check for multiple sessions from different IPs in short time
		const recent_sessions = await prisma.user_sessions.findMany({
			where: {
				user_id: user_id,
				created_at: {
					gte: new Date(Date.now() - 24 * 60 * 60 * 1000), // Last 24 hours
				},
				is_revoked: false,
			},
			select: {
				ip_address: true,
				device_fingerprint: true,
				created_at: true,
			},
		});

		// Count unique IPs and devices
		const unique_ips = new Set(recent_sessions.map((s) => s.ip_address));
		const unique_devices = new Set(
			recent_sessions.map((s) => s.device_fingerprint),
		);

		// Flag as suspicious if more than threshold different IPs or devices in 24h
		if (
			unique_ips.size > TFA_SUSPICIOUS_ACTIVITY_THRESHOLD ||
			unique_devices.size > TFA_SUSPICIOUS_ACTIVITY_THRESHOLD
		) {
			console.warn(
				`Suspicious activity detected for user ${user_id}: ${unique_ips.size} IPs, ${unique_devices.size} devices`,
			);
			return true;
		}

		return false;
	} catch (error) {
		console.error("Error checking suspicious activity:", error);
		return false;
	}
}

/**
 * Create a new session for user
 */
async function create_session(
	user_id,
	ip_address,
	user_agent,
	remember_me = false,
	req = null,
) {
	try {
		const session_id = crypto.randomUUID();
		const refresh_token = generate_refresh_token();
		const access_token = generate_access_token(user_id, session_id);

		// Generate device fingerprint if request is available
		const device_fingerprint = req ? generate_device_fingerprint(req) : null;

		// Check for suspicious activity
		if (device_fingerprint) {
			const is_suspicious = await check_suspicious_activity(
				user_id,
				ip_address,
				device_fingerprint,
			);
			if (is_suspicious) {
				console.warn(
					`Suspicious activity detected for user ${user_id}, session creation may be restricted`,
				);
			}
		}

		// Check session limits for remember me
		if (remember_me) {
			const existing_remember_sessions = await prisma.user_sessions.count({
				where: {
					user_id: user_id,
					tfa_remember_me: true,
					is_revoked: false,
					expires_at: { gt: new Date() },
				},
			});

			// Limit remember me sessions per user
			if (existing_remember_sessions >= TFA_MAX_REMEMBER_SESSIONS) {
				throw new Error(
					"Maximum number of remembered devices reached. Please revoke an existing session first.",
				);
			}
		}

		// Use longer expiration for remember me sessions
		const expires_at = remember_me
			? parse_expiration(TFA_REMEMBER_ME_EXPIRES_IN)
			: parse_expiration(JWT_REFRESH_EXPIRES_IN);

		// Calculate TFA bypass until date for remember me sessions
		const tfa_bypass_until = remember_me
			? parse_expiration(TFA_REMEMBER_ME_EXPIRES_IN)
			: null;

		// Store session in database
		await prisma.user_sessions.create({
			data: {
				id: session_id,
				user_id: user_id,
				refresh_token: hash_token(refresh_token),
				access_token_hash: hash_token(access_token),
				ip_address: ip_address || null,
				user_agent: user_agent || null,
				device_fingerprint: device_fingerprint,
				last_login_ip: ip_address || null,
				last_activity: new Date(),
				expires_at: expires_at,
				tfa_remember_me: remember_me,
				tfa_bypass_until: tfa_bypass_until,
				login_count: 1,
			},
		});

		return {
			session_id,
			access_token,
			refresh_token,
			expires_at,
			tfa_bypass_until,
		};
	} catch (error) {
		console.error("Error creating session:", error);
		throw error;
	}
}

/**
 * Validate session and check for inactivity timeout
 */
async function validate_session(session_id, access_token) {
	try {
		const session = await prisma.user_sessions.findUnique({
			where: { id: session_id },
			include: { users: true },
		});

		if (!session) {
			return { valid: false, reason: "Session not found" };
		}

		// Check if session is revoked
		if (session.is_revoked) {
			return { valid: false, reason: "Session revoked" };
		}

		// Check if session has expired
		if (new Date() > session.expires_at) {
			await revoke_session(session_id);
			return { valid: false, reason: "Session expired" };
		}

		// Check for inactivity timeout
		const inactivity_threshold = new Date(
			Date.now() - INACTIVITY_TIMEOUT_MINUTES * 60 * 1000,
		);
		if (session.last_activity < inactivity_threshold) {
			await revoke_session(session_id);
			return {
				valid: false,
				reason: "Session inactive",
				message: `Session timed out after ${INACTIVITY_TIMEOUT_MINUTES} minutes of inactivity`,
			};
		}

		// Validate access token hash (optional security check)
		if (session.access_token_hash) {
			const provided_hash = hash_token(access_token);
			if (session.access_token_hash !== provided_hash) {
				return { valid: false, reason: "Token mismatch" };
			}
		}

		// Check if user is still active
		if (!session.users.is_active) {
			await revoke_session(session_id);
			return { valid: false, reason: "User inactive" };
		}

		return {
			valid: true,
			session,
			user: session.users,
		};
	} catch (error) {
		console.error("Error validating session:", error);
		return { valid: false, reason: "Validation error" };
	}
}

/**
 * Update session activity timestamp
 */
async function update_session_activity(session_id) {
	try {
		await prisma.user_sessions.update({
			where: { id: session_id },
			data: { last_activity: new Date() },
		});
		return true;
	} catch (error) {
		console.error("Error updating session activity:", error);
		return false;
	}
}

/**
 * Refresh access token using refresh token
 */
async function refresh_access_token(refresh_token) {
	try {
		const hashed_token = hash_token(refresh_token);

		const session = await prisma.user_sessions.findUnique({
			where: { refresh_token: hashed_token },
			include: { users: true },
		});

		if (!session) {
			return { success: false, error: "Invalid refresh token" };
		}

		// Validate session
		const validation = await validate_session(session.id, "");
		if (!validation.valid) {
			return { success: false, error: validation.reason };
		}

		// Generate new access token
		const new_access_token = generate_access_token(session.user_id, session.id);

		// Update access token hash
		await prisma.user_sessions.update({
			where: { id: session.id },
			data: {
				access_token_hash: hash_token(new_access_token),
				last_activity: new Date(),
			},
		});

		return {
			success: true,
			access_token: new_access_token,
			user: session.users,
		};
	} catch (error) {
		console.error("Error refreshing access token:", error);
		return { success: false, error: "Token refresh failed" };
	}
}

/**
 * Revoke a session
 */
async function revoke_session(session_id) {
	try {
		await prisma.user_sessions.update({
			where: { id: session_id },
			data: { is_revoked: true },
		});
		return true;
	} catch (error) {
		console.error("Error revoking session:", error);
		return false;
	}
}

/**
 * Revoke all sessions for a user
 */
async function revoke_all_user_sessions(user_id) {
	try {
		await prisma.user_sessions.updateMany({
			where: { user_id: user_id },
			data: { is_revoked: true },
		});
		return true;
	} catch (error) {
		console.error("Error revoking user sessions:", error);
		return false;
	}
}

/**
 * Clean up expired sessions (should be run periodically)
 */
async function cleanup_expired_sessions() {
	try {
		const result = await prisma.user_sessions.deleteMany({
			where: {
				OR: [{ expires_at: { lt: new Date() } }, { is_revoked: true }],
			},
		});
		console.log(`Cleaned up ${result.count} expired sessions`);
		return result.count;
	} catch (error) {
		console.error("Error cleaning up sessions:", error);
		return 0;
	}
}

/**
 * Get active sessions for a user
 */
async function get_user_sessions(user_id) {
	try {
		return await prisma.user_sessions.findMany({
			where: {
				user_id: user_id,
				is_revoked: false,
				expires_at: { gt: new Date() },
			},
			select: {
				id: true,
				ip_address: true,
				user_agent: true,
				last_activity: true,
				created_at: true,
				expires_at: true,
				tfa_remember_me: true,
				tfa_bypass_until: true,
			},
			orderBy: { last_activity: "desc" },
		});
	} catch (error) {
		console.error("Error getting user sessions:", error);
		return [];
	}
}

/**
 * Check if TFA is bypassed for a session
 */
async function is_tfa_bypassed(session_id) {
	try {
		const session = await prisma.user_sessions.findUnique({
			where: { id: session_id },
			select: {
				tfa_remember_me: true,
				tfa_bypass_until: true,
				is_revoked: true,
				expires_at: true,
			},
		});

		if (!session) {
			return false;
		}

		// Check if session is still valid
		if (session.is_revoked || new Date() > session.expires_at) {
			return false;
		}

		// Check if TFA is bypassed and still within bypass period
		if (session.tfa_remember_me && session.tfa_bypass_until) {
			return new Date() < session.tfa_bypass_until;
		}

		return false;
	} catch (error) {
		console.error("Error checking TFA bypass:", error);
		return false;
	}
}

module.exports = {
	create_session,
	validate_session,
	update_session_activity,
	refresh_access_token,
	revoke_session,
	revoke_all_user_sessions,
	cleanup_expired_sessions,
	get_user_sessions,
	is_tfa_bypassed,
	generate_device_fingerprint,
	check_suspicious_activity,
	generate_access_token,
	INACTIVITY_TIMEOUT_MINUTES,
};
