import { Request, Response, NextFunction } from 'express';
import { getModels } from '../models';
import { invalidateRoleDirectoryCache } from '../services/auth/roleDirectory';
import { cacheGet, cacheSet, cacheDelete, cacheClearPattern } from '../utils/redis';

// Cache TTL: 5 minutes
const PERMISSION_CACHE_TTL = 300;

/**
 * Generate cache key for a user+company permission resolution.
 */
function permissionCacheKey(userId: string, companyId: string): string {
  return `perms:${userId}:${companyId}`;
}

/**
 * Resolve whether a user has a specific permission.
 * Priority: Super Admin > User-specific deny > User-specific grant > Role permission > Default (none)
 *
 * Uses Redis cache for the resolved permission set (5-min TTL).
 */
export async function resolvePermission(
  userId: string,
  companyId: string,
  module: string,
  action: string,
  page?: string,
  feature?: string
): Promise<boolean> {
  // 1. Super-admin bypass — check user role first (cheapest check)
  const { User, Role, UserAccessOverride } = getModels();
  const user = await User.findById(userId);
  if (!user) return false;
  // Super-admin and the org main Admin always have full access by default.
  // They are never subject to role-based restrictions or user-specific
  // overrides (denies), so no grant-access configuration is required.
  if (user.role === 'super-admin' || user.role === 'admin') return true;

  // 2. Try cache
  const cacheKey = permissionCacheKey(userId, companyId);
  let cached: Record<string, Record<string, boolean>> | null = null;
  try {
    cached = await cacheGet<Record<string, Record<string, boolean>>>(cacheKey);
  } catch {
    // Redis unavailable — continue without cache
  }

  // 3. Build permission map if not cached
  if (!cached) {
    cached = await buildPermissionMap(user, userId, companyId, Role, UserAccessOverride);
    try {
      await cacheSet(cacheKey, cached, PERMISSION_CACHE_TTL);
    } catch {
      // Redis unavailable — continue without cache
    }
  }

  // 4. Check the specific permission
  const modulePerms = cached[module];
  if (!modulePerms) return false;

  // Check specific page/feature/action or wildcard
  const specificKey = [page, feature, action].filter(Boolean).join(':');
  const manageKey = [page, feature, 'manage'].filter(Boolean).join(':');
  const moduleActionKey = action;
  const moduleManageKey = 'manage';

  return !!(
    modulePerms[specificKey] ||
    modulePerms[manageKey] ||
    modulePerms[moduleActionKey] ||
    modulePerms[moduleManageKey]
  );
}

/**
 * Build a flat permission map from role + overrides.
 * Map structure: { module: { "action" | "page:feature:action" | "manage": boolean } }
 */
export async function buildPermissionMap(
  user: any,
  userId: string,
  companyId: string,
  Role: any,
  UserAccessOverride: any
): Promise<Record<string, Record<string, boolean>>> {
  const permMap: Record<string, Record<string, boolean>> = {};

  // Helper to set a permission
  const setPerm = (module: string, page: string | undefined, feature: string | undefined, action: string, value: boolean) => {
    if (!permMap[module]) permMap[module] = {};
    const key = [page, feature, action].filter(Boolean).join(':');
    permMap[module][key] = value;
  };

  // Load role — prefer roleId, then fall back to name+scope match
  let role = null;
  if (user.roleId) {
    role = await Role.findById(user.roleId);
  }
  if (!role) {
    // Fall back: find role matching user.role field, scoped to their company or global
    role = await Role.findOne({
      name: user.role,
      $or: [{ scope: 'global' }, { scope: companyId }],
      isActive: true,
    });
  }

  // Apply role permissions (base layer)
  if (role) {
    for (const perm of role.permissions) {
      for (const action of perm.actions) {
        setPerm(perm.module, perm.page, perm.feature, action, true);
      }
    }
  }

  // Load overrides
  const override = await UserAccessOverride.findOne({ userId, companyId });

  if (override) {
    // Apply grants (add permissions)
    for (const grant of override.grants) {
      for (const action of grant.actions) {
        setPerm(grant.module, grant.page, grant.feature, action, true);
      }
    }

    // Apply denies (remove permissions) — these take priority over grants
    for (const deny of override.denies) {
      for (const rawAction of deny.actions) {
        const action = rawAction.startsWith('!') ? rawAction.slice(1) : rawAction;
        setPerm(deny.module, deny.page, deny.feature, action, false);
      }
    }
  }

  return permMap;
}

/**
 * Invalidate the permission cache for a user+company.
 * Called when roles or overrides change.
 */
export async function invalidatePermissionCache(userId: string, companyId?: string): Promise<void> {
  // Role data just changed, so the 2FA policy's id → name directory is stale
  // too. Hooking it here rather than at each of the ten role mutation sites
  // means a future role endpoint cannot forget to do it. Dropping a small map
  // is cheap, so the extra clearing on an override-only change costs nothing.
  invalidateRoleDirectoryCache();

  try {
    if (companyId) {
      await cacheDelete(permissionCacheKey(userId, companyId));
    } else {
      // Clear all permission caches for this user
      await cacheClearPattern(`perms:${userId}:*`);
    }
  } catch {
    // Redis unavailable — cache will expire naturally
  }
}

/**
 * Express middleware to require a specific permission.
 * Must be used after `authenticate` middleware.
 *
 * If module is an empty string, it will be resolved from req.params.moduleId.
 * This allows dynamic module-based permission checks on generic routes.
 */
export function requirePermission(module: string, action: string, page?: string, feature?: string) {
  return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    if (!req.user) {
      res.status(401).json({ error: 'Authentication required' });
      return;
    }

    // Super admin and org main Admin bypass — full access by default,
    // never subject to role-based restrictions or overrides.
    if (req.user.role === 'super-admin' || req.user.role === 'admin') {
      next();
      return;
    }

    // Resolve dynamic module from route params if empty string passed
    const resolvedModule = module || req.params.moduleId || '';
    if (!resolvedModule) {
      res.status(400).json({ error: 'Module identifier required for permission check' });
      return;
    }

    const companyId = req.user.activeCompanyId || (req.user.companyIds?.[0] || '');
    const hasPermission = await resolvePermission(
      req.user._id.toString(),
      companyId,
      resolvedModule,
      action,
      page,
      feature
    );

    if (!hasPermission) {
      res.status(403).json({ error: 'Insufficient permissions' });
      return;
    }

    next();
  };
}