/**
 * uat/cli/lib/rbac-matrix.ts — Pure RBAC projection (specific to /uat).
 *
 * Projects a route/endpoint permission requirement onto each role, producing the
 * ground-truth `access` map a `.plantest.yml` encodes. Wildcard-aware, matching
 * the SmartStack permission model (core.nav_Permissions: Path, Action, Level,
 * IsWildcard  x  core.auth_RolePermissions). PURE — no DB access; callers pass the
 * already-loaded grants (sql-discovery.ts does the I/O).
 *
 * Verdict rules (plan section 1.4):
 *   - anonymous            -> redirect_login (never authenticated)
 *   - role grants a permission matching the requirement -> allowed
 *   - otherwise            -> denied
 */
import type { AccessVerdict } from './plantest-schema.js';

/**
 * A permission granted to a role (one row of core.auth_RolePermissions joined to
 * core.nav_Permissions). `path` is dot-segmented, e.g. "administration.users.read".
 * `isWildcard` marks a grant that also covers descendants of `path`.
 */
export interface GrantedPermission {
  path: string;
  isWildcard: boolean;
}

/** Reserved role name treated as never-authenticated. */
export const ANONYMOUS_ROLE = 'anonymous';

/** Split a dot-path into segments, dropping a trailing `*` and empty segments. */
function segments(path: string): string[] {
  return path.split('.').filter((s) => s.length > 0 && s !== '*');
}

/**
 * Does a granted permission satisfy a required permission?
 *   - non-wildcard grant: satisfies only an EXACT match
 *   - wildcard grant: satisfies the requirement when the grant's segments are a
 *     prefix of (or equal to) the requirement's segments — e.g. grant
 *     "administration.users" [wildcard] satisfies "administration.users.read",
 *     grant "administration.*" satisfies any "administration.<...>".
 */
export function permissionMatches(granted: GrantedPermission, required: string): boolean {
  const g = segments(granted.path);
  const r = segments(required);
  if (g.length === 0) return false;

  if (!granted.isWildcard) {
    return g.length === r.length && g.every((seg, i) => seg === r[i]);
  }
  if (g.length > r.length) return false;
  return g.every((seg, i) => seg === r[i]);
}

/** Verdict for ONE role given its grants and a required permission. */
export function accessFor(
  role: string,
  requiredPermission: string | undefined,
  grants: readonly GrantedPermission[],
): AccessVerdict {
  if (role === ANONYMOUS_ROLE) return 'redirect_login';
  // No permission required -> any authenticated role may access.
  if (!requiredPermission) return 'allowed';
  return grants.some((g) => permissionMatches(g, requiredPermission)) ? 'allowed' : 'denied';
}

/**
 * Build the full `access` map (every role -> verdict) for a required permission.
 * Roles absent from `grantsByRole` are treated as having no grants (=> denied,
 * unless anonymous or no permission required).
 */
export function buildAccessMap(
  roles: readonly string[],
  requiredPermission: string | undefined,
  grantsByRole: Readonly<Record<string, readonly GrantedPermission[]>>,
): Record<string, AccessVerdict> {
  const out: Record<string, AccessVerdict> = {};
  for (const role of roles) {
    out[role] = accessFor(role, requiredPermission, grantsByRole[role] ?? []);
  }
  return out;
}
