import { Req, Res } from '#interfaces/IApi';
import { NextFunction } from 'express';
import { AuthService, TokenService, SessionService, JWTPayload } from '../services/auth';
import { GenericError } from '#interfaces/error/generic';
import { detectPlatform } from './platform';
import { createDistributedRateLimit } from './rate-limiter';
import RedisService from '#services/storage/redisService';
import sequelize from '#services/database/sequelize/sequelizeService';
import { QueryTypes } from 'sequelize';
import Config from '#config';

const redis = RedisService.getInstance();

// DEBT-02: Moved to environment variable for tunability
/** Permissions cache TTL (5 minutes default) */
const PERM_CACHE_TTL = Config.notifications.cacheTtl?.perm ?? 300;
/** Cache key prefix for user permission sets */
const permCacheKey = (userId: string) => `perm:${userId}`;

type UserRole = 'user' | 'admin' | 'system_admin';

// Extend Request interface
declare module 'express' {
	interface Request {
		user?: JWTPayload;
		platform?: 'web' | 'mobile' | 'zalo_miniapp' | 'internal';
		deviceId?: string;
		deviceName?: string;
	}
}

/**
 * Authentication middleware - verifies JWT token and session
 */
export async function authenticate(req: Req, res: Res, next: NextFunction): Promise<void> {
	try {
		// Detect platform if not already detected
		if (!req.platform) {
			detectPlatform(req, res, () => {});
		}

		// Try to get token from cookie first (preferred), then fallback to Authorization header
		let token: string | undefined;

		// Priority 1: HttpOnly Cookie (most secure)
		if (req.cookies?.access_token) {
			token = req.cookies.access_token;
		}
		// Priority 2: Authorization Bearer header (backward compatibility)
		else {
			const authHeader = req.header('Authorization');
			if (authHeader) {
				const [bearer, bearerToken] = authHeader.split(' ');
				if (bearer === 'Bearer' && bearerToken) {
					token = bearerToken;
				}
			}
		}

		if (!token) {
			const error = new GenericError('UNAUTHORIZED');
			return res.error(error);
		}

		// Verify JWT token
		const decoded = TokenService.verifyAccessToken(token);

		// H-06: Use async/await instead of floating .then/.catch chain
		let user;
		try {
			user = await SessionService.validateSession(token);
		} catch {
			const error = new GenericError('UNAUTHORIZED');
			return res.error(error);
		}
		if (!user) {
			const error = new GenericError('UNAUTHORIZED');
			return res.error(error);
		}

		// Attach user to request
		req.user = decoded;
		next();
	} catch (err) {
		if (err instanceof GenericError) {
			return res.error(err);
		}
		return res.error(new GenericError('INTERNAL_SERVER_ERROR'));
	}
}

export async function attachPermissions(req: Req, res: Res, next: NextFunction): Promise<void> {
	try {
		if (!req.user?.id) {
			const error = new GenericError('UNAUTHORIZED');
			return res.error(error);
		}

		const userId = req.user.id;

		// Try Redis cache first
		type CachedPerms = { roles: string[]; permissions: string[]; roleIds: string[] };
		const cached = await redis.get<CachedPerms>(permCacheKey(userId));
		if (cached) {
			req.user = { ...req.user, ...cached };
			return next();
		}

		const rows = await sequelize.query<{
			role_id: string;
			role_name: string;
			perm_name: string | null;
			perm_key: string | null;
		}>(
			`SELECT ur.role_id, r.name AS role_name, 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>();
		const rolesSet = new Set<string>();
		const roleIdsSet = new Set<string>();

		for (const row of rows) {
			if (row.role_id) roleIdsSet.add(row.role_id);
			if (row.role_name) rolesSet.add(row.role_name);
			if (row.perm_key) permissionsSet.add(row.perm_key);
		}

		const permData: CachedPerms = {
			roles: Array.from(rolesSet),
			permissions: Array.from(permissionsSet),
			roleIds: Array.from(roleIdsSet),
		};

		// Cache the result
		await redis.set(permCacheKey(userId), permData, PERM_CACHE_TTL);

		req.user = { ...req.user, ...permData };

		return next();
	} catch (error) {
		return res.error(error);
	}
}

/**
 * Invalidate the permission cache for a specific user.
 * Call this after modifying user roles or role permissions.
 */
export async function invalidatePermissionsCache(userId: string): Promise<void> {
	await redis.delete(permCacheKey(userId));
}

/**
 * Authorization middleware - checks user roles/permissions
 */
export function authorize(allowedRoles: UserRole[] = []) {
	return (req: Req, res: Res, next: NextFunction): void => {
		if (!req.user) {
			const error = new GenericError('UNAUTHORIZED');
			return res.error(error);
		}

		// Check if user has required role(s)
		if (allowedRoles.length > 0) {
			const userRoles = req.user?.roles || [];
			const hasRole = userRoles.some((r) => allowedRoles.includes(r as UserRole));
			if (!hasRole) {
				const error = new GenericError('FORBIDDEN');
				return res.error(error);
			}
		}

		next();
	};
}

/**
 * Admin only middleware
 */
export function requireAdmin(req: Req, res: Res, next: NextFunction): void {
	return authorize(['admin', 'system_admin'])(req, res, next);
}

/**
 * Super admin only middleware
 */
export function requireSuperAdmin(req: Req, res: Res, next: NextFunction): void {
	return authorize(['system_admin'])(req, res, next);
}

/**
 * Granular permission check middleware.
 * Requires `attachPermissions` to have been run first so `req.user.permissions` is populated.
 * Accepts a single permission key (e.g. 'read:profile') or an array (any-of check).
 *
 * @example
 * router.get('/admin/users', [verify, attachPermissions, requirePermission('read:user')], handler)
 */
export function requirePermission(keys: string | string[]) {
	const required = Array.isArray(keys) ? keys : [keys];
	return (req: Req, res: Res, next: NextFunction): void => {
		if (!req.user) {
			const error = new GenericError('UNAUTHORIZED');
			return res.error(error);
		}
		const userPermissions: string[] = req.user.permissions ?? [];
		const hasPermission = required.some((k) => userPermissions.includes(k));
		if (!hasPermission) {
			const error = new GenericError('FORBIDDEN');
			return res.error(error);
		}
		next();
	};
}

/**
 * Requires the requesting user to be the resource owner or an admin.
 * Requires `authenticate` to have run first.
 *
 * @param paramKey - Route parameter name holding the resource owner's userId (default: 'id')
 */
export function requireOwnerOrAdmin(paramKey = 'id') {
	return (req: Req, res: Res, next: NextFunction): void => {
		if (!req.user) {
			const error = new GenericError('UNAUTHORIZED');
			return res.error(error);
		}
		const ownerId = req.params[paramKey];
		const isAdmin = req.user.roles?.some((r) => ['admin', 'system_admin'].includes(r));
		if (isAdmin || req.user.id === ownerId) {
			return next();
		}
		const error = new GenericError('FORBIDDEN');
		return res.error(error);
	};
}

/**
 * Optional authentication - doesn't fail if no token
 */
export function optionalAuth(req: Req, res: Res, next: NextFunction): void {
	const authHeader = req.header('Authorization');

	if (!authHeader) {
		return next();
	}

	try {
		const [bearer, token] = authHeader.split(' ');

		if (bearer === 'Bearer' && token) {
			const decoded = TokenService.verifyAccessToken(token);
			req.user = decoded;
		}
	} catch {
		// Ignore auth errors for optional auth
	}

	next();
}

/**
 * Rate limiting helper for auth endpoints (now uses distributed Redis-based rate limiting)
 */
export function createRateLimit(windowMs: number = 15 * 60 * 1000, max = 5) {
	return createDistributedRateLimit({
		windowMs,
		max,
		keyGenerator: (req) => req.ip || req.connection.remoteAddress || 'unknown',
	});
}

// Export utilities
export { AuthService };
export type { JWTPayload };

// Default export for backward compatibility
export default authenticate;
