/**
 * Resolve — and AUTHORIZE — the space that gates SSH-console access for a task.
 *
 * Why this exists (spec §11 + adversarial review):
 *   `resolveTask` gates on task VISIBILITY (public / org / admin can SEE a
 *   task). Visibility is NOT space membership. The SSH access resolver, for a
 *   space-scoped connection, allows on a plain `connection.spaceId === job
 *   spaceId` match — it trusts that reaching the gate WITH that space id
 *   implies membership (true for the worker, which runs AS the task owner).
 *   The console REST / WS paths break that assumption: the acting principal is
 *   the logged-in VIEWER, who may see a public/org task without belonging to
 *   its space. Passing the task's space id on visibility alone would let a
 *   non-member open — or re-attach to — a console on that space's servers
 *   (IDOR / privilege escalation).
 *
 *   So: resolve the task's effective space (personal-WS NULL+owner→personal
 *   space, mirroring the worker's resolveToolSpaceId) and then require the
 *   ACTING USER to be a genuine owner/member of it. If not, return null — which
 *   fails closed for space-scoped connections AND correctly re-enables the
 *   legacy space-less (owner/admin/grant) path for system/global connections.
 *
 *   Admin is intentionally NOT a shortcut here: the space-scoped model has no
 *   admin bypass (unlike space-less connections — see src/ssh/access.ts). An
 *   admin who is not a space member must not silently gain SSH into that
 *   workspace's hosts just by being able to view the task.
 */

export interface ConsoleSpaceResolverRepo {
  ensurePersonalSpace(ownerId: string): Promise<{ id: string }>;
  /** Raw space row (no viewer gate) — we only read owner_id. */
  getSpace(spaceId: string): Promise<{ ownerId: string | null } | null>;
  /** space_members role for (space, user), or null if not a member. */
  getSpaceMemberRole(spaceId: string, userId: string): string | null;
}

export interface ConsoleTaskLike {
  ownerId: string | null;
  spaceId: string | null;
}

/**
 * Non-null deny sentinel. Returned when a task IS space-scoped but the acting
 * user is not authorized (non-member) or the space could not be resolved.
 *
 * Why a sentinel and not `null`: `null` means "genuine no-space (legacy) task"
 * and sends resolveAccess down the space-less owner/admin/grant path. Returning
 * `null` for an UNAUTHORIZED space-scoped task would wrongly re-open that legacy
 * path — letting the user reach a `space_id=NULL` connection they own / are
 * granted / (as admin) can bypass, from a task whose space they are not in.
 * That breaks the "a space-scoped job never sees space-less connections"
 * isolation. This sentinel is non-null and matches NO real space id, so
 * resolveAccess denies BOTH space-scoped (no id match) AND space-less
 * (jobSpaceId non-null blocks the legacy branch) with space_mismatch. Mirrors
 * the worker's `personal-unresolved:` fail-closed sentinel.
 */
export const CONSOLE_SPACE_DENY = '__ssh_console_access_denied__';

/**
 * @returns
 *   - a real space id when the acting user may use that space's space-scoped
 *     SSH connections (they are its owner or a member);
 *   - `null` ONLY for a genuine no-space (no-owner) legacy task — the legacy
 *     space-less path is then intentionally available;
 *   - {@link CONSOLE_SPACE_DENY} when the task is space-scoped but the user is
 *     not authorized, or the space could not be resolved (hard fail closed).
 */
export async function resolveConsoleTaskSpace(
  repo: ConsoleSpaceResolverRepo,
  task: ConsoleTaskLike,
  user: { id: string },
  onError?: (msg: string) => void,
): Promise<string | null> {
  let spaceId: string | null = task.spaceId ?? null;

  // Personal-WS tasks carry space_id=NULL; their connections live in the
  // owner's real personal space. Resolve NULL+owner→personal (worker parity).
  // A genuine no-owner (no-auth legacy) task has no personal space → the ONLY
  // case that stays no-space (null → legacy path).
  if (spaceId === null) {
    if (!task.ownerId) return null;
    try {
      spaceId = (await repo.ensurePersonalSpace(task.ownerId)).id;
    } catch (e) {
      onError?.(`personal-space resolve failed: ${String(e)}`);
      return CONSOLE_SPACE_DENY; // space-scoped intent, unresolved → hard deny
    }
  }

  // Membership gate: owner OR space_members. No admin shortcut (see header).
  let ownerId: string | null = null;
  try {
    ownerId = (await repo.getSpace(spaceId))?.ownerId ?? null;
  } catch (e) {
    onError?.(`space lookup failed space=${spaceId}: ${String(e)}`);
    return CONSOLE_SPACE_DENY; // hard deny
  }
  const isOwner = ownerId !== null && ownerId === user.id;
  const isMember = !isOwner && repo.getSpaceMemberRole(spaceId, user.id) !== null;
  if (!isOwner && !isMember) return CONSOLE_SPACE_DENY; // non-member: hard deny

  return spaceId;
}
