/**
 * lib/shared-cloud.js — Hybrid local-first ⇄ Supabase shared layer.
 *
 * THE MODEL
 *   Personal work stays local. Local SQLite + the filesystem are the source of
 *   truth for every private artifact and local run. Supabase is the *cloud
 *   coordination layer* for org collaboration: auth/orgs/members, explicitly
 *   shared spaces, and metadata (optionally payloads) for artifacts a user has
 *   explicitly published. Nothing leaves the machine implicitly — publishing is
 *   always an explicit, audited action and requires Supabase to be configured.
 *
 * SEPARATION OF CONCERNS
 *   - Local-only artifacts: rows in `artifacts` with NO row in
 *     `shared_publications`. This is the default for everything.
 *   - Published/shared artifacts: an explicit `publishArtifact()` call writes a
 *     `shared_publications` ledger row locally AND upserts cloud metadata into
 *     Supabase `shared_artifacts`. `getPublishStatus()` reads ONLY the local
 *     ledger (no network) so callers can cheaply tell the two apart.
 *
 * SECRET SAFETY
 *   This module is SERVER-ONLY. It resolves the Supabase service-role key from
 *   the secrets vault (or process.env during migration) and must never be
 *   imported into browser/client code. `assertServerOnly()` throws if it is.
 *   No secret value is ever returned, logged, or placed in audit/ledger rows.
 *
 * INDEPENDENT OF MADARX_AUTH_MODE
 *   Shared-cloud readiness keys off SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY,
 *   NOT MADARX_AUTH_MODE. Sharing can be enabled while auth stays local, and the
 *   product never has to flip globally to supabase mode.
 */

import { createHash } from 'node:crypto';
import { getDb, nowIso, newId } from './db.js';
import { getSecret } from './secrets.js';
import { wrapFetch } from './egress.js';
import { appendComplianceAudit } from './compliance-audit.js';
import { getProject } from './projects.js';

// ── Server-only guard ─────────────────────────────────────────────────────────

/**
 * Throws if this module is reached from browser/client code. The service-role
 * key must never cross the network to a browser.
 */
export function assertServerOnly() {
  if (typeof window !== 'undefined') {
    throw new Error('lib/shared-cloud.js is server-only and must not be imported into client code.');
  }
}

// ── Credential resolution (vault → process.env), identical to tool adapters ────

function resolveCredential(name, options = {}) {
  try {
    const secret = getSecret(name, {
      agent_id: options.agent_id,
      project_id: options.project_id,
    });
    if (secret) return secret;
  } catch {
    // Vault not ready (e.g. very early boot) — fall through to process.env.
  }
  return process.env[name] || null;
}

/**
 * Returns true when the shared layer is fully configured (URL + service-role
 * key resolvable). Pure boolean — never returns or logs the secret itself.
 *
 * @param {{ agent_id?: string, project_id?: string }} [options]
 * @returns {boolean}
 */
export function isSharedCloudReady(options = {}) {
  assertServerOnly();
  const url = resolveCredential('SUPABASE_URL', options);
  const key = resolveCredential('SUPABASE_SERVICE_ROLE_KEY', options);
  return Boolean(url && key);
}

// ── Lazy REST client (PostgREST over fetch; no @supabase/* hard dep) ──────────

function buildFilterParams(url, filters = {}) {
  for (const [column, value] of Object.entries(filters)) {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
      throw new Error(`Invalid filter column: ${column}`);
    }
    url.searchParams.set(column, String(value).includes('.') ? String(value) : `eq.${value}`);
  }
}

/**
 * Builds a PostgREST client over fetch. Shared by BOTH the service-role admin
 * client (RLS-bypassing, owner/server) and the user-JWT client (RLS-applied,
 * member). The ONLY difference is the auth headers — factored here (DRY) per
 * eng-review decision 2 so member publish flows through RLS with the same
 * select/upsert/delete/rpc surface.
 *
 * @param {object} args
 * @param {string} args.baseUrl   Supabase project URL.
 * @param {string} args.apikey    The `apikey` header value (anon or service key).
 * @param {string} args.bearer    The `Authorization: Bearer` value (service key or user JWT).
 * @param {string|null} [args.agentId]
 */
function _buildRestClient({ baseUrl, apikey, bearer, agentId = null }) {
  const rest = `${baseUrl.replace(/\/+$/g, '')}/rest/v1`;

  function authHeaders(extra = {}) {
    return { Authorization: `Bearer ${bearer}`, apikey, Accept: 'application/json', ...extra };
  }

  async function readError(res) {
    let body = {};
    try { body = await res.json(); } catch { /* non-JSON body */ }
    return body.message || body.error || res.statusText || `HTTP ${res.status}`;
  }

  return {
    /** SELECT rows from a shared-layer table. */
    async select(table, { filters = {}, select = '*', limit = 100 } = {}) {
      const url = new URL(`${rest}/${table}`);
      url.searchParams.set('select', select);
      url.searchParams.set('limit', String(Math.min(Math.max(Number(limit) || 1, 1), 1000)));
      buildFilterParams(url, filters);
      const res = await wrapFetch('shared_cloud_select', agentId)(url, { headers: authHeaders() });
      if (!res.ok) throw new Error(`shared_cloud select(${table}) failed: ${await readError(res)}`);
      return res.json();
    },

    /** UPSERT rows into a shared-layer table; returns the representation. */
    async upsert(table, rows, { onConflict } = {}) {
      const payload = Array.isArray(rows) ? rows : [rows];
      const url = new URL(`${rest}/${table}`);
      if (onConflict) url.searchParams.set('on_conflict', String(onConflict));
      const res = await wrapFetch('shared_cloud_upsert', agentId)(url, {
        method: 'POST',
        headers: authHeaders({
          'Content-Type': 'application/json',
          Prefer: 'resolution=merge-duplicates,return=representation',
        }),
        body: JSON.stringify(payload),
      });
      if (!res.ok) throw new Error(`shared_cloud upsert(${table}) failed: ${await readError(res)}`);
      return res.json();
    },

    /** DELETE rows from a shared-layer table; filters are required. */
    async delete(table, { filters } = {}) {
      if (!filters || !Object.keys(filters).length) {
        throw new Error('shared_cloud delete requires filters; bulk delete is blocked.');
      }
      const url = new URL(`${rest}/${table}`);
      buildFilterParams(url, filters);
      const res = await wrapFetch('shared_cloud_delete', agentId)(url, {
        method: 'DELETE',
        headers: authHeaders({ Prefer: 'return=representation' }),
      });
      if (!res.ok) throw new Error(`shared_cloud delete(${table}) failed: ${await readError(res)}`);
      return res.json();
    },

    /** Calls a Postgres function (SECURITY DEFINER RPC). Used for invite redemption. */
    async rpc(fn, args = {}) {
      if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(fn)) throw new Error(`Invalid rpc function name: ${fn}`);
      const url = new URL(`${rest}/rpc/${fn}`);
      const res = await wrapFetch('shared_cloud_rpc', agentId)(url, {
        method: 'POST',
        headers: authHeaders({ 'Content-Type': 'application/json' }),
        body: JSON.stringify(args ?? {}),
      });
      if (!res.ok) throw new Error(`shared_cloud rpc(${fn}) failed: ${await readError(res)}`);
      return res.json();
    },
  };
}

/**
 * Returns true when the URL + anon key are resolvable — the readiness bar for a
 * MEMBER instance (which has the anon key but NOT the service-role key). Member
 * publish/redeem flows through RLS with the user's JWT.
 */
export function isSharedCloudUserReady(options = {}) {
  assertServerOnly();
  const url = resolveCredential('SUPABASE_URL', options);
  const anon = resolveCredential('SUPABASE_ANON_KEY', options);
  return Boolean(url && anon);
}

/**
 * Builds the lazy server-side ADMIN client (service-role key over PostgREST,
 * RLS-bypassing, server-only). Every request goes through wrapFetch() for egress
 * logging. Throws if the shared layer is not configured — gate on
 * isSharedCloudReady() first.
 *
 * @param {{ agent_id?: string, project_id?: string }} [options]
 */
export function getSharedCloudAdmin(options = {}) {
  assertServerOnly();
  const baseUrl = resolveCredential('SUPABASE_URL', options);
  const key = resolveCredential('SUPABASE_SERVICE_ROLE_KEY', options);
  if (!baseUrl || !key) {
    throw new Error('Shared cloud not configured: set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY.');
  }
  return _buildRestClient({ baseUrl, apikey: key, bearer: key, agentId: options.agent_id ?? null });
}

/**
 * Builds a USER-SCOPED client: the anon key as `apikey` + the member's JWT as the
 * `Authorization: Bearer` token, so PostgREST applies RLS as that user. This is
 * how member instances publish and redeem WITHOUT ever holding the service-role
 * key (eng-review decision 2). Throws if URL/anon are not configured.
 *
 * @param {string} accessToken  The member's Supabase access token (JWT).
 * @param {{ agent_id?: string, project_id?: string }} [options]
 */
export function getSharedCloudUserClient(accessToken, options = {}) {
  assertServerOnly();
  if (!accessToken) throw new Error('getSharedCloudUserClient: an access token (user JWT) is required');
  const baseUrl = resolveCredential('SUPABASE_URL', options);
  const anon = resolveCredential('SUPABASE_ANON_KEY', options);
  if (!baseUrl || !anon) {
    throw new Error('Shared cloud (user) not configured: set SUPABASE_URL and SUPABASE_ANON_KEY.');
  }
  return _buildRestClient({ baseUrl, apikey: anon, bearer: accessToken, agentId: options.agent_id ?? null });
}

/**
 * Pure decision: which cloud client should a publish/unpublish use?
 *   • service-role key present → 'admin' (owner/server, RLS-bypassing).
 *   • else anon key + a member JWT → 'user' (RLS-applied; member publish).
 *   • neither → null (not configured).
 * Exported for direct unit testing without network or import mocking.
 */
export function selectCloudClientKind({ serviceReady, userReady, accessToken }) {
  if (serviceReady) return 'admin';
  if (userReady && accessToken) return 'user';
  return null;
}

/**
 * Resolves the concrete client for a publish/unpublish call. Owner instances use
 * the service-role admin client; member instances (no service key) publish under
 * their own JWT through RLS (eng-review decision 2 — the service key never
 * reaches a member machine).
 */
function _resolvePublishClient(args, options) {
  const accessToken = args?.session?.supabase_access_token || args?.accessToken || null;
  const kind = selectCloudClientKind({
    serviceReady: isSharedCloudReady(options),
    userReady: isSharedCloudUserReady(options),
    accessToken,
  });
  if (kind === 'admin') return { client: getSharedCloudAdmin(options), kind };
  if (kind === 'user') return { client: getSharedCloudUserClient(accessToken, options), kind };
  return { client: null, kind: null };
}

// ── Metadata mappers (pure) ───────────────────────────────────────────────────

const VALID_SCOPES = Object.freeze(['private', 'team', 'org', 'link']);

function sha256Hex(text) {
  return createHash('sha256').update(String(text ?? ''), 'utf8').digest('hex');
}

/**
 * Maps a local artifact row → the cloud `shared_artifacts` metadata shape.
 *
 * By default ONLY metadata is mapped — the artifact body is NOT included, so a
 * publish that forgets `includePayload` can never leak content. When
 * `includePayload` is true the body and its byte size are attached as well.
 *
 * Pure function: no DB or network access.
 *
 * @param {object} artifact  Local artifacts row ({ id, title, kind, content, ... }).
 * @param {object} opts
 * @param {string} opts.org_id            Required.
 * @param {string} [opts.local_space_id]  Local space/project id (source of truth).
 * @param {string} [opts.cloud_space_id]  Cloud shared_spaces.id, if already mapped.
 * @param {string} [opts.scope]           'private'|'team'|'org'|'link' (default 'org').
 * @param {string} [opts.published_by]    Cloud user id (uuid) of the publisher.
 * @param {boolean} [opts.includePayload] When true, attach `content`/`byte_size`.
 */
export function mapArtifactToCloudMetadata(artifact, opts = {}) {
  if (!artifact || !artifact.id) throw new Error('mapArtifactToCloudMetadata: artifact with id is required');
  if (!opts.org_id) throw new Error('mapArtifactToCloudMetadata: org_id is required');
  const scope = opts.scope || 'org';
  if (!VALID_SCOPES.includes(scope)) throw new Error(`mapArtifactToCloudMetadata: invalid scope "${scope}"`);

  const content = artifact.content ?? artifact.source_md ?? '';
  const meta = {
    org_id: opts.org_id,
    space_id: opts.cloud_space_id ?? null,
    local_artifact_id: artifact.id,
    title: artifact.title || 'Untitled',
    kind: artifact.kind || 'html',
    scope,
    content_hash: sha256Hex(content),
    byte_size: Buffer.byteLength(String(content), 'utf8'),
    published_by: opts.published_by ?? null,
    // content is the body — ONLY present on explicit opt-in.
    content: opts.includePayload ? String(content) : null,
  };
  return meta;
}

/**
 * Maps a local space/project row → the cloud `shared_spaces` metadata shape.
 * Pure function.
 *
 * @param {object} space  Local projects/space row ({ id, slug, name, description }).
 * @param {object} opts   { org_id, visibility?, created_by? }.
 */
export function mapSpaceToCloudMetadata(space, opts = {}) {
  if (!space || !space.id) throw new Error('mapSpaceToCloudMetadata: space with id is required');
  if (!opts.org_id) throw new Error('mapSpaceToCloudMetadata: org_id is required');
  const visibility = opts.visibility || 'org';
  if (!['org', 'team', 'private'].includes(visibility)) {
    throw new Error(`mapSpaceToCloudMetadata: invalid visibility "${visibility}"`);
  }
  return {
    org_id: opts.org_id,
    local_space_id: space.id,
    slug: space.slug || space.id,
    name: space.name || space.slug || space.id,
    description: space.description ?? null,
    visibility,
    created_by: opts.created_by ?? null,
  };
}

/**
 * Validates a cloud artifact-metadata object's shape. Throws on any violation.
 * Used to guarantee we never push malformed rows to the shared layer. Returns
 * the same object on success for chaining.
 */
export function validateSharedArtifactMetadata(meta) {
  if (!meta || typeof meta !== 'object') throw new Error('shared metadata must be an object');
  const required = ['org_id', 'local_artifact_id', 'title', 'kind', 'scope', 'content_hash'];
  for (const key of required) {
    if (meta[key] == null || meta[key] === '') throw new Error(`shared metadata missing required field: ${key}`);
  }
  if (!VALID_SCOPES.includes(meta.scope)) throw new Error(`shared metadata invalid scope: ${meta.scope}`);
  if (typeof meta.byte_size !== 'number' || meta.byte_size < 0) {
    throw new Error('shared metadata byte_size must be a non-negative number');
  }
  if (!/^[0-9a-f]{64}$/.test(meta.content_hash)) {
    throw new Error('shared metadata content_hash must be a sha256 hex digest');
  }
  return meta;
}

// ── Local publication ledger (no network) ─────────────────────────────────────

function _readArtifact(artifactId) {
  const db = getDb();
  return db.prepare('SELECT * FROM artifacts WHERE id = ?').get(artifactId) || null;
}

/**
 * Returns the local publish status for an artifact. Reads ONLY the local ledger
 * — never touches the network — so the default (no row) cheaply identifies a
 * private, never-uploaded artifact.
 *
 * @returns {{ published: boolean, status: string, cloud_artifact_id: string|null,
 *             scope: string|null, last_synced_at: string|null }}
 */
export function getPublishStatus(artifactId) {
  const db = getDb();
  const row = db
    .prepare('SELECT * FROM shared_publications WHERE artifact_id = ?')
    .get(artifactId);
  if (!row) {
    return { published: false, status: 'local', cloud_artifact_id: null, scope: null, last_synced_at: null };
  }
  return {
    published: row.status === 'published',
    status: row.status,
    cloud_artifact_id: row.cloud_artifact_id || null,
    cloud_space_id: row.cloud_space_id || null,
    scope: row.scope || null,
    content_hash: row.content_hash || null,
    include_payload: row.include_payload === 1,
    last_synced_at: row.last_synced_at || null,
    last_error: row.last_error || null,
  };
}

/** Lists ledger rows, optionally filtered by status. Local read only. */
export function listPublications({ status } = {}) {
  const db = getDb();
  if (status) {
    return db.prepare('SELECT * FROM shared_publications WHERE status = ? ORDER BY published_at DESC').all(status);
  }
  return db.prepare('SELECT * FROM shared_publications ORDER BY published_at DESC').all();
}

function _upsertLedger(fields) {
  const db = getDb();
  const existing = db.prepare('SELECT id FROM shared_publications WHERE artifact_id = ?').get(fields.artifact_id);
  const id = existing?.id || newId('pub');
  db.prepare(
    `INSERT INTO shared_publications
       (id, artifact_id, space_id, org_id, cloud_artifact_id, cloud_space_id,
        scope, content_hash, include_payload, status, last_error,
        published_by, published_at, last_synced_at, unpublished_at)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
     ON CONFLICT(artifact_id) DO UPDATE SET
       space_id          = excluded.space_id,
       org_id            = excluded.org_id,
       cloud_artifact_id = excluded.cloud_artifact_id,
       cloud_space_id    = excluded.cloud_space_id,
       scope             = excluded.scope,
       content_hash      = excluded.content_hash,
       include_payload   = excluded.include_payload,
       status            = excluded.status,
       last_error        = excluded.last_error,
       published_by      = excluded.published_by,
       last_synced_at    = excluded.last_synced_at,
       unpublished_at    = excluded.unpublished_at`
  ).run(
    id,
    fields.artifact_id,
    fields.space_id ?? null,
    fields.org_id,
    fields.cloud_artifact_id ?? null,
    fields.cloud_space_id ?? null,
    fields.scope,
    fields.content_hash ?? null,
    fields.include_payload ? 1 : 0,
    fields.status,
    fields.last_error ?? null,
    fields.published_by ?? null,
    fields.published_at ?? nowIso(),
    fields.last_synced_at ?? null,
    fields.unpublished_at ?? null
  );
  return id;
}

// ── Publish / unpublish primitives ────────────────────────────────────────────

/**
 * Explicitly publishes a local artifact's metadata (and optionally its payload)
 * to the Supabase shared layer. This is the ONLY path by which artifact data
 * leaves the machine, and it is always explicit + audited.
 *
 * Guards:
 *   - Throws if the shared layer is not configured (isSharedCloudReady()===false).
 *   - Throws if the artifact does not exist locally.
 *   - Default `includePayload=false` → only metadata is uploaded.
 *
 * @param {object} args
 * @param {string} args.artifact_id         Local artifact id. Required.
 * @param {object} args.session             { user_id, org_id, ... }. Required for org/audit.
 * @param {string} [args.org_id]            Override org (defaults to session.org_id).
 * @param {string} [args.scope]             'private'|'team'|'org'|'link' (default 'org').
 * @param {string} [args.local_space_id]    Local space/project id.
 * @param {string} [args.cloud_space_id]    Cloud shared_spaces.id, if already mapped.
 * @param {string} [args.published_by]      Cloud user uuid of publisher.
 * @param {boolean} [args.includePayload]   Upload the body too (default false).
 * @param {object} [options]                { agent_id, project_id } for credential scope.
 */
/**
 * Upserts the org row into the cloud `organizations` table so cloud
 * org_memberships / org_invites (both FK → organizations.id) can reference it.
 *
 * Why this exists: orgs are created in LOCAL SQLite. Without mirroring the org
 * to the cloud first, the invite mirror (org_invites) and the redeem RPC (which
 * inserts org_memberships) both FK-fail against a non-existent organizations
 * row — the invite looks created locally but cloud joins silently break. This
 * is the missing half of the local-first → shared-layer bridge for orgs.
 *
 * Best-effort + idempotent: upsert on id. No-op (returns false) when the shared
 * layer is not configured. The id/slug are stable; name/plan are kept in sync so
 * a rename here propagates to every member's view of the org.
 *
 * @param {{ id: string, slug: string, name: string, plan?: string }} org
 * @param {object} [options]  { agent_id, project_id } for credential scope.
 * @returns {Promise<boolean>} true if the cloud upsert ran, false if skipped.
 */
export async function ensureCloudOrg(org, options = {}) {
  assertServerOnly();
  if (!org?.id || !org?.slug || !org?.name) {
    throw new Error('ensureCloudOrg: id, slug, and name are required');
  }
  if (!isSharedCloudReady(options)) return false;
  const row = { id: org.id, slug: org.slug, name: org.name, plan: org.plan || 'free' };
  await getSharedCloudAdmin(options).upsert('organizations', row, { onConflict: 'id' });
  return true;
}

export async function publishArtifact(args = {}, options = {}) {
  assertServerOnly();
  const { artifact_id, session, scope = 'org', includePayload = false } = args;
  if (!artifact_id) throw new Error('publishArtifact: artifact_id is required');
  const org_id = args.org_id || session?.org_id;
  if (!org_id) throw new Error('publishArtifact: org_id (or session.org_id) is required');

  // Owner → service-role admin client; member → user-JWT client (RLS applies).
  const { client } = _resolvePublishClient(args, options);
  if (!client) {
    throw new Error(
      'publishArtifact: shared cloud is not configured. Owner instances need ' +
      'SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY; member instances need ' +
      'SUPABASE_URL + SUPABASE_ANON_KEY and a signed-in session.'
    );
  }

  const artifact = _readArtifact(artifact_id);
  if (!artifact) throw new Error(`publishArtifact: artifact ${artifact_id} not found locally`);

  // Tie the artifact to its shared space automatically: if the caller did not
  // pass an explicit cloud_space_id but the artifact's local space/project has
  // already been published, reuse that space's cloud id so the artifact lands
  // inside the shared folder/space rather than orphaned at the org root.
  let cloud_space_id = args.cloud_space_id ?? null;
  if (!cloud_space_id && args.local_space_id) {
    const spaceRow =
      _readSpaceLedger(args.local_space_id, 'project') ||
      _readSpaceLedger(args.local_space_id, 'dashboard_space');
    if (spaceRow && spaceRow.status === 'published' && spaceRow.cloud_space_id) {
      cloud_space_id = spaceRow.cloud_space_id;
    }
  }

  const meta = mapArtifactToCloudMetadata(artifact, {
    org_id,
    local_space_id: args.local_space_id,
    cloud_space_id,
    scope,
    published_by: args.published_by,
    includePayload,
  });
  validateSharedArtifactMetadata(meta);

  let cloudRow = null;
  try {
    const returned = await client.upsert('shared_artifacts', meta, { onConflict: 'org_id,local_artifact_id' });
    cloudRow = Array.isArray(returned) ? returned[0] : returned;
  } catch (err) {
    // Record the failure in the ledger so the artifact is not silently "stuck".
    _upsertLedger({
      artifact_id, org_id, scope,
      space_id: args.local_space_id,
      cloud_space_id,
      content_hash: meta.content_hash,
      include_payload: includePayload,
      status: 'error',
      last_error: err.message,
      published_by: session?.user_id,
    });
    appendComplianceAudit({
      user_id: session?.user_id, org_id,
      action: 'artifact.shared.publish.error',
      action_target_id: artifact_id, action_target_type: 'artifact',
      status: 'error',
      summary: `Failed to publish artifact ${artifact_id} to shared cloud`,
      meta: { scope, include_payload: includePayload, error: err.message },
    });
    throw err;
  }

  _upsertLedger({
    artifact_id, org_id, scope,
    space_id: args.local_space_id,
    cloud_artifact_id: cloudRow?.id ?? null,
    cloud_space_id,
    content_hash: meta.content_hash,
    include_payload: includePayload,
    status: 'published',
    published_by: session?.user_id,
    published_at: nowIso(),
    last_synced_at: nowIso(),
  });

  appendComplianceAudit({
    user_id: session?.user_id, org_id,
    action: 'artifact.shared.published',
    action_target_id: artifact_id, action_target_type: 'artifact',
    status: 'success',
    summary: `Published artifact ${artifact_id} to shared cloud (scope: ${scope})`,
    meta: { scope, include_payload: includePayload, cloud_artifact_id: cloudRow?.id ?? null },
  });

  return getPublishStatus(artifact_id);
}

/**
 * Explicitly unpublishes an artifact from the shared layer: removes the cloud
 * `shared_artifacts` row and marks the local ledger 'unpublished'. The local
 * artifact itself is never touched — it remains the source of truth.
 */
export async function unpublishArtifact(args = {}, options = {}) {
  assertServerOnly();
  const { artifact_id, session } = args;
  if (!artifact_id) throw new Error('unpublishArtifact: artifact_id is required');
  const org_id = args.org_id || session?.org_id;
  if (!org_id) throw new Error('unpublishArtifact: org_id (or session.org_id) is required');

  const { client } = _resolvePublishClient(args, options);
  if (!client) throw new Error('unpublishArtifact: shared cloud is not configured.');
  await client.delete('shared_artifacts', { filters: { org_id, local_artifact_id: artifact_id } });

  _upsertLedger({
    artifact_id, org_id,
    scope: 'private',
    status: 'unpublished',
    published_by: session?.user_id,
    unpublished_at: nowIso(),
    last_synced_at: nowIso(),
  });

  appendComplianceAudit({
    user_id: session?.user_id, org_id,
    action: 'artifact.shared.unpublished',
    action_target_id: artifact_id, action_target_type: 'artifact',
    status: 'success',
    summary: `Unpublished artifact ${artifact_id} from shared cloud`,
  });

  return getPublishStatus(artifact_id);
}

// ════════════════════════════════════════════════════════════════════════════
//  SHAREABLE SPACES  — dashboard/UI spaces and project folders
//
//  A teammate collaborates on a shared *space*. There are two kinds, both mapped
//  onto the cloud `shared_spaces` table and distinguished by `space_type`:
//
//    'dashboard_space' — a UI space from the local `dashboard_spaces` table.
//    'project'         — a project folder (local `projects` row) plus a
//                        metadata-only index of its folders/files/artifacts in
//                        `shared_space_resources`.
//
//  The same local-first guarantees as artifacts apply:
//    - Nothing is shared until an explicit publish call.
//    - Local SQLite + the filesystem stay the source of truth.
//    - File BODIES are never uploaded. Project sharing publishes resource
//      *metadata* only (name/path/kind/size/sha256/local_only).
//    - `local_only` files are excluded by default and require an explicit,
//      double opt-in (`includeLocalOnly` + `allowLocalOnlyOverride`) before even
//      their metadata is shared — and the body is still never uploaded.
// ════════════════════════════════════════════════════════════════════════════

const VALID_SPACE_VISIBILITY = Object.freeze(['org', 'team', 'private']);
const VALID_SPACE_TYPES = Object.freeze(['dashboard_space', 'project']);
const VALID_RESOURCE_TYPES = Object.freeze(['folder', 'file', 'artifact']);

function assertVisibility(visibility, where) {
  if (!VALID_SPACE_VISIBILITY.includes(visibility)) {
    throw new Error(`${where}: invalid visibility "${visibility}" (expected one of ${VALID_SPACE_VISIBILITY.join('|')})`);
  }
}

function assertSpaceType(space_type, where) {
  if (!VALID_SPACE_TYPES.includes(space_type)) {
    throw new Error(`${where}: invalid space_type "${space_type}" (expected one of ${VALID_SPACE_TYPES.join('|')})`);
  }
}

// ── Space metadata mappers (pure) ──────────────────────────────────────────────

/**
 * Maps a local `dashboard_spaces` row → the cloud `shared_spaces` shape with
 * space_type='dashboard_space'. Pure function (no DB/network).
 *
 * @param {object} space  { id, name, slug?, description? }.
 * @param {object} opts   { org_id, visibility?, created_by? }.
 */
export function mapDashboardSpaceToSharedSpaceMetadata(space, opts = {}) {
  if (!space || !space.id) throw new Error('mapDashboardSpaceToSharedSpaceMetadata: space with id is required');
  if (!opts.org_id) throw new Error('mapDashboardSpaceToSharedSpaceMetadata: org_id is required');
  const visibility = opts.visibility || 'org';
  assertVisibility(visibility, 'mapDashboardSpaceToSharedSpaceMetadata');
  const slug = space.slug || String(space.id);
  return {
    org_id: opts.org_id,
    space_type: 'dashboard_space',
    local_space_id: String(space.id),
    local_project_id: null,
    slug,
    name: space.name || slug,
    description: space.description ?? null,
    visibility,
    created_by: opts.created_by ?? null,
  };
}

/**
 * Maps a local `projects` row → the cloud `shared_spaces` shape with
 * space_type='project'. Pure function (no DB/network).
 *
 * @param {object} project  { id, slug, name, description? }.
 * @param {object} opts     { org_id, visibility?, created_by? }.
 */
export function mapProjectToSharedSpaceMetadata(project, opts = {}) {
  if (!project || !project.id) throw new Error('mapProjectToSharedSpaceMetadata: project with id is required');
  if (!opts.org_id) throw new Error('mapProjectToSharedSpaceMetadata: org_id is required');
  const visibility = opts.visibility || 'org';
  assertVisibility(visibility, 'mapProjectToSharedSpaceMetadata');
  return {
    org_id: opts.org_id,
    space_type: 'project',
    local_space_id: String(project.id),
    local_project_id: String(project.id),
    slug: project.slug || String(project.id),
    name: project.name || project.slug || String(project.id),
    description: project.description ?? null,
    visibility,
    created_by: opts.created_by ?? null,
  };
}

/**
 * Maps a local project resource (folder / file / artifact) → the cloud
 * `shared_space_resources` shape. METADATA ONLY — no file body is ever included.
 * Pure function (no DB/network).
 *
 * @param {object} resource  A normalized resource descriptor.
 * @param {object} opts      { org_id, cloud_space_id }.
 */
export function mapProjectResourceMetadata(resource, opts = {}) {
  if (!resource || !resource.local_resource_id) {
    throw new Error('mapProjectResourceMetadata: resource with local_resource_id is required');
  }
  if (!opts.org_id) throw new Error('mapProjectResourceMetadata: org_id is required');
  if (!opts.cloud_space_id) throw new Error('mapProjectResourceMetadata: cloud_space_id is required');
  if (!VALID_RESOURCE_TYPES.includes(resource.resource_type)) {
    throw new Error(`mapProjectResourceMetadata: invalid resource_type "${resource.resource_type}"`);
  }
  return {
    org_id: opts.org_id,
    shared_space_id: opts.cloud_space_id,
    local_resource_id: String(resource.local_resource_id),
    resource_type: resource.resource_type,
    name: resource.name || resource.local_resource_id,
    path: resource.path ?? null,
    mime_type: resource.mime_type ?? null,
    kind: resource.kind ?? null,
    size_bytes: typeof resource.size_bytes === 'number' ? resource.size_bytes : null,
    sha256: resource.sha256 ?? null,
    local_only: Boolean(resource.local_only),
    metadata: resource.metadata ?? null,
  };
}

/**
 * Validates a cloud shared-space metadata object's shape. Throws on violation;
 * returns the same object for chaining. Guarantees no malformed space row is
 * ever pushed to the shared layer.
 */
export function validateSharedSpaceMetadata(meta) {
  if (!meta || typeof meta !== 'object') throw new Error('shared space metadata must be an object');
  const required = ['org_id', 'space_type', 'local_space_id', 'slug', 'name', 'visibility'];
  for (const key of required) {
    if (meta[key] == null || meta[key] === '') throw new Error(`shared space metadata missing required field: ${key}`);
  }
  assertSpaceType(meta.space_type, 'validateSharedSpaceMetadata');
  assertVisibility(meta.visibility, 'validateSharedSpaceMetadata');
  return meta;
}

// ── Local space-publication ledger (no network) ────────────────────────────────

function _readSpaceLedger(localSpaceId, spaceType) {
  const db = getDb();
  return db
    .prepare('SELECT * FROM shared_space_publications WHERE local_space_id = ? AND space_type = ?')
    .get(localSpaceId, spaceType) || null;
}

function _upsertSpaceLedger(fields) {
  const db = getDb();
  const existing = db
    .prepare('SELECT id FROM shared_space_publications WHERE local_space_id = ? AND space_type = ?')
    .get(fields.local_space_id, fields.space_type);
  const id = existing?.id || newId('spub');
  db.prepare(
    `INSERT INTO shared_space_publications
       (id, local_space_id, space_type, org_id, cloud_space_id, slug, name,
        visibility, include_resource_metadata, resource_count, status, last_error,
        published_by, published_at, last_synced_at, unpublished_at)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
     ON CONFLICT(local_space_id, space_type) DO UPDATE SET
       org_id                    = excluded.org_id,
       cloud_space_id            = excluded.cloud_space_id,
       slug                      = excluded.slug,
       name                      = excluded.name,
       visibility                = excluded.visibility,
       include_resource_metadata = excluded.include_resource_metadata,
       resource_count            = excluded.resource_count,
       status                    = excluded.status,
       last_error                = excluded.last_error,
       published_by              = excluded.published_by,
       last_synced_at            = excluded.last_synced_at,
       unpublished_at            = excluded.unpublished_at`
  ).run(
    id,
    fields.local_space_id,
    fields.space_type,
    fields.org_id,
    fields.cloud_space_id ?? null,
    fields.slug ?? null,
    fields.name ?? null,
    fields.visibility,
    fields.include_resource_metadata ? 1 : 0,
    Number(fields.resource_count) || 0,
    fields.status,
    fields.last_error ?? null,
    fields.published_by ?? null,
    fields.published_at ?? nowIso(),
    fields.last_synced_at ?? null,
    fields.unpublished_at ?? null
  );
  return id;
}

/**
 * Returns the local share status for a space/project. Reads ONLY the local
 * ledger (no network), so the default (no row) cheaply identifies a private,
 * never-shared space.
 *
 * @param {string} localSpaceId  projects.id or dashboard_spaces.id.
 * @param {string} [spaceType]   'project' | 'dashboard_space' (default 'dashboard_space').
 */
export function getSharedSpaceStatus(localSpaceId, spaceType = 'dashboard_space') {
  assertSpaceType(spaceType, 'getSharedSpaceStatus');
  const row = _readSpaceLedger(localSpaceId, spaceType);
  if (!row) {
    return {
      shared: false,
      status: 'local',
      space_type: spaceType,
      local_space_id: localSpaceId,
      cloud_space_id: null,
      visibility: null,
      resource_count: 0,
      last_synced_at: null,
    };
  }
  return {
    shared: row.status === 'published',
    status: row.status,
    space_type: row.space_type,
    local_space_id: row.local_space_id,
    cloud_space_id: row.cloud_space_id || null,
    slug: row.slug || null,
    name: row.name || null,
    visibility: row.visibility || null,
    include_resource_metadata: row.include_resource_metadata === 1,
    resource_count: Number(row.resource_count) || 0,
    last_error: row.last_error || null,
    published_by: row.published_by || null,
    published_at: row.published_at || null,
    last_synced_at: row.last_synced_at || null,
  };
}

/** Lists space-publication ledger rows, optionally filtered by status/type. Local read only. */
export function listSharedSpaces({ status, space_type } = {}) {
  const db = getDb();
  const where = [];
  const params = [];
  if (status) { where.push('status = ?'); params.push(status); }
  if (space_type) { where.push('space_type = ?'); params.push(space_type); }
  const sql = `SELECT * FROM shared_space_publications ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY published_at DESC`;
  return db.prepare(sql).all(...params);
}

// ── Project resource collection (metadata only, no bodies) ──────────────────────

/**
 * Collects the folders, files, and artifacts of a local project as metadata-only
 * resource descriptors. NEVER reads file bodies. By default `local_only` files
 * are excluded entirely (the hard content boundary). The unsafe path is gated:
 *   - includeLocalOnly omitted → local_only files are silently excluded.
 *   - includeLocalOnly:true without allowLocalOnlyOverride → throws (blocked).
 *   - includeLocalOnly:true + allowLocalOnlyOverride:true → their *metadata* is
 *     included and flagged local_only:true (body still never uploaded).
 *
 * @returns {{ resources: object[], excludedLocalOnly: number }}
 */
function _collectProjectResources(projectId, { includeLocalOnly = false, allowLocalOnlyOverride = false } = {}) {
  if (includeLocalOnly && !allowLocalOnlyOverride) {
    throw new Error(
      'publishProjectFolder: refusing to share local_only resources. ' +
      'local_only is a hard content boundary; pass allowLocalOnlyOverride:true to force ' +
      'metadata-only sharing of local_only files (bodies are never uploaded).'
    );
  }
  const db = getDb();
  const resources = [];

  const folders = db.prepare('SELECT * FROM disk_folders WHERE project_id = ? ORDER BY path ASC').all(projectId);
  for (const f of folders) {
    resources.push({
      local_resource_id: f.id,
      resource_type: 'folder',
      name: f.name,
      path: f.path,
    });
  }

  let excludedLocalOnly = 0;
  const files = db.prepare('SELECT * FROM disk_files WHERE project_id = ? ORDER BY updated_at DESC').all(projectId);
  for (const file of files) {
    const isLocalOnly = file.local_only === 1;
    if (isLocalOnly && !includeLocalOnly) { excludedLocalOnly += 1; continue; }
    resources.push({
      local_resource_id: file.id,
      resource_type: 'file',
      name: file.name,
      path: file.path ?? null,
      mime_type: file.mime_type ?? null,
      kind: file.kind ?? null,
      size_bytes: typeof file.size_bytes === 'number' ? file.size_bytes : null,
      sha256: file.sha256 ?? null,
      local_only: isLocalOnly,
    });
  }

  // Artifacts are surfaced as metadata-only resource rows so teammates can see
  // what exists. The body is never copied here — full payload sharing goes
  // through publishArtifact() and lands in shared_artifacts.
  const artifacts = db.prepare('SELECT id, title, kind FROM artifacts WHERE project_id = ? ORDER BY created_at DESC').all(projectId);
  for (const a of artifacts) {
    resources.push({
      local_resource_id: a.id,
      resource_type: 'artifact',
      name: a.title || a.id,
      kind: a.kind ?? null,
    });
  }

  return { resources, excludedLocalOnly };
}

// ── Publish / unpublish spaces ──────────────────────────────────────────────────

async function _publishSpace(meta, {
  session,
  options,
  resources = null,
  include_resource_metadata = false,
  excludedLocalOnly = 0,
  target_type,
  target_id,
}) {
  validateSharedSpaceMetadata(meta);
  const { org_id, space_type, local_space_id } = meta;

  if (!isSharedCloudReady(options)) {
    throw new Error(
      `publish ${space_type}: shared cloud is not configured. Set SUPABASE_URL and ` +
      'SUPABASE_SERVICE_ROLE_KEY (vault or env) before sharing.'
    );
  }

  const admin = getSharedCloudAdmin(options);

  let cloudSpace = null;
  try {
    const returned = await admin.upsert('shared_spaces', meta, { onConflict: 'org_id,space_type,local_space_id' });
    cloudSpace = Array.isArray(returned) ? returned[0] : returned;
  } catch (err) {
    _upsertSpaceLedger({
      local_space_id, space_type, org_id,
      slug: meta.slug, name: meta.name, visibility: meta.visibility,
      include_resource_metadata, status: 'error', last_error: err.message,
      published_by: session?.user_id,
    });
    appendComplianceAudit({
      user_id: session?.user_id, org_id,
      action: `${space_type}.shared.publish.error`,
      action_target_id: target_id, action_target_type: target_type,
      status: 'error',
      summary: `Failed to share ${space_type} ${local_space_id} to shared cloud`,
      meta: { visibility: meta.visibility, error: err.message },
    });
    throw err;
  }

  const cloud_space_id = cloudSpace?.id ?? null;

  // Project folders: replace the resource metadata index (metadata only).
  let resourceCount = 0;
  if (include_resource_metadata && Array.isArray(resources) && cloud_space_id) {
    // Clean removed resources first so the cloud index mirrors the local state.
    await admin.delete('shared_space_resources', { filters: { org_id, shared_space_id: cloud_space_id } });
    if (resources.length) {
      const rows = resources.map((r) => mapProjectResourceMetadata(r, { org_id, cloud_space_id }));
      await admin.upsert('shared_space_resources', rows, { onConflict: 'shared_space_id,resource_type,local_resource_id' });
      resourceCount = rows.length;
    }
  }

  _upsertSpaceLedger({
    local_space_id, space_type, org_id,
    cloud_space_id,
    slug: meta.slug, name: meta.name, visibility: meta.visibility,
    include_resource_metadata, resource_count: resourceCount,
    status: 'published',
    published_by: session?.user_id,
    published_at: nowIso(),
    last_synced_at: nowIso(),
  });

  appendComplianceAudit({
    user_id: session?.user_id, org_id,
    action: `${space_type}.shared.published`,
    action_target_id: target_id, action_target_type: target_type,
    status: 'success',
    summary: `Shared ${space_type} ${local_space_id} to shared cloud (visibility: ${meta.visibility})`,
    meta: {
      visibility: meta.visibility,
      cloud_space_id,
      resource_count: resourceCount,
      excluded_local_only: excludedLocalOnly,
    },
  });

  return getSharedSpaceStatus(local_space_id, space_type);
}

/**
 * Explicitly shares a local dashboard/UI space into the org as a cloud
 * shared_space (space_type='dashboard_space'). Writes the local ledger AND the
 * cloud row. This is the ONLY path by which a dashboard space becomes visible to
 * teammates, and it is always explicit + audited.
 *
 * @param {object} args
 * @param {string} args.space_id     Local dashboard_spaces.id. Required.
 * @param {object} args.session      { user_id, org_id, ... }. Required for org/audit.
 * @param {string} [args.org_id]     Override org (defaults to session.org_id).
 * @param {string} [args.visibility] 'org'|'team'|'private' (default 'org').
 * @param {object} [options]         { agent_id, project_id } for credential scope.
 */
export async function publishDashboardSpace(args = {}, options = {}) {
  assertServerOnly();
  const { space_id, session, visibility = 'org' } = args;
  if (!space_id) throw new Error('publishDashboardSpace: space_id is required');
  const org_id = args.org_id || session?.org_id;
  if (!org_id) throw new Error('publishDashboardSpace: org_id (or session.org_id) is required');
  assertVisibility(visibility, 'publishDashboardSpace');

  const db = getDb();
  const space = db.prepare('SELECT * FROM dashboard_spaces WHERE id = ?').get(String(space_id));
  if (!space) throw new Error(`publishDashboardSpace: dashboard space ${space_id} not found locally`);

  const meta = mapDashboardSpaceToSharedSpaceMetadata(space, {
    org_id, visibility, created_by: args.published_by ?? null,
  });

  // Index the space's artifacts (metadata only — bodies are never uploaded), so
  // teammates see what the shared space contains. Re-publishing re-syncs new or
  // moved-in artifacts.
  const resources = _collectDashboardSpaceResources(String(space_id));

  return _publishSpace(meta, {
    session, options,
    resources,
    include_resource_metadata: true,
    target_type: 'dashboard_space',
    target_id: String(space_id),
  });
}

/**
 * Collects metadata-only resource descriptors for a dashboard space's artifacts.
 * Mirrors the artifact branch of _collectProjectResources. Bodies are never
 * uploaded — full payload sharing goes through publishArtifact().
 */
function _collectDashboardSpaceResources(spaceId) {
  const db = getDb();
  const artifacts = db
    .prepare('SELECT id, title, kind FROM artifacts WHERE space_id = ? ORDER BY created_at DESC')
    .all(String(spaceId));
  return artifacts.map((a) => ({
    local_resource_id: a.id,
    resource_type: 'artifact',
    name: a.title || a.id,
    kind: a.kind ?? null,
  }));
}

/**
 * Explicitly shares a local project folder into the org as a cloud shared_space
 * (space_type='project'), and (by default) publishes a metadata-only index of
 * its folders/files/artifacts into shared_space_resources. File BODIES are never
 * uploaded; `local_only` files are excluded unless an explicit double opt-in is
 * given. Writes the local ledger AND the cloud rows.
 *
 * @param {object} args
 * @param {string} [args.project_id]              Local projects.id.
 * @param {string} [args.slug]                    Local projects.slug (alt to project_id).
 * @param {object} args.session                   { user_id, org_id, ... }. Required.
 * @param {string} [args.org_id]                  Override org (defaults to session.org_id).
 * @param {string} [args.visibility]              'org'|'team'|'private' (default 'org').
 * @param {boolean} [args.includeResourceMetadata=true]  Publish folder/file/artifact metadata.
 * @param {boolean} [args.includeLocalOnly=false] Unsafe: include local_only files' metadata.
 * @param {boolean} [args.allowLocalOnlyOverride=false]  Required acknowledgement for includeLocalOnly.
 * @param {object} [options]                      { agent_id, project_id } for credential scope.
 */
export async function publishProjectFolder(args = {}, options = {}) {
  assertServerOnly();
  const { session, visibility = 'org', includeResourceMetadata = true } = args;
  const idOrSlug = args.project_id || args.slug;
  if (!idOrSlug) throw new Error('publishProjectFolder: project_id or slug is required');
  const org_id = args.org_id || session?.org_id;
  if (!org_id) throw new Error('publishProjectFolder: org_id (or session.org_id) is required');
  assertVisibility(visibility, 'publishProjectFolder');

  const project = getProject(idOrSlug);
  if (!project) throw new Error(`publishProjectFolder: project ${idOrSlug} not found locally`);

  const meta = mapProjectToSharedSpaceMetadata(project, {
    org_id, visibility, created_by: args.published_by ?? null,
  });

  let resources = null;
  let excludedLocalOnly = 0;
  if (includeResourceMetadata) {
    const collected = _collectProjectResources(project.id, {
      includeLocalOnly: args.includeLocalOnly === true,
      allowLocalOnlyOverride: args.allowLocalOnlyOverride === true,
    });
    resources = collected.resources;
    excludedLocalOnly = collected.excludedLocalOnly;
  }

  return _publishSpace(meta, {
    session, options,
    resources,
    include_resource_metadata: includeResourceMetadata,
    excludedLocalOnly,
    target_type: 'project',
    target_id: project.id,
  });
}

/**
 * Explicitly unshares a space/project from the shared layer: removes the cloud
 * `shared_spaces` row (and its `shared_space_resources` via FK cascade) and marks
 * the local ledger 'unpublished'. The local space/project is never touched — it
 * remains the source of truth.
 *
 * @param {object} args
 * @param {string} args.local_space_id  projects.id or dashboard_spaces.id. Required.
 * @param {object} args.session         { user_id, org_id, ... }. Required.
 * @param {string} [args.org_id]        Override org (defaults to session.org_id).
 * @param {string} [args.space_type]    'project'|'dashboard_space' (default 'dashboard_space').
 * @param {object} [options]            { agent_id, project_id } for credential scope.
 */
export async function unpublishSpace(args = {}, options = {}) {
  assertServerOnly();
  const { local_space_id, session, space_type = 'dashboard_space' } = args;
  if (!local_space_id) throw new Error('unpublishSpace: local_space_id is required');
  const org_id = args.org_id || session?.org_id;
  if (!org_id) throw new Error('unpublishSpace: org_id (or session.org_id) is required');
  assertSpaceType(space_type, 'unpublishSpace');

  if (!isSharedCloudReady(options)) {
    throw new Error('unpublishSpace: shared cloud is not configured.');
  }

  const admin = getSharedCloudAdmin(options);

  // Resource rows cascade on shared_spaces delete, but we also delete them
  // explicitly first in case the cloud row was already gone (idempotent cleanup).
  const ledger = _readSpaceLedger(local_space_id, space_type);
  if (ledger?.cloud_space_id) {
    await admin.delete('shared_space_resources', { filters: { org_id, shared_space_id: ledger.cloud_space_id } });
  }
  await admin.delete('shared_spaces', { filters: { org_id, space_type, local_space_id } });

  _upsertSpaceLedger({
    local_space_id, space_type, org_id,
    visibility: ledger?.visibility || 'private',
    status: 'unpublished',
    resource_count: 0,
    published_by: session?.user_id,
    unpublished_at: nowIso(),
    last_synced_at: nowIso(),
  });

  appendComplianceAudit({
    user_id: session?.user_id, org_id,
    action: `${space_type}.shared.unpublished`,
    action_target_id: local_space_id, action_target_type: space_type,
    status: 'success',
    summary: `Unshared ${space_type} ${local_space_id} from shared cloud`,
  });

  return getSharedSpaceStatus(local_space_id, space_type);
}

// ════════════════════════════════════════════════════════════════════════════
//  ORG-SHARED BRAND PROFILE — publish / adopt / unpublish
//
//  The brand profile is a singleton per install (the local config.brand_profile
//  row). Local-first by default. An owner/admin can PUBLISH it so one canonical
//  row lands in cloud `shared_brand_profiles`; any teammate can then ADOPT it,
//  which writes the profile into their own local config. Same guarantees as
//  artifacts/spaces: nothing leaves implicitly, every action is explicit +
//  audited, and a brand carries NO secrets so the full payload is safe to store.
//
//  Adopt writes LOCAL state (config.brand_profile), which is owned by lib/brand.
//  To avoid a module cycle, this file only READS the cloud (fetchOrgBrandProfile)
//  and stamps the local ledger (markBrandAdopted); the API route performs the
//  saveBrandProfile() local write between the two.
// ════════════════════════════════════════════════════════════════════════════

/**
 * Maps a local brand profile → the cloud `shared_brand_profiles` row shape.
 * Pure function (no DB/network). The whole profile object is stored (jsonb);
 * brand identity has no secret material so the full payload is safe.
 */
export function mapBrandToCloud(profile, opts = {}) {
  if (!profile || typeof profile !== 'object') throw new Error('mapBrandToCloud: a profile object is required');
  if (!opts.org_id) throw new Error('mapBrandToCloud: org_id is required');
  return {
    org_id: opts.org_id,
    company_name: String(profile.company_name || 'Brand').slice(0, 200),
    profile_json: profile,
    logo_data_url: profile.logo_data_url ?? null,
    content_hash: sha256Hex(JSON.stringify(profile)),
    published_by: opts.published_by ?? null,
    updated_at: nowIso(),
  };
}

/** Validates a cloud brand-metadata object's shape. Throws on violation; returns it for chaining. */
export function validateSharedBrandMetadata(meta) {
  if (!meta || typeof meta !== 'object') throw new Error('shared brand metadata must be an object');
  for (const key of ['org_id', 'company_name', 'profile_json', 'content_hash']) {
    if (meta[key] == null || meta[key] === '') throw new Error(`shared brand metadata missing required field: ${key}`);
  }
  if (typeof meta.profile_json !== 'object') throw new Error('shared brand metadata profile_json must be an object');
  if (!/^[0-9a-f]{64}$/.test(meta.content_hash)) {
    throw new Error('shared brand metadata content_hash must be a sha256 hex digest');
  }
  return meta;
}

// ── Local brand-publication ledger (no network) ────────────────────────────────

function _readBrandLedger(orgId) {
  const db = getDb();
  return db.prepare('SELECT * FROM brand_publications WHERE org_id = ?').get(orgId) || null;
}

function _upsertBrandLedger(fields) {
  const db = getDb();
  const existing = _readBrandLedger(fields.org_id);
  const id = existing?.id || newId('bpub');
  db.prepare(
    `INSERT INTO brand_publications
       (id, org_id, source, content_hash, status, last_error,
        published_by, published_at, adopted_at, last_synced_at, unpublished_at)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
     ON CONFLICT(org_id) DO UPDATE SET
       source         = excluded.source,
       content_hash   = excluded.content_hash,
       status         = excluded.status,
       last_error     = excluded.last_error,
       published_by   = excluded.published_by,
       published_at   = COALESCE(excluded.published_at, brand_publications.published_at),
       adopted_at     = COALESCE(excluded.adopted_at, brand_publications.adopted_at),
       last_synced_at = excluded.last_synced_at,
       unpublished_at = excluded.unpublished_at`
  ).run(
    id,
    fields.org_id,
    fields.source || 'local',
    fields.content_hash ?? null,
    fields.status,
    fields.last_error ?? null,
    fields.published_by ?? null,
    fields.published_at ?? null,
    fields.adopted_at ?? null,
    fields.last_synced_at ?? null,
    fields.unpublished_at ?? null
  );
  return id;
}

/**
 * Returns the org-brand share status. Reads ONLY the local ledger (no network),
 * so the default (no row) cheaply identifies a purely local brand.
 */
export function getBrandPublishStatus(orgId) {
  const row = _readBrandLedger(orgId);
  if (!row) {
    return { published: false, status: 'local', source: 'local', content_hash: null, last_synced_at: null };
  }
  return {
    published: row.status === 'published',
    status: row.status,
    source: row.source || 'local',
    content_hash: row.content_hash || null,
    published_by: row.published_by || null,
    published_at: row.published_at || null,
    adopted_at: row.adopted_at || null,
    last_synced_at: row.last_synced_at || null,
    last_error: row.last_error || null,
  };
}

/**
 * Explicitly publishes the local brand profile to the org's cloud row. Owner →
 * service-role admin client; the API route gates this to owner/admin (brand is
 * admin-managed; member writes are blocked by RLS anyway). Always explicit + audited.
 *
 * @param {object} args      { session, org_id?, profile, published_by? }.
 * @param {object} [options] { agent_id, project_id } for credential scope.
 */
export async function publishBrandProfile(args = {}, options = {}) {
  assertServerOnly();
  const { session, profile } = args;
  const org_id = args.org_id || session?.org_id;
  if (!org_id) throw new Error('publishBrandProfile: org_id (or session.org_id) is required');
  if (!profile || typeof profile !== 'object') throw new Error('publishBrandProfile: a profile is required');

  const { client } = _resolvePublishClient(args, options);
  if (!client) {
    throw new Error(
      'publishBrandProfile: shared cloud is not configured. Owner instances need ' +
      'SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY; member instances need ' +
      'SUPABASE_URL + SUPABASE_ANON_KEY and a signed-in session.'
    );
  }

  const meta = mapBrandToCloud(profile, { org_id, published_by: args.published_by });
  validateSharedBrandMetadata(meta);

  try {
    await client.upsert('shared_brand_profiles', meta, { onConflict: 'org_id' });
  } catch (err) {
    _upsertBrandLedger({
      org_id, source: 'local', content_hash: meta.content_hash,
      status: 'error', last_error: err.message, published_by: session?.user_id,
    });
    appendComplianceAudit({
      user_id: session?.user_id, org_id,
      action: 'brand.shared.publish.error',
      action_target_id: org_id, action_target_type: 'brand',
      status: 'error',
      summary: 'Failed to publish org brand to shared cloud',
      meta: { error: err.message },
    });
    throw err;
  }

  _upsertBrandLedger({
    org_id, source: 'local', content_hash: meta.content_hash,
    status: 'published', published_by: session?.user_id,
    published_at: nowIso(), last_synced_at: nowIso(),
  });
  appendComplianceAudit({
    user_id: session?.user_id, org_id,
    action: 'brand.shared.published',
    action_target_id: org_id, action_target_type: 'brand',
    status: 'success',
    summary: `Published org brand "${meta.company_name}" to shared cloud`,
  });
  return getBrandPublishStatus(org_id);
}

/**
 * Reads the org's shared brand from the cloud (network). Any member may read it
 * (so they can adopt). Returns the normalized profile or null when none exists.
 * Does NOT write local state — the route saves it via lib/brand then calls
 * markBrandAdopted() to stamp the ledger.
 */
export async function fetchOrgBrandProfile(args = {}, options = {}) {
  assertServerOnly();
  const { session } = args;
  const org_id = args.org_id || session?.org_id;
  if (!org_id) throw new Error('fetchOrgBrandProfile: org_id (or session.org_id) is required');

  const { client } = _resolvePublishClient(args, options);
  if (!client) throw new Error('fetchOrgBrandProfile: shared cloud is not configured.');

  const rows = await client.select('shared_brand_profiles', { filters: { org_id }, limit: 1 });
  const row = Array.isArray(rows) ? rows[0] : rows;
  if (!row) return null;
  return {
    org_id,
    company_name: row.company_name || 'Brand',
    profile: row.profile_json || null,
    logo_data_url: row.logo_data_url || null,
    content_hash: row.content_hash || null,
    updated_at: row.updated_at || null,
    published_by: row.published_by || null,
  };
}

/**
 * Stamps the local ledger to record that this install adopted the org brand.
 * Local-only (no network). Call AFTER the route has saved the profile locally.
 */
export function markBrandAdopted(orgId, { content_hash, user_id } = {}) {
  if (!orgId) throw new Error('markBrandAdopted: orgId is required');
  _upsertBrandLedger({
    org_id: orgId, source: 'org', content_hash: content_hash ?? null,
    status: 'adopted', published_by: user_id ?? null,
    adopted_at: nowIso(), last_synced_at: nowIso(),
  });
  appendComplianceAudit({
    user_id, org_id: orgId,
    action: 'brand.shared.adopted',
    action_target_id: orgId, action_target_type: 'brand',
    status: 'success',
    summary: 'Adopted org brand from shared cloud',
  });
  return getBrandPublishStatus(orgId);
}

/**
 * Explicitly stops sharing the org brand: deletes the cloud row and marks the
 * local ledger 'unpublished'. The local brand profile itself is never touched.
 */
export async function unpublishBrandProfile(args = {}, options = {}) {
  assertServerOnly();
  const { session } = args;
  const org_id = args.org_id || session?.org_id;
  if (!org_id) throw new Error('unpublishBrandProfile: org_id (or session.org_id) is required');

  const { client } = _resolvePublishClient(args, options);
  if (!client) throw new Error('unpublishBrandProfile: shared cloud is not configured.');
  await client.delete('shared_brand_profiles', { filters: { org_id } });

  _upsertBrandLedger({
    org_id, source: 'local', status: 'unpublished',
    published_by: session?.user_id, unpublished_at: nowIso(), last_synced_at: nowIso(),
  });
  appendComplianceAudit({
    user_id: session?.user_id, org_id,
    action: 'brand.shared.unpublished',
    action_target_id: org_id, action_target_type: 'brand',
    status: 'success',
    summary: 'Stopped sharing org brand from shared cloud',
  });
  return getBrandPublishStatus(org_id);
}

// ════════════════════════════════════════════════════════════════════════════
//  ORG INVITE REDEMPTION via cloud RPC (member's own JWT — never service key)
//
//  On a MEMBER machine the org_invites row lives only in the cloud (the owner
//  created it there). The member redeems through a SECURITY DEFINER RPC under
//  their OWN JWT (anon key as apikey + Bearer access token). After the cloud
//  insert succeeds we mirror the resulting membership into local SQLite so the
//  local requireFreshMembership fallback and the UI keep working offline.
//
//  These call paths require a live, migrated Supabase project to exercise and
//  are integration-verified separately; the local-first redeem/claim in
//  lib/invites.js is the unit-tested source of truth for owner machines.
// ════════════════════════════════════════════════════════════════════════════

function _ensureLocalOrgMembership(orgId, userId, role, orgName) {
  const db = getDb();
  const exists = db.prepare('SELECT id FROM organizations WHERE id = ?').get(orgId);
  if (!exists) {
    const now = nowIso();
    db.prepare(
      `INSERT INTO organizations (id, slug, name, plan, created_at, updated_at)
       VALUES (?, ?, ?, 'free', ?, ?) ON CONFLICT(id) DO NOTHING`
    ).run(orgId, `org-${String(orgId).slice(-8)}`, orgName || String(orgId), now, now);
  }
  const m = db.prepare('SELECT id FROM org_memberships WHERE org_id = ? AND user_id = ?').get(orgId, userId);
  if (!m) {
    db.prepare(
      `INSERT INTO org_memberships (id, org_id, user_id, role, accepted_at) VALUES (?, ?, ?, ?, ?)`
    ).run(newId('mem'), orgId, userId, role, nowIso());
  }
}

/**
 * Claims pending EMAIL invites for the signed-in member via the cloud
 * claim_email_invites() RPC, mirroring each granted membership locally. Returns
 * [] (never throws) when the cloud is not configured or the RPC is unavailable,
 * so getSession can fall back to the local table / bootstrap.
 *
 * @param {object} session  Must carry supabase_access_token + user_id.
 * @returns {Promise<Array<{ org_id, role }>>}
 */
export async function claimEmailInvitesViaCloud(session, options = {}) {
  assertServerOnly();
  const token = session?.supabase_access_token;
  if (!token || !isSharedCloudUserReady(options)) return [];
  let rows;
  try {
    rows = await getSharedCloudUserClient(token, options).rpc('claim_email_invites');
  } catch {
    return []; // RPC absent / project not migrated / network — caller falls back.
  }
  const claimed = Array.isArray(rows) ? rows : [];
  const out = [];
  for (const r of claimed) {
    const role = r.granted_role || r.role;
    if (r.org_id) {
      _ensureLocalOrgMembership(r.org_id, session.user_id, role);
      out.push({ org_id: r.org_id, role });
    }
  }
  return out;
}

/**
 * Redeems a shareable-link token for the signed-in member via the cloud
 * redeem_invite_token() RPC (under the member's own JWT), mirroring the granted
 * membership locally. Throws on an invalid/expired/used token.
 *
 * @param {string} token
 * @param {object} session  Must carry supabase_access_token + user_id.
 * @returns {Promise<{ org_id, role }>}
 */
export async function redeemInviteViaCloud(token, session, options = {}) {
  assertServerOnly();
  const jwt = session?.supabase_access_token;
  if (!jwt) throw new Error('redeemInviteViaCloud: no session access token');
  if (!isSharedCloudUserReady(options)) throw new Error('redeemInviteViaCloud: shared cloud (user) not configured');
  const out = await getSharedCloudUserClient(jwt, options).rpc('redeem_invite_token', { p_token: token });
  const row = Array.isArray(out) ? out[0] : out;
  if (!row || !row.org_id) {
    throw Object.assign(new Error('invite is no longer valid (expired, revoked, or fully used)'), { code: 'invite_invalid' });
  }
  const role = row.granted_role || row.role;
  _ensureLocalOrgMembership(row.org_id, session.user_id, role);
  return { org_id: row.org_id, role };
}

/**
 * Mirrors the signed-in user's EXISTING cloud org memberships into local SQLite.
 *
 * Runs at login BEFORE the local first-org bootstrap. In supabase mode the cloud
 * `org_memberships` table is authoritative for who belongs to which org. A fresh
 * local install (a new $MADARX_HOME, a wiped data dir, a second machine) has no
 * local membership rows, which would otherwise drive `_bootstrapFirstOrg` to
 * invent a brand-new phantom org. That phantom org has no cloud row, so the first
 * `requireFreshMembership` live-check returns 0 rows and locks the user out with
 * `membership_revoked`. Mirroring the real cloud memberships down first keeps the
 * local session org in agreement with the cloud the live-check validates against.
 *
 * Prefers the service-role admin client (owner/server). Falls back to the
 * member's own JWT — RLS lets a member SELECT their own membership rows — so a
 * member machine without the service key still reconciles. Returns [] and never
 * throws when the cloud is unconfigured/unreachable, so getSession can fall back
 * to the local table / invite claim / bootstrap.
 *
 * NOTE: additive only. Like `_ensureLocalOrgMembership`, this inserts missing
 * local rows; it never deletes a local membership the cloud no longer has. That
 * keeps the mirror safe to run on every no-local-membership login. Revocation of
 * an existing membership is handled separately by `requireFreshMembership`.
 *
 * @param {object} session  Must carry user_id; supabase_access_token for the member path.
 * @returns {Promise<Array<{ org_id, role }>>}
 */
export async function mirrorCloudMembershipsForUser(session, options = {}) {
  assertServerOnly();
  const userId = session?.user_id;
  if (!userId) return [];

  const accessToken = session?.supabase_access_token || null;
  const kind = selectCloudClientKind({
    serviceReady: isSharedCloudReady(options),
    userReady: isSharedCloudUserReady(options),
    accessToken,
  });
  if (!kind) return [];

  let client;
  try {
    client = kind === 'admin'
      ? getSharedCloudAdmin(options)
      : getSharedCloudUserClient(accessToken, options);
  } catch {
    return [];
  }

  let rows;
  try {
    rows = await client.select('org_memberships', {
      filters: { user_id: userId },
      select: 'org_id,role',
      limit: 100,
    });
  } catch {
    return []; // table absent / project not migrated / network — caller falls back.
  }
  if (!Array.isArray(rows) || rows.length === 0) return [];

  // Resolve org names so the mirrored local `organizations` row isn't an opaque
  // id. Best-effort: on failure we fall back to the id as the local name.
  const orgIds = [...new Set(rows.map((r) => r.org_id).filter(Boolean))];
  const nameById = new Map();
  try {
    if (orgIds.length) {
      const orgRows = await client.select('organizations', {
        filters: { id: `in.(${orgIds.join(',')})` },
        select: 'id,name',
        limit: 1000,
      });
      for (const o of Array.isArray(orgRows) ? orgRows : []) {
        if (o.id) nameById.set(o.id, o.name);
      }
    }
  } catch {
    /* name lookup is best-effort */
  }

  const out = [];
  for (const r of rows) {
    if (!r.org_id) continue;
    _ensureLocalOrgMembership(r.org_id, userId, r.role, nameById.get(r.org_id));
    out.push({ org_id: r.org_id, role: r.role });
  }
  return out;
}

// ════════════════════════════════════════════════════════════════════════════
//  SPACE / PROJECT ACL — per-principal access grants
//
//  The space-level mirror of the artifact ACL. A grant gives a principal
//  (a user, a team, or the whole org) a permission on a shared space/project:
//    read   browse the resource index + open published artifact bodies
//    write  read + re-sync / add resources
//    admin  write + manage this space's ACL
//
//  These rows AUGMENT the coarse `shared_spaces.visibility`:
//    - visibility='private' + user grants → "only these people"
//    - visibility='org'                   → baseline read for every member;
//                                            grants only elevate permission.
//
//  'user' and 'org' principals are live. 'team' is reserved (no teams model yet)
//  and is rejected by the writer functions to avoid dead grants.
//
//  Grants live in the cloud (`shared_space_acl`) because space sharing is a
//  cloud-coordination concern; the space must already be published.
// ════════════════════════════════════════════════════════════════════════════

const VALID_ACL_PRINCIPAL_TYPES = Object.freeze(['user', 'team', 'org']);
// 'team' is in the cloud CHECK domain for forward-compat but not yet writable.
const WRITABLE_ACL_PRINCIPAL_TYPES = Object.freeze(['user', 'org']);
const VALID_ACL_PERMISSIONS = Object.freeze(['read', 'write', 'admin']);
const ACL_PERMISSION_RANK = Object.freeze({ read: 1, write: 2, admin: 3 });

function assertPrincipalType(principalType, where, { writableOnly = false } = {}) {
  const allowed = writableOnly ? WRITABLE_ACL_PRINCIPAL_TYPES : VALID_ACL_PRINCIPAL_TYPES;
  if (!allowed.includes(principalType)) {
    throw new Error(
      `${where}: invalid principal_type "${principalType}" (expected one of ${allowed.join('|')})`
    );
  }
}

function assertPermission(permission, where) {
  if (!VALID_ACL_PERMISSIONS.includes(permission)) {
    throw new Error(
      `${where}: invalid permission "${permission}" (expected one of ${VALID_ACL_PERMISSIONS.join('|')})`
    );
  }
}

/**
 * Normalises a principal_id. 'user'/'team' require an explicit id; 'org' defaults
 * to the org_id (so the unique key stays non-null and the row is org-scoped).
 */
function _normalizePrincipalId(principalType, principalId, orgId, where) {
  if (principalType === 'org') return principalId || orgId;
  if (!principalId) throw new Error(`${where}: principal_id is required for principal_type "${principalType}"`);
  return principalId;
}

/**
 * Resolves the cloud `shared_spaces.id` for a published local space. Reads the
 * local ledger only (no network). Throws if the space has never been published —
 * you cannot grant access to something that does not exist in the shared cloud.
 */
function _requireCloudSpaceId(localSpaceId, spaceType, where) {
  const ledger = _readSpaceLedger(localSpaceId, spaceType);
  if (!ledger || ledger.status !== 'published' || !ledger.cloud_space_id) {
    throw new Error(`${where}: ${spaceType} ${localSpaceId} is not published to the shared cloud`);
  }
  return ledger.cloud_space_id;
}

/**
 * PURE decision function for read-time enforcement. Given a space's visibility
 * and its ACL grants, returns the effective permission for (userId, orgId), or
 * null when the principal has no access. Exported for unit testing without any
 * network/DB dependency.
 *
 * Rules (highest wins):
 *   - visibility='org' grants a baseline 'read' to any org member.
 *   - a matching 'user' grant applies its permission.
 *   - an 'org' principal grant (principal_id null or === orgId) applies its permission.
 *   - 'team' grants are ignored until the teams model ships.
 *
 * @param {object} args
 * @param {string} [args.visibility]  'org'|'team'|'private'.
 * @param {Array}  [args.grants]      [{ principal_type, principal_id, permission }].
 * @param {string} [args.userId]      Cloud user id of the caller.
 * @param {string} [args.orgId]       Org id of the space.
 * @returns {('read'|'write'|'admin'|null)}
 */
export function decideSpacePermission({ visibility, grants = [], userId, orgId } = {}) {
  let best = null;
  const consider = (perm) => {
    if (!perm || !ACL_PERMISSION_RANK[perm]) return;
    if (!best || ACL_PERMISSION_RANK[perm] > ACL_PERMISSION_RANK[best]) best = perm;
  };

  if (visibility === 'org') consider('read');

  for (const g of grants) {
    if (!g) continue;
    if (g.principal_type === 'user' && userId && g.principal_id === userId) {
      consider(g.permission);
    } else if (g.principal_type === 'org' && (g.principal_id == null || g.principal_id === orgId)) {
      consider(g.permission);
    }
    // 'team' grants are intentionally ignored until the teams model ships.
  }

  return best;
}

/**
 * Grants (or updates) a principal's permission on a published shared space.
 * Service-role write; the caller must already be authorized by the API layer.
 *
 * @param {object} args
 * @param {string} args.local_space_id   projects.id or dashboard_spaces.id. Required.
 * @param {string} args.principal_type   'user' | 'org' ('team' rejected for now). Required.
 * @param {string} [args.principal_id]   Required for 'user'; defaults to org_id for 'org'.
 * @param {string} [args.permission]     'read'|'write'|'admin' (default 'read').
 * @param {object} args.session          { user_id, org_id, ... }. Required.
 * @param {string} [args.org_id]         Override org (defaults to session.org_id).
 * @param {string} [args.space_type]     'project'|'dashboard_space' (default 'project').
 * @param {object} [options]             { agent_id, project_id } for credential scope.
 */
export async function grantSpaceAccess(args = {}, options = {}) {
  assertServerOnly();
  const {
    local_space_id, session,
    principal_type,
    permission = 'read',
    space_type = 'project',
  } = args;
  if (!local_space_id) throw new Error('grantSpaceAccess: local_space_id is required');
  const org_id = args.org_id || session?.org_id;
  if (!org_id) throw new Error('grantSpaceAccess: org_id (or session.org_id) is required');
  assertSpaceType(space_type, 'grantSpaceAccess');
  assertPrincipalType(principal_type, 'grantSpaceAccess', { writableOnly: true });
  assertPermission(permission, 'grantSpaceAccess');
  const principal_id = _normalizePrincipalId(principal_type, args.principal_id, org_id, 'grantSpaceAccess');

  if (!isSharedCloudReady(options)) {
    throw new Error('grantSpaceAccess: shared cloud is not configured.');
  }

  const cloud_space_id = _requireCloudSpaceId(local_space_id, space_type, 'grantSpaceAccess');
  const admin = getSharedCloudAdmin(options);

  let row;
  try {
    const returned = await admin.upsert(
      'shared_space_acl',
      {
        shared_space_id: cloud_space_id,
        org_id,
        principal_type,
        principal_id,
        permission,
        created_by: session?.user_id ?? null,
        updated_at: nowIso(),
      },
      { onConflict: 'shared_space_id,principal_type,principal_id' }
    );
    row = Array.isArray(returned) ? returned[0] : returned;
  } catch (err) {
    appendComplianceAudit({
      user_id: session?.user_id, org_id,
      action: `${space_type}.shared.acl.grant.error`,
      action_target_id: local_space_id, action_target_type: space_type,
      status: 'error',
      summary: `Failed to grant ${permission} on ${space_type} ${local_space_id} to ${principal_type}:${principal_id}`,
      meta: { principal_type, principal_id, permission, error: err.message },
    });
    throw err;
  }

  appendComplianceAudit({
    user_id: session?.user_id, org_id,
    action: `${space_type}.shared.acl.granted`,
    action_target_id: local_space_id, action_target_type: space_type,
    status: 'success',
    summary: `Granted ${permission} on ${space_type} ${local_space_id} to ${principal_type}:${principal_id}`,
    meta: { principal_type, principal_id, permission, cloud_space_id },
  });

  return row;
}

/**
 * Revokes a principal's grant on a published shared space. Idempotent.
 *
 * @param {object} args  Same identity shape as grantSpaceAccess (no permission).
 * @param {object} [options]
 */
export async function revokeSpaceAccess(args = {}, options = {}) {
  assertServerOnly();
  const { local_space_id, session, principal_type, space_type = 'project' } = args;
  if (!local_space_id) throw new Error('revokeSpaceAccess: local_space_id is required');
  const org_id = args.org_id || session?.org_id;
  if (!org_id) throw new Error('revokeSpaceAccess: org_id (or session.org_id) is required');
  assertSpaceType(space_type, 'revokeSpaceAccess');
  assertPrincipalType(principal_type, 'revokeSpaceAccess', { writableOnly: true });
  const principal_id = _normalizePrincipalId(principal_type, args.principal_id, org_id, 'revokeSpaceAccess');

  if (!isSharedCloudReady(options)) {
    throw new Error('revokeSpaceAccess: shared cloud is not configured.');
  }

  const cloud_space_id = _requireCloudSpaceId(local_space_id, space_type, 'revokeSpaceAccess');
  const admin = getSharedCloudAdmin(options);

  const removed = await admin.delete('shared_space_acl', {
    filters: { shared_space_id: cloud_space_id, principal_type, principal_id },
  });

  appendComplianceAudit({
    user_id: session?.user_id, org_id,
    action: `${space_type}.shared.acl.revoked`,
    action_target_id: local_space_id, action_target_type: space_type,
    status: 'success',
    summary: `Revoked ${space_type} ${local_space_id} grant from ${principal_type}:${principal_id}`,
    meta: { principal_type, principal_id, cloud_space_id },
  });

  return { revoked: Array.isArray(removed) ? removed.length : 0, principal_type, principal_id };
}

/**
 * Lists the ACL grants for a published shared space (live cloud read).
 * Returns [] when the space is not published.
 *
 * @param {object} args  { local_space_id, space_type?, org_id?, session? }.
 * @param {object} [options]
 */
export async function listSpaceAccess(args = {}, options = {}) {
  assertServerOnly();
  const { local_space_id, session, space_type = 'project' } = args;
  if (!local_space_id) throw new Error('listSpaceAccess: local_space_id is required');
  const org_id = args.org_id || session?.org_id;
  if (!org_id) throw new Error('listSpaceAccess: org_id (or session.org_id) is required');
  assertSpaceType(space_type, 'listSpaceAccess');

  const ledger = _readSpaceLedger(local_space_id, space_type);
  if (!ledger || ledger.status !== 'published' || !ledger.cloud_space_id) return [];

  if (!isSharedCloudReady(options)) {
    throw new Error('listSpaceAccess: shared cloud is not configured.');
  }

  const admin = getSharedCloudAdmin(options);
  return admin.select('shared_space_acl', {
    filters: { shared_space_id: ledger.cloud_space_id },
    select: '*',
    limit: 1000,
  });
}

/**
 * Read-time enforcement entry point: returns the effective permission a user has
 * on a local space, or null when they have no access. Combines the published
 * space's visibility with its cloud ACL grants via decideSpacePermission().
 *
 * @param {object} args  { local_space_id, user_id, space_type?, org_id?, session? }.
 * @param {object} [options]
 * @returns {Promise<('read'|'write'|'admin'|null)>}
 */
export async function resolveSpacePermission(args = {}, options = {}) {
  assertServerOnly();
  const { local_space_id, user_id, space_type = 'project', session } = args;
  if (!local_space_id) throw new Error('resolveSpacePermission: local_space_id is required');
  assertSpaceType(space_type, 'resolveSpacePermission');
  const org_id = args.org_id || session?.org_id;

  const ledger = _readSpaceLedger(local_space_id, space_type);
  if (!ledger || ledger.status !== 'published') return null; // not shared → no cloud access

  const grants = await listSpaceAccess({ local_space_id, space_type, org_id }, options);
  return decideSpacePermission({
    visibility: ledger.visibility,
    grants,
    userId: user_id,
    orgId: org_id,
  });
}
