import {
  DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,
  DATA_STORE_CHANGE_DETAIL_TYPE,
  buildFhirCurrentResourceChangeDetail,
  dynamodbImageToPlain
} from "./chunk-RC7HHZR6.mjs";
import {
  decompressResource
} from "./chunk-APVVG7BO.mjs";
import {
  require_lib
} from "./chunk-KMEWULMX.mjs";
import {
  __toESM
} from "./chunk-LZOMFHX3.mjs";

// src/components/dynamodb/firehose-archive-transform.handler.ts
var import_workflows = __toESM(require_lib());
import { randomUUID } from "crypto";
import {
  EventBridgeClient,
  PutEventsCommand
} from "@aws-sdk/client-eventbridge";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
var PK_PATTERN = /^TID#(?<tenantId>[^#]+)#WID#(?<workspaceId>[^#]+)#RT#(?<resourceType>[^#]+)#ID#(?<resourceId>.+)$/i;
var CURRENT_SK_PATTERN = /^(CURRENT|\$[A-Za-z0-9_-]+_[0-9]+#sk_current)$/i;
var AWS_REP_UPDATE_REGION = "aws:rep:updateregion";
function getDynamoDbStringAttr(image, name) {
  if (!image) {
    return void 0;
  }
  const av = image[name];
  if (typeof av?.S === "string" && av.S.trim() !== "") {
    return av.S.trim();
  }
  return void 0;
}
function primaryImageForReplicationCheck(record) {
  if (record.eventName === "REMOVE") {
    return record.dynamodb?.OldImage;
  }
  return record.dynamodb?.NewImage;
}
function shouldDropAsGlobalTableReplicationRecord(record, archiveLambdaRegion) {
  const image = primaryImageForReplicationCheck(record);
  const updateRegion = getDynamoDbStringAttr(image, AWS_REP_UPDATE_REGION);
  if (updateRegion && archiveLambdaRegion && updateRegion !== archiveLambdaRegion) {
    return true;
  }
  return isDynamoDbReplicationUserIdentity(record.userIdentity);
}
function isDynamoDbReplicationUserIdentity(userIdentity) {
  if (!userIdentity || typeof userIdentity !== "object") {
    return false;
  }
  const ui = userIdentity;
  const principalRaw = ui.principalId ?? ui.PrincipalId;
  const typeRaw = ui.type ?? ui.Type;
  const principal = typeof principalRaw === "string" ? principalRaw.toLowerCase() : "";
  const type = typeof typeRaw === "string" ? typeRaw.toLowerCase() : "";
  if (type === "service" && principal === "dynamodb.amazonaws.com") {
    return false;
  }
  const replicationMarkers = [
    "awsservicerolefordynamodbreplication",
    "replication.dynamodb.amazonaws.com"
  ];
  return replicationMarkers.some((m) => principal.includes(m));
}
function parseCurrentResourceKeys(record) {
  const keys = record.dynamodb?.Keys;
  if (!keys) {
    return null;
  }
  const pkAttr = keys.PK?.S;
  const skAttr = keys.SK?.S;
  if (!pkAttr || !skAttr || !CURRENT_SK_PATTERN.test(skAttr)) {
    return null;
  }
  const m = PK_PATTERN.exec(pkAttr);
  if (!m?.groups) {
    return null;
  }
  const {
    tenantId,
    workspaceId,
    resourceType: pkResourceType,
    resourceId
  } = m.groups;
  const image = record.eventName === "REMOVE" ? record.dynamodb?.OldImage : record.dynamodb?.NewImage;
  if (!image) {
    return null;
  }
  const plain = dynamodbImageToPlain(image);
  const version = typeof plain.vid === "string" ? plain.vid : null;
  if (!version) {
    return null;
  }
  const resourceType = extractFhirResourceTypeFromImage(plain) ?? pkResourceType;
  return { tenantId, workspaceId, resourceType, resourceId, version };
}
function extractFhirResourceTypeFromImage(plain) {
  const resourceStr = typeof plain.resource === "string" ? plain.resource : void 0;
  if (!resourceStr) {
    return void 0;
  }
  try {
    const parsed = JSON.parse(decompressResource(resourceStr));
    if (typeof parsed.resourceType === "string" && parsed.resourceType !== "") {
      return parsed.resourceType;
    }
  } catch {
  }
  return void 0;
}
function partitionToken(value) {
  if (!value || value.trim() === "") {
    return "-";
  }
  return value.replace(/[/\\]/g, "_");
}
function buildArchivePayload(record, keys) {
  const newImage = record.dynamodb?.NewImage;
  const oldImage = record.dynamodb?.OldImage;
  const resourceImage = record.eventName === "REMOVE" ? oldImage : newImage;
  const resourcePlain = resourceImage ? dynamodbImageToPlain(resourceImage) : {};
  if (typeof resourcePlain.resource === "string") {
    try {
      resourcePlain.resource = JSON.parse(
        decompressResource(resourcePlain.resource)
      );
    } catch {
    }
  }
  return {
    eventName: record.eventName,
    archivedAt: (/* @__PURE__ */ new Date()).toISOString(),
    tenantId: keys.tenantId,
    workspaceId: keys.workspaceId,
    resourceType: keys.resourceType,
    resourceId: keys.resourceId,
    version: keys.version,
    resource: resourcePlain
  };
}
var PUT_EVENTS_BATCH_SIZE = 10;
var MAX_PUT_EVENTS_ROUNDS = 3;
var eventBridgeClient;
function getEventBridgeClient() {
  const bus = process.env.DATA_EVENT_BUS_NAME?.trim();
  if (!bus) {
    return void 0;
  }
  if (!eventBridgeClient) {
    eventBridgeClient = new EventBridgeClient({});
  }
  return eventBridgeClient;
}
var s3ClientForDlq;
function getS3ClientForDlq() {
  const bucket = process.env.DATA_STORE_PUT_EVENTS_DLQ_BUCKET?.trim();
  if (!bucket) {
    return void 0;
  }
  if (!s3ClientForDlq) {
    s3ClientForDlq = new S3Client({});
  }
  return s3ClientForDlq;
}
async function writePutEventsFailuresToDlq(payload) {
  const bucket = process.env.DATA_STORE_PUT_EVENTS_DLQ_BUCKET?.trim();
  const client = getS3ClientForDlq();
  if (!bucket || !client) {
    throw new Error(
      `PutEvents exhausted retries but DATA_STORE_PUT_EVENTS_DLQ_BUCKET is not set (${payload.reason})`
    );
  }
  const day = payload.failedAt.slice(0, 10);
  const key = `put-events-failed/${day}/${randomUUID()}.json`;
  await client.send(
    new PutObjectCommand({
      Bucket: bucket,
      Key: key,
      Body: JSON.stringify(payload),
      ContentType: "application/json"
    })
  );
}
async function putEventsChunkWithRetriesAndDlq(client, entries) {
  if (entries.length === 0) {
    return;
  }
  let pending = [...entries];
  for (let round = 1; round <= MAX_PUT_EVENTS_ROUNDS; round++) {
    try {
      const out = await client.send(new PutEventsCommand({ Entries: pending }));
      const failed = out.FailedEntryCount ?? 0;
      if (failed === 0) {
        return;
      }
      const nextPending = [];
      out.Entries?.forEach((e, i) => {
        if (e?.ErrorCode && pending[i]) {
          nextPending.push(pending[i]);
        }
      });
      pending = nextPending;
      if (pending.length === 0) {
        return;
      }
      if (round === MAX_PUT_EVENTS_ROUNDS) {
        await writePutEventsFailuresToDlq({
          dlqSchemaVersion: 1,
          failedAt: (/* @__PURE__ */ new Date()).toISOString(),
          reason: "put_events_partial_failure",
          attemptRounds: MAX_PUT_EVENTS_ROUNDS,
          entries: pending,
          putEventsResultEntries: out.Entries
        });
        return;
      }
    } catch (sdkErr) {
      const sdkMessage = sdkErr instanceof Error ? sdkErr.message : String(sdkErr);
      if (round === MAX_PUT_EVENTS_ROUNDS) {
        await writePutEventsFailuresToDlq({
          dlqSchemaVersion: 1,
          failedAt: (/* @__PURE__ */ new Date()).toISOString(),
          reason: "put_events_sdk_error",
          attemptRounds: MAX_PUT_EVENTS_ROUNDS,
          entries: pending,
          sdkError: sdkMessage
        });
        return;
      }
      await new Promise((r) => setTimeout(r, 50 * round));
    }
  }
}
async function publishDataStoreChangeEvents(pending) {
  const client = getEventBridgeClient();
  const busName = process.env.DATA_EVENT_BUS_NAME?.trim();
  if (!client || !busName || pending.length === 0) {
    return;
  }
  const entries = [];
  for (const { change, keys } of pending) {
    const detailObj = buildFhirCurrentResourceChangeDetail(change, keys);
    const detail = JSON.stringify(detailObj);
    const detailBytes = Buffer.byteLength(detail, "utf8");
    if (detailBytes > DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES) {
      throw new Error(
        `Event detail is ${detailBytes} bytes (max ${DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES}); oversize strategy deferred per ADR 2026-03-02-01 (${keys.resourceType}/${keys.resourceId}).`
      );
    }
    entries.push({
      Source: import_workflows.OPENHI_DATA_SOURCE,
      DetailType: DATA_STORE_CHANGE_DETAIL_TYPE,
      Detail: detail,
      EventBusName: busName
    });
  }
  for (let i = 0; i < entries.length; i += PUT_EVENTS_BATCH_SIZE) {
    const chunk = entries.slice(i, i + PUT_EVENTS_BATCH_SIZE);
    await putEventsChunkWithRetriesAndDlq(client, chunk);
  }
}
async function handler(event) {
  const records = [];
  const archiveLambdaRegion = process.env.AWS_REGION ?? "";
  const pendingPublish = [];
  for (const rec of event.records) {
    try {
      const payload = Buffer.from(rec.data, "base64").toString("utf8");
      const change = JSON.parse(payload);
      if (shouldDropAsGlobalTableReplicationRecord(change, archiveLambdaRegion)) {
        records.push({
          recordId: rec.recordId,
          result: "Dropped",
          data: rec.data
        });
        continue;
      }
      const keys = parseCurrentResourceKeys(change);
      if (!keys) {
        records.push({
          recordId: rec.recordId,
          result: "Dropped",
          data: rec.data
        });
        continue;
      }
      const archive = buildArchivePayload(change, keys);
      const out = Buffer.from(`${JSON.stringify(archive)}
`).toString(
        "base64"
      );
      pendingPublish.push({ change, keys });
      records.push({
        recordId: rec.recordId,
        result: "Ok",
        data: out,
        metadata: {
          partitionKeys: {
            tenantId: partitionToken(keys.tenantId),
            workspaceId: partitionToken(keys.workspaceId),
            resourceType: partitionToken(keys.resourceType),
            resourceId: partitionToken(keys.resourceId),
            version: partitionToken(keys.version)
          }
        }
      });
    } catch {
      records.push({
        recordId: rec.recordId,
        result: "ProcessingFailed",
        data: rec.data
      });
    }
  }
  await publishDataStoreChangeEvents(pendingPublish);
  return { records };
}

export {
  shouldDropAsGlobalTableReplicationRecord,
  parseCurrentResourceKeys,
  buildArchivePayload,
  handler
};
//# sourceMappingURL=chunk-AWAWCRWW.mjs.map