import {
  compressResource,
  decompressResource
} from "./chunk-APVVG7BO.mjs";
import {
  NotFoundError
} from "./chunk-FYHBHHWK.mjs";
import {
  SHARD_COUNT,
  getDynamoControlService
} from "./chunk-EUIP2U5F.mjs";

// src/data/audit-meta.ts
var OPENHI_EXT = "http://openhi.org/fhir/StructureDefinition";
function mergeAuditIntoMeta(meta, audit) {
  const existing = meta ?? {};
  const ext = [
    ...Array.isArray(existing.extension) ? existing.extension : []
  ];
  const byUrl = new Map(ext.map((e) => [e.url, e]));
  function set(url, value, type) {
    if (value == null) return;
    byUrl.set(url, { url, [type]: value });
  }
  set(`${OPENHI_EXT}/created-date`, audit.createdDate, "valueDateTime");
  set(`${OPENHI_EXT}/created-by-id`, audit.createdById, "valueString");
  set(`${OPENHI_EXT}/created-by-name`, audit.createdByName, "valueString");
  set(`${OPENHI_EXT}/modified-date`, audit.modifiedDate, "valueDateTime");
  set(`${OPENHI_EXT}/modified-by-id`, audit.modifiedById, "valueString");
  set(`${OPENHI_EXT}/modified-by-name`, audit.modifiedByName, "valueString");
  set(`${OPENHI_EXT}/deleted-date`, audit.deletedDate, "valueDateTime");
  set(`${OPENHI_EXT}/deleted-by-id`, audit.deletedById, "valueString");
  set(`${OPENHI_EXT}/deleted-by-name`, audit.deletedByName, "valueString");
  return { ...existing, extension: Array.from(byUrl.values()) };
}

// src/data/operations/data-operations-common.ts
import { extractSortKey, extractSummary } from "@openhi/types";
var DATA_ENTITY_SK = "CURRENT";
function deriveVid(lastUpdated) {
  return lastUpdated.replace(/\D/g, "") || Date.now().toString(36);
}
async function getDataEntityById(entity, tenantId, workspaceId, id, resourceLabel) {
  const result = await entity.get({
    tenantId,
    workspaceId,
    id,
    sk: DATA_ENTITY_SK
  }).go();
  if (!result.data) {
    throw new NotFoundError(`${resourceLabel} ${id} not found`, {
      details: { id }
    });
  }
  const parsed = JSON.parse(decompressResource(result.data.resource));
  return {
    id: result.data.id,
    resource: { ...parsed, id: result.data.id }
  };
}
async function deleteDataEntityById(entity, tenantId, workspaceId, id) {
  await entity.delete({
    tenantId,
    workspaceId,
    id,
    sk: DATA_ENTITY_SK
  }).go();
}
var BATCH_GET_MAX_ATTEMPTS = 3;
var BATCH_GET_BASE_BACKOFF_MS = 50;
async function batchGetWithRetry(entity, keys, options) {
  if (keys.length === 0) return [];
  const collected = [];
  let pending = keys;
  let attempt = 0;
  while (pending.length > 0) {
    if (attempt > 0) {
      await new Promise(
        (resolve) => setTimeout(resolve, BATCH_GET_BASE_BACKOFF_MS * 2 ** (attempt - 1))
      );
    }
    attempt++;
    const result = await entity.get(pending).go(options?.consistent ? { consistent: true } : void 0);
    collected.push(...result.data);
    const unprocessed = result.unprocessed ?? [];
    if (unprocessed.length === 0) break;
    if (attempt >= BATCH_GET_MAX_ATTEMPTS) {
      throw new Error(
        `BatchGet exhausted retries: ${unprocessed.length} key(s) still unprocessed after ${BATCH_GET_MAX_ATTEMPTS} attempt(s)`
      );
    }
    pending = unprocessed;
  }
  return collected;
}
async function dispatchListMode(mode, shardResults, hooks) {
  if (mode === "count") {
    let total = 0;
    for (const shardResult of shardResults) {
      total += (shardResult.data ?? []).length;
    }
    return { entries: [], total };
  }
  if (mode === "summary") {
    const entries2 = [];
    for (const shardResult of shardResults) {
      for (const item of shardResult.data ?? []) {
        if (typeof item.summary !== "string") continue;
        let parsed;
        try {
          parsed = JSON.parse(item.summary);
        } catch {
          continue;
        }
        entries2.push(hooks.buildSummaryEntry(item.id, parsed));
      }
    }
    return { entries: entries2, total: entries2.length };
  }
  const orderedIds = [];
  for (const shardResult of shardResults) {
    for (const item of shardResult.data ?? []) {
      orderedIds.push(item.id);
    }
  }
  if (orderedIds.length === 0) return { entries: [], total: 0 };
  const items = await hooks.hydrate(orderedIds);
  const byId = new Map(items.map((item) => [hooks.getId(item), item]));
  const entries = [];
  for (const id of orderedIds) {
    const item = byId.get(id);
    if (!item) continue;
    entries.push(hooks.buildEntry(id, item));
  }
  return { entries, total: entries.length };
}
async function listDataEntitiesByWorkspace(entity, tenantId, workspaceId, mode = "full") {
  const shardResults = await Promise.all(
    Array.from(
      { length: SHARD_COUNT },
      (_, shard) => entity.query.gsi1({ tenantId, workspaceId, gsi1Shard: String(shard) }).go()
    )
  );
  return dispatchListMode(
    mode,
    shardResults,
    {
      hydrate: (orderedIds) => batchGetWithRetry(
        entity,
        orderedIds.map((id) => ({
          tenantId,
          workspaceId,
          id,
          sk: DATA_ENTITY_SK
        }))
      ),
      getId: (item) => item.id,
      buildEntry: (id, item) => {
        const parsed = JSON.parse(decompressResource(item.resource));
        return { id, resource: { ...parsed, id } };
      },
      buildSummaryEntry: (id, parsed) => ({
        id,
        resource: { ...parsed, id }
      })
    }
  );
}
async function createDataEntityRecord(entity, tenantId, workspaceId, id, resourceWithAudit, fallbackDate) {
  const lastUpdated = resourceWithAudit.meta?.lastUpdated ?? fallbackDate ?? (/* @__PURE__ */ new Date()).toISOString();
  const vid = deriveVid(lastUpdated);
  const resourceLike = resourceWithAudit;
  const summary = JSON.stringify(extractSummary(resourceLike));
  const gsi1sk = extractSortKey(resourceLike);
  await entity.put({
    sk: DATA_ENTITY_SK,
    tenantId,
    workspaceId,
    id,
    resource: compressResource(JSON.stringify(resourceWithAudit)),
    summary,
    vid,
    lastUpdated,
    gsi1sk
  }).go();
  return {
    id,
    resource: resourceWithAudit
  };
}
function buildUpdatedResourceWithAudit(body, id, date, actorId, actorName, existingResourceStr, resourceType) {
  const existingMeta = JSON.parse(existingResourceStr).meta;
  const bodyWithMeta = body;
  const resourceWithVersion = {
    ...body,
    resourceType,
    id,
    meta: {
      ...bodyWithMeta.meta ?? {},
      lastUpdated: date,
      versionId: "2"
    }
  };
  const resourceWithAudit = {
    ...resourceWithVersion,
    meta: mergeAuditIntoMeta(resourceWithVersion.meta ?? existingMeta, {
      modifiedDate: date,
      modifiedById: actorId,
      modifiedByName: actorName
    })
  };
  return {
    resource: resourceWithAudit,
    lastUpdated: date
  };
}
async function updateDataEntityById(entity, tenantId, workspaceId, id, resourceLabel, context, buildPatched) {
  const existing = await entity.get({
    tenantId,
    workspaceId,
    id,
    sk: DATA_ENTITY_SK
  }).go();
  if (!existing.data) {
    throw new NotFoundError(`${resourceLabel} ${id} not found`, {
      details: { id }
    });
  }
  const existingStr = decompressResource(existing.data.resource);
  const { resource, lastUpdated } = buildPatched(existingStr);
  const resourceLike = resource;
  const summary = JSON.stringify(extractSummary(resourceLike));
  const gsi1sk = extractSortKey(resourceLike);
  await entity.patch({
    tenantId,
    workspaceId,
    id,
    sk: DATA_ENTITY_SK
  }).set({
    resource: compressResource(JSON.stringify(resource)),
    summary,
    // A fresh vid is what makes this UPDATE visible to the search tier:
    // the replication upsert rejects any event whose version does not
    // strictly exceed the stored row's. See deriveVid.
    vid: deriveVid(lastUpdated),
    lastUpdated,
    gsi1sk
  }).go();
  return {
    id,
    resource
  };
}

// src/data/operations/control/user/user-list-operation.ts
var SK = "CURRENT";
function counterValue(value) {
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
async function listUsersOperation(params) {
  const { tableName, mode = "full" } = params;
  const service = getDynamoControlService(tableName);
  const shardResults = await Promise.all(
    Array.from(
      { length: SHARD_COUNT },
      (_, shard) => service.entities.user.query.gsi1({ gsi1Shard: String(shard) }).go()
    )
  );
  return dispatchListMode(mode, shardResults, {
    hydrate: (orderedIds) => batchGetWithRetry(
      service.entities.user,
      orderedIds.map((id) => ({ id, sk: SK }))
    ),
    getId: (item) => item.id,
    // FULL mode (admin list default): read the ADR-028 counters off the
    // canonical record hydrated by BatchGet and expose them as
    // `resource.counts`. Missing counters render as 0.
    buildEntry: (id, item) => ({
      id,
      resource: {
        resourceType: "User",
        id,
        ...JSON.parse(item.resource),
        counts: {
          tenantsForUser: counterValue(item.tenantsForUser),
          workspacesForUser: counterValue(item.workspacesForUser)
        }
      }
    }),
    // SUMMARY mode reads only the GSI1 `summary` projection (no
    // counters); surface zeros so the shape stays uniform.
    buildSummaryEntry: (id, parsed) => ({
      id,
      resource: {
        resourceType: "User",
        id,
        ...parsed,
        counts: { tenantsForUser: 0, workspacesForUser: 0 }
      }
    })
  });
}

// src/data/operations/control/membership/membership-list-by-user-operation.ts
function buildSkPrefix(mode, tenantId) {
  switch (mode) {
    case "tenant":
      return "MEMBERSHIP#TENANT#";
    case "workspace":
      return "MEMBERSHIP#WORKSPACE#";
    case "workspaceInTenant":
      return `MEMBERSHIP#WORKSPACE#TID#${tenantId}#`;
    case "all":
    default:
      return "MEMBERSHIP#";
  }
}
async function membershipListByUserOperation(params) {
  const {
    userId,
    mode = "all",
    tenantId,
    cursor = null,
    limit,
    order,
    tableName
  } = params;
  if (mode === "workspaceInTenant" && !tenantId) {
    throw new Error(
      'membershipListByUserOperation: tenantId is required when mode === "workspaceInTenant"'
    );
  }
  const service = getDynamoControlService(tableName);
  const skPrefix = buildSkPrefix(mode, tenantId);
  const goOptions = {
    cursor
  };
  if (limit !== void 0) {
    goOptions.limit = limit;
  }
  if (order !== void 0) {
    goOptions.order = order;
  }
  const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: skPrefix }).go(goOptions);
  const items = (result.data ?? []).map(
    (row) => ({
      userId: row.userId,
      sk: row.sk,
      tenantId: row.tenantId,
      workspaceId: row.workspaceId,
      membershipId: row.membershipId,
      summary: row.summary,
      vid: row.vid,
      lastUpdated: row.lastUpdated,
      denormalizedTenantName: row.denormalizedTenantName,
      denormalizedUserName: row.denormalizedUserName,
      denormalizedWorkspaceName: row.denormalizedWorkspaceName
    })
  );
  return { items, cursor: result.cursor ?? null };
}

export {
  mergeAuditIntoMeta,
  DATA_ENTITY_SK,
  getDataEntityById,
  deleteDataEntityById,
  batchGetWithRetry,
  dispatchListMode,
  listDataEntitiesByWorkspace,
  createDataEntityRecord,
  buildUpdatedResourceWithAudit,
  updateDataEntityById,
  buildSkPrefix,
  membershipListByUserOperation,
  listUsersOperation
};
//# sourceMappingURL=chunk-V6AO5T57.mjs.map