// Per-project authorization — the single source of truth for "can this user
// touch this project?". v1 auth answers "are you logged in?"; this answers
// "which projects, and at what level?".
//
// Model (locked with Zac):
//   - role 'admin'            → global bypass; every project at 'edit'.
//   - role 'editor'/'viewer'  → scoped to projects granted to them directly
//                               (project_access subject_type='user') or via a
//                               group they belong to (subject_type='group').
//   - grant level 'view'      → read-only; 'edit' → read-write.
//
// A user's effective level on a project is the MAX of every matching grant
// (direct + each group). 'edit' outranks 'view'.
//
// All functions take an optional `db` (defaults to the shared pool) so tests
// can inject an isolated test pool.

import defaultPool from '../db/pool.js';

const LEVEL_RANK = { view: 1, edit: 2 };

export function isAdmin(user) {
  return user?.role === 'admin';
}

// Returns the higher of two levels (either may be null/undefined).
function maxLevel(a, b) {
  const ra = LEVEL_RANK[a] || 0;
  const rb = LEVEL_RANK[b] || 0;
  if (ra === 0 && rb === 0) return null;
  return ra >= rb ? a : b;
}

// Resolve every project the user can see, with their effective level.
//   admin → { all: true, ids: null, levelByProject: null }
//   else  → { all: false, ids: Set<projectId>, levelByProject: Map<projectId, 'view'|'edit'> }
export async function accessibleProjectIds(user, db = defaultPool) {
  if (isAdmin(user)) return { all: true, ids: null, levelByProject: null };

  const levelByProject = new Map();
  if (!user?.id) return { all: false, ids: new Set(), levelByProject };

  const { rows } = await db.query(
    `SELECT pa.project_id, pa.level
       FROM project_access pa
      WHERE (pa.subject_type = 'user'  AND pa.subject_id = $1)
         OR (pa.subject_type = 'group' AND pa.subject_id IN (
              SELECT group_id FROM user_groups WHERE user_id = $1
            ))`,
    [user.id]
  );

  for (const r of rows) {
    levelByProject.set(r.project_id, maxLevel(levelByProject.get(r.project_id), r.level));
  }
  return { all: false, ids: new Set(levelByProject.keys()), levelByProject };
}

// Effective level on a single project: 'edit' | 'view' | null.
export async function projectLevel(user, projectId, db = defaultPool) {
  if (isAdmin(user)) return 'edit';
  if (!user?.id || !projectId) return null;

  const { rows } = await db.query(
    `SELECT pa.level
       FROM project_access pa
      WHERE pa.project_id = $1
        AND ( (pa.subject_type = 'user'  AND pa.subject_id = $2)
           OR (pa.subject_type = 'group' AND pa.subject_id IN (
                 SELECT group_id FROM user_groups WHERE user_id = $2
               )) )`,
    [projectId, user.id]
  );

  let level = null;
  for (const r of rows) level = maxLevel(level, r.level);
  return level;
}

// Throw a 403-shaped error (caught by errorHandler) unless the user has at
// least `need` access on the project. `need` ∈ 'view' | 'edit'.
export async function assertProjectAccess(user, projectId, need = 'view', db = defaultPool) {
  if (isAdmin(user)) return;
  const have = await projectLevel(user, projectId, db);
  if (!have || (LEVEL_RANK[have] || 0) < (LEVEL_RANK[need] || 0)) {
    const err = new Error('forbidden');
    err.status = 403;
    throw err;
  }
}
