import { isRecord } from '@/shared/object';
import logger from '@/shared/logger';
import type { RequestHandler } from 'express';
import ValidationError from '@/shared/errors/validation';
import RolesDBApi from '@/db/api/roles';
import { ROLE_NAMES } from '@/shared/constants/roles';

// Cache for the unauthenticated fallback `guest` role, loaded once at startup.
let publicRoleCache: Record<string, unknown> | null = null;

async function fetchAndCachePublicRole(): Promise<void> {
  try {
    publicRoleCache = await RolesDBApi.findBy({ name: ROLE_NAMES.GUEST });

    if (!publicRoleCache) {
      logger.error(
        "WARNING: Role 'Public' not found in database during middleware startup. Check your migrations.",
      );
    } else {
      logger.info("'Public' role successfully loaded and cached.");
    }
  } catch (error) {
    logger.error(
      "Error fetching 'Public' role during middleware startup:",
      error,
    );
    throw error;
  }
}

// Trigger role caching at module load (application startup).
fetchAndCachePublicRole().catch((error) => {
  logger.error(
    'Critical error during permissions middleware initialization:',
    error,
  );
});

function permissionNamesFrom(value: unknown): string[] {
  if (!Array.isArray(value)) {
    return [];
  }
  return value
    .map((p) => (isRecord(p) && typeof p.name === 'string' ? p.name : null))
    .filter((name): name is string => name !== null);
}

/**
 * Resolves the permission names granted by a role, supporting both a loaded
 * Sequelize instance (`getPermissions()`) and a plain record (`permissions`).
 * Returns null when the role object has neither (invalid format).
 */
async function resolveRolePermissions(
  effectiveRole: unknown,
): Promise<string[] | null> {
  if (!isRecord(effectiveRole)) {
    return null;
  }

  // Prefer permissions already eager-loaded on the role (the per-request user
  // and the cached Public role both carry them), avoiding an extra query.
  if (Array.isArray(effectiveRole.permissions)) {
    return permissionNamesFrom(effectiveRole.permissions);
  }

  const getPermissions = effectiveRole.getPermissions;
  if (typeof getPermissions === 'function') {
    const perms: unknown = await getPermissions.call(effectiveRole);
    return permissionNamesFrom(perms);
  }

  return null;
}

function roleNameOf(effectiveRole: unknown): string {
  return isRecord(effectiveRole) && typeof effectiveRole.name === 'string'
    ? effectiveRole.name
    : 'unknown role';
}

/** Middleware: allow the request only if the effective role has `permission`. */
function checkPermissions(permission: string): RequestHandler {
  return async (req, _res, next) => {
    const currentUser = req.currentUser;

    // 1. Self-access bypass — read-only, on the caller's own record by route
    //    param. (A `req.body.id` bypass would let any user pass a write guard by
    //    putting their own id in the body; profile self-edits go through the
    //    dedicated `/api/auth/profile` endpoint.)
    if (
      currentUser &&
      req.method === 'GET' &&
      currentUser.id === req.params.id
    ) {
      return next();
    }

    // 2. Global-access roles (system scope) bypass per-permission checks.
    if (currentUser?.app_role?.globalAccess === true) {
      return next();
    }

    // 3. Custom (per-user) permissions.
    if (currentUser) {
      const customPermissions = currentUser.custom_permissions ?? [];
      if (customPermissions.some((cp) => cp.name === permission)) {
        return next();
      }
    }

    try {
      // 3. Determine the effective role (assigned role or cached 'Public').
      let effectiveRole: unknown;
      if (currentUser && currentUser.app_role) {
        effectiveRole = currentUser.app_role;
      } else if (!publicRoleCache) {
        logger.error(
          'Public role cache is empty. Attempting synchronous fetch...',
        );
        effectiveRole = await RolesDBApi.findBy({ name: ROLE_NAMES.GUEST });
        if (!effectiveRole) {
          return next(
            new Error(
              'Internal Server Error: Public role missing and cannot be fetched.',
            ),
          );
        }
      } else {
        effectiveRole = publicRoleCache;
      }

      if (!effectiveRole) {
        return next(
          new Error(
            'Internal Server Error: Could not determine effective role.',
          ),
        );
      }

      // 4. Check the effective role's permissions.
      const rolePermissions = await resolveRolePermissions(effectiveRole);
      if (rolePermissions === null) {
        logger.error(
          'Role object lacks getPermissions() method or permissions property:',
          effectiveRole,
        );
        return next(
          new Error('Internal Server Error: Invalid role object format.'),
        );
      }

      if (rolePermissions.includes(permission)) {
        next();
      } else {
        logger.error(
          `Role '${roleNameOf(effectiveRole)}' denied access to '${permission}'.`,
        );
        next(new ValidationError('auth.forbidden'));
      }
    } catch (error) {
      logger.error('Error during permission check:', error);
      next(error);
    }
  };
}

const METHOD_MAP: Record<string, string> = {
  POST: 'CREATE',
  GET: 'READ',
  PUT: 'UPDATE',
  PATCH: 'UPDATE',
  DELETE: 'DELETE',
};

/** Middleware: check the CRUD permission derived from method + entity name. */
function checkCrudPermissions(name: string): RequestHandler {
  return (req, res, next) => {
    const permissionName = `${METHOD_MAP[req.method]}_${name.toUpperCase()}`;
    checkPermissions(permissionName)(req, res, next);
  };
}

export default {
  checkPermissions,
  checkCrudPermissions,
};
