'use strict';

/**
 * SINGLE SOURCE OF TRUTH for backend permission rules.
 *
 * Every endpoint that needs to know "what can this user see / do?" reads
 * from THIS file, not inline `if (user.role === 'admin')` checks. When the
 * rules change, change them here once.
 *
 * The frontend mirror lives at src/hooks/usePermissions.tsx — they MUST stay
 * in sync. The persona test scenarios in docs/auth-personas.md verify this
 * after any change.
 *
 * --- Glossary ---
 *
 * login_context  the user's session scope, set at login from the URL:
 *                  'system' = Ops, logged in via /auth (cross-tenant)
 *                  'app'    = App user, logged in via /login/<app>
 *                  'agent'  = Agency staff, logged in via /login/<app>/<agency>
 *
 * role           'admin' | 'manager' | 'agent' | 'customer_service'
 *                Roles describe the *position* in the org; login_context
 *                describes the *door they walked through this session*. Both
 *                matter — a manager logged in as an agent is still scoped
 *                to that agent for the session.
 *
 * allowed_app_ids  user.allowed_app_ids:
 *                    null   → unrestricted (typical for Ops admins)
 *                    [...]  → must be one of these
 *                    []     → no apps (treated as restricted-to-nothing)
 *
 * locked_app_id    the user's STARTING app for the session — the app
 *                  whose URL they came in via (/login/<app>). It does NOT
 *                  restrict the apps they can switch to. The full set of
 *                  reachable apps lives in allowed_app_ids. Treating
 *                  locked_app_id as a restriction would break the multi-app
 *                  dropdown for users with several IAM grants.
 */

// ── Role primitives ────────────────────────────────────────────────────
function isAdminRole(user) {
  return !!user && (user.role === 'admin' || user.role === 'manager');
}

function isSystemContext(user) { return user?.login_context === 'system'; }
function isAppContext(user)    { return user?.login_context === 'app'; }
function isAgentContext(user)  { return user?.login_context === 'agent'; }

// ── High-level personas ────────────────────────────────────────────────

/**
 * Ops = full cross-tenant authority.
 * Requires BOTH system context AND admin/manager role. Either alone is not
 * enough: a manager logged into an agent URL is NOT Ops for that session.
 */
function isOps(user) { return isSystemContext(user) && isAdminRole(user); }

/**
 * Ops READ access — broader than `isOps`. Returns true for:
 *   1. Classic Ops (system context + admin/manager), AND
 *   2. Any member of an active Ops team — regardless of role or
 *      login_context.
 *
 * Use this for READ-side visibility of shared/system resources (the main
 * email inbox feed, system-owned IMAP accounts, cross-tenant dashboards).
 * Do NOT use it for write gates — those should keep going through
 * `isOps` / `requireOps` so credential mutation stays admin-only.
 *
 * Background: the auth flow auto-downgrades any non-admin/non-manager
 * user with allowed apps to login_context='app'. That means an
 * agent-role user added to an "Ops Frontline" team will have
 * login_context='app' even though they're functionally Ops. The Ops
 * permission rule engine (server/index.cjs getEffectiveOpsRules) already
 * gates on team membership rather than login_context; this helper
 * mirrors that for read-side checks.
 *
 * Reads `user.is_ops_team_member` (baked into JWT at login + back-filled
 * by authMiddleware for legacy JWTs).
 */
function isOpsReadAccess(user) {
  return isOps(user) || user?.is_ops_team_member === true;
}

// ── App scope helpers ──────────────────────────────────────────────────

/**
 * Returns the array of app IDs this user is restricted to, or null if
 * unrestricted (Ops with no override).
 *
 * Rules (in priority order):
 *   1. allowed_app_ids set and non-empty → that list. THIS WINS.
 *      The IAM app-members registry is the authoritative list of apps
 *      the user can reach — populated at login from app_members_cache.
 *   2. allowed_app_ids unset BUT locked_app_id set → [locked_app_id].
 *      Defensive fallback for legacy JWTs without an allowed_app_ids
 *      claim.
 *   3. system-context, neither set → null (unrestricted Ops).
 *   4. otherwise → []  (no apps — restricted-to-nothing).
 *
 * IMPORTANT: locked_app_id alone DOES NOT restrict the reachable set.
 * It's the user's *starting* app for the session (the URL they came in
 * via). Treating it as a restriction would collapse the multi-app
 * dropdown — a user with allowed_app_ids=[A,B,C] and locked_app_id=A
 * would only ever see A.
 *
 * Callers SHOULD treat `null` and `[]` differently:
 *   null → no SQL `WHERE app_id IN (...)` clause at all
 *   []   → no rows (apply `WHERE 1=0` or skip the query entirely)
 *   [..] → standard `WHERE app_id IN (...)`
 */
function scopedAppIds(user) {
  if (!user) return [];
  if (Array.isArray(user.allowed_app_ids) && user.allowed_app_ids.length > 0) {
    return user.allowed_app_ids;
  }
  if (user.locked_app_id) return [user.locked_app_id];
  if (isSystemContext(user)) return null; // Ops, unrestricted
  return [];
}

/**
 * Can this user access this specific app?
 *   - Ops with no restriction: always yes
 *   - User with allowed_app_ids: yes if appId is in list
 *   - User with locked_app_id: yes only if appId === locked_app_id
 */
function canAccessApp(user, appId) {
  if (!user || !appId) return false;
  const allowed = scopedAppIds(user);
  if (allowed === null) return true; // unrestricted
  return allowed.includes(appId);
}

// ── Express helpers ────────────────────────────────────────────────────

/**
 * Reads the platform user from the request. authMiddleware sets this as
 * `req.platformUser`; callers shouldn't have to remember the exact key.
 */
function getUser(req) { return req?.platformUser || null; }

/**
 * Sends 403 and returns true if the caller is NOT Ops. Use as a guard:
 *
 *   if (requireOps(req, res)) return;   // 403 already sent, bail out
 *   ... ops-only logic ...
 */
function requireOps(req, res) {
  if (!isOps(getUser(req))) {
    res.status(403).json({ data: null, error: { message: 'Ops access required' } });
    return true;
  }
  return false;
}

/**
 * Sends 403 and returns true if the caller can't access this specific app.
 * Use BEFORE calling any per-app endpoint where the user supplies app_id:
 *
 *   if (requireAppAccess(req, res, req.query.app_id)) return;
 */
function requireAppAccess(req, res, appId) {
  if (!canAccessApp(getUser(req), appId)) {
    res.status(403).json({ data: null, error: { message: 'Access denied for this application' } });
    return true;
  }
  return false;
}

/**
 * Sends 403 and returns true if the caller is in agent context.
 * Used for endpoints that should not be reachable by agency staff at all
 * (suppliers list, email inbox configs, settings tabs, etc.).
 */
function requireNotAgent(req, res) {
  if (isAgentContext(getUser(req))) {
    res.status(403).json({ data: null, error: { message: 'Forbidden' } });
    return true;
  }
  return false;
}

/**
 * Filters an array of objects with an `id` field by the user's app scope.
 * Convenience for endpoints that proxy upstream lists.
 */
function filterByAppScope(user, items) {
  const allowed = scopedAppIds(user);
  if (allowed === null) return items; // unrestricted
  if (allowed.length === 0) return [];
  const set = new Set(allowed);
  return items.filter(it => set.has(it?.id));
}

module.exports = {
  // primitives
  isAdminRole,
  isSystemContext,
  isAppContext,
  isAgentContext,
  // personas
  isOps,
  isOpsReadAccess,
  // app scope
  scopedAppIds,
  canAccessApp,
  filterByAppScope,
  // express helpers
  getUser,
  requireOps,
  requireAppAccess,
  requireNotAgent,
};
