import { ReferenceKind } from "@mikro-orm/core";
import { resolveEntityIdFromMetadata } from "./entityIds.js";
import { parseDecryptedFieldValue } from "./tenantDataEncryptionService.js";
import { isTenantDataEncryptionEnabled } from "./toggles.js";
import { isEncryptionDebugEnabled } from "./toggles.js";
import { resolveTenantEncryptionService } from "./customFieldValues.js";
import { createLogger } from "../logger/index.js";
import { listEntityMetadataFromRegistry } from "../db/entityMetadata.js";
const logger = createLogger("shared").child({ component: "encryption" });
function resolveScope(entity) {
  const tenantId = entity.tenantId ?? entity.tenant_id ?? entity.tenant?.id ?? null;
  const organizationId = entity.organizationId ?? entity.organization_id ?? entity.organization?.id ?? null;
  return {
    tenantId: tenantId ? String(tenantId) : null,
    organizationId: organizationId ? String(organizationId) : null
  };
}
function debug(event, payload) {
  if (!isEncryptionDebugEnabled()) return;
  try {
    logger.debug(event, payload);
  } catch {
  }
}
function isJsonColumnProperty(prop) {
  if (!prop || typeof prop !== "object") return false;
  const candidate = prop;
  const type = typeof candidate.type === "string" ? candidate.type.toLowerCase() : "";
  if (type === "json" || type === "jsonb") return true;
  const columnTypes = Array.isArray(candidate.columnTypes) ? candidate.columnTypes : [];
  if (columnTypes.some((value) => typeof value === "string" && value.toLowerCase().includes("json"))) return true;
  const customTypeName = candidate.customType?.constructor?.name;
  return typeof customTypeName === "string" && customTypeName.toLowerCase().includes("json");
}
const registeredEventManagers = /* @__PURE__ */ new WeakSet();
const subscribersByService = /* @__PURE__ */ new WeakMap();
function getSubscriberForService(service) {
  const existing = subscribersByService.get(service);
  if (existing) return existing;
  const subscriber = new TenantEncryptionSubscriber(service);
  subscribersByService.set(service, subscriber);
  return subscriber;
}
const toSnakeCase = (value) => value.replace(/([A-Z])/g, "_$1").replace(/__/g, "_").toLowerCase();
class TenantEncryptionSubscriber {
  constructor(service) {
    this.service = service;
  }
  getSubscribedEntities() {
    return [];
  }
  resolveMeta(meta, entity, em) {
    if (meta) return meta;
    const ctor = entity?.constructor;
    const name = ctor?.name;
    const registry = em?.getMetadata?.();
    if (!registry || !name) return meta;
    try {
      return registry.find?.(name);
    } catch {
    }
    try {
      return registry.find?.(ctor);
    } catch {
    }
    try {
      return registry.get?.(name);
    } catch {
    }
    try {
      return registry.get?.(ctor);
    } catch {
    }
    try {
      const entries = listEntityMetadataFromRegistry(registry);
      const match = entries.find(
        (m) => m?.className === name || m?.name === name || m?.entityName === name || m?.collection === ctor?.prototype?.__meta?.tableName || m?.tableName === ctor?.prototype?.__meta?.tableName
      );
      if (match) return match;
    } catch {
    }
    return meta;
  }
  resolveEntityId(meta) {
    try {
      return resolveEntityIdFromMetadata(meta);
    } catch {
      return null;
    }
  }
  restoreDecryptedJsonColumns(target, meta) {
    const properties = meta?.properties;
    if (!properties || typeof properties !== "object") return;
    for (const [propertyName, prop] of Object.entries(properties)) {
      if (!isJsonColumnProperty(prop)) continue;
      const value = target[propertyName];
      if (typeof value !== "string") continue;
      const parsed = parseDecryptedFieldValue(value);
      if (parsed !== value) target[propertyName] = parsed;
    }
  }
  syncOriginalEntityData(target, meta, em) {
    const helper = target?.__helper;
    if (!helper || typeof helper !== "object") return;
    try {
      const comparator = em?.getComparator?.();
      if (comparator?.prepareEntity) {
        helper.__originalEntityData = comparator.prepareEntity(target);
        helper.__touched = false;
        return;
      }
    } catch (err) {
      debug("\u26AA\uFE0F subscriber.sync_original.comparator_failed", {
        entity: meta?.className || meta?.name,
        message: err?.message ?? String(err)
      });
    }
    const properties = meta?.properties ? Object.values(meta.properties) : [];
    if (properties.length === 0) return;
    const snapshot = { ...helper.__originalEntityData ?? {} };
    for (const prop of properties) {
      if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) continue;
      const name = prop.name;
      if (typeof name !== "string" || !name.length) continue;
      snapshot[name] = target[name];
    }
    helper.__originalEntityData = snapshot;
    helper.__touched = false;
  }
  /**
   * Reports whether a managed entity currently carries un-flushed changes relative to its load
   * baseline, using MikroORM's own comparator so the verdict matches the change-set computer that
   * runs at flush time. Used to avoid re-baselining (and thereby discarding) pending writes when a
   * decrypt pass traverses back into an entity a command already mutated.
   */
  hasPendingChanges(target, meta, em) {
    const helper = target?.__helper;
    if (!helper || typeof helper !== "object") return false;
    const original = helper.__originalEntityData;
    if (!original) return false;
    const entityName = meta?.className || meta?.name;
    try {
      const comparator = em?.getComparator?.();
      if (entityName && comparator?.prepareEntity) {
        const current = comparator.prepareEntity(target);
        if (typeof comparator.matching === "function") {
          return !comparator.matching(entityName, original, current);
        }
        if (typeof comparator.diffEntities === "function") {
          const diff = comparator.diffEntities(entityName, original, current);
          return !!diff && Object.keys(diff).length > 0;
        }
      }
    } catch (err) {
      debug("\u26AA\uFE0F subscriber.pending_changes.compare_failed", {
        entity: entityName,
        message: err?.message ?? String(err)
      });
    }
    return helper.__touched === true;
  }
  async encrypt(target, meta, em, changeSet) {
    if (!isTenantDataEncryptionEnabled() || !this.service.isEnabled()) {
      debug("\u26AA\uFE0F subscriber.skip", { reason: "disabled", entity: meta?.className || meta?.name });
      return;
    }
    const resolvedMeta = this.resolveMeta(meta, target, em);
    const entityId = this.resolveEntityId(resolvedMeta);
    if (!entityId) {
      debug("\u26A0\uFE0F subscriber.decrypt.skip.entity_id_missing", {
        metaName: resolvedMeta?.className || resolvedMeta?.name,
        table: resolvedMeta?.tableName
      });
      return;
    }
    const { tenantId, organizationId } = resolveScope(target);
    const encrypted = await this.service.encryptEntityPayload(entityId, target, tenantId, organizationId);
    const metaProps = resolvedMeta?.properties && typeof resolvedMeta.properties === "object" ? resolvedMeta.properties : {};
    const payloadObj = changeSet && typeof changeSet === "object" ? typeof changeSet.payload === "object" && changeSet.payload ? changeSet.payload : changeSet.payload = {} : null;
    const updates = {};
    const columnNameFor = (propKey, prop) => {
      try {
        if (prop && typeof prop === "object") {
          const explicit = prop?.fieldName;
          if (typeof explicit === "string" && explicit.length) return explicit;
          const name = prop?.name;
          if (typeof name === "string" && name.length) return name;
        }
      } catch (err) {
        debug("\u26A0\uFE0F subscriber.column_name.resolve", {
          entityId,
          propKey,
          message: err?.message ?? String(err)
        });
      }
      return toSnakeCase(propKey);
    };
    for (const [key, value] of Object.entries(encrypted)) {
      const prop = metaProps[key];
      if (!prop || typeof prop !== "object") continue;
      if (target[key] === value) continue;
      updates[key] = value;
    }
    if (Object.keys(updates).length === 0) return;
    Object.assign(target, updates);
    if (payloadObj) {
      try {
        const ensureColumnKey = (propKey, value) => {
          const columnName = columnNameFor(propKey, metaProps[propKey]);
          const canonicalKey = columnName || toSnakeCase(propKey);
          const aliases = new Set(
            [propKey, toSnakeCase(propKey), columnName, columnName ? toSnakeCase(columnName) : void 0].filter(
              (v) => typeof v === "string" && v.length > 0
            )
          );
          for (const alias of aliases) {
            if (Object.prototype.hasOwnProperty.call(payloadObj, alias)) delete payloadObj[alias];
          }
          const finalKey = columnName || toSnakeCase(propKey);
          payloadObj[finalKey] = value;
        };
        for (const key of Object.keys(updates)) {
          ensureColumnKey(key, updates[key]);
        }
      } catch (err) {
        debug("\u26A0\uFE0F subscriber.payload.normalize.error", {
          entityId,
          message: err?.message ?? String(err)
        });
      }
    }
  }
  async decryptEntityGraph(target, meta, em, opts = {}) {
    await this.decrypt(target, meta, em, opts);
  }
  async decrypt(target, meta, em, {
    syncOriginal = false,
    seen,
    fallbackScope
  } = {}) {
    const visited = seen ?? /* @__PURE__ */ new WeakSet();
    if (visited.has(target)) return;
    visited.add(target);
    if (!isTenantDataEncryptionEnabled() || !this.service.isEnabled()) {
      debug("\u26AA\uFE0F subscriber.skip", { reason: "disabled", entity: meta?.className || meta?.name });
      return;
    }
    const resolvedMeta = this.resolveMeta(meta, target, em);
    const entityId = this.resolveEntityId(resolvedMeta);
    if (!entityId) return;
    const { tenantId, organizationId } = resolveScope(target);
    const scopedTenantId = tenantId ?? fallbackScope?.tenantId ?? null;
    const scopedOrgId = organizationId ?? fallbackScope?.organizationId ?? null;
    const hadPendingChanges = syncOriginal ? this.hasPendingChanges(target, resolvedMeta, em) : false;
    const decrypted = await this.service.decryptEntityPayload(entityId, target, scopedTenantId, scopedOrgId);
    Object.assign(target, decrypted);
    this.restoreDecryptedJsonColumns(target, resolvedMeta);
    if (syncOriginal && !hadPendingChanges) {
      this.syncOriginalEntityData(target, resolvedMeta, em);
    }
    const nextFallback = fallbackScope ?? (tenantId || organizationId ? { tenantId: tenantId ?? null, organizationId: organizationId ?? null } : { tenantId: scopedTenantId, organizationId: scopedOrgId });
    try {
      const extractEntities = (value) => {
        if (!value) return [];
        if (typeof value === "object" && typeof value.isInitialized === "function" && typeof value.getItems === "function") {
          try {
            return value.isInitialized() ? value.getItems() ?? [] : [];
          } catch {
            return [];
          }
        }
        if (typeof value === "object" && typeof value.isInitialized === "function") {
          try {
            if (value.isInitialized()) {
              const unwrapped = typeof value.unwrap === "function" ? value.unwrap() : value.__entity ?? value;
              if (unwrapped && typeof unwrapped === "object") return [unwrapped];
            }
          } catch {
          }
          return [];
        }
        if (Array.isArray(value)) return value;
        if (typeof value === "object") return [value];
        return [];
      };
      const props = resolvedMeta?.properties ? Object.values(resolvedMeta.properties) : [];
      for (const prop of props) {
        const kind = prop?.kind;
        const name = prop?.name;
        if (typeof name !== "string" || !name.length) continue;
        const value = target[name];
        if (!value) continue;
        if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(kind)) {
          const nestedEntities = extractEntities(value);
          for (const nested of nestedEntities) {
            const nestedMeta = this.resolveMeta(nested.__meta ?? nested.__helper?.__meta, nested, em);
            await this.decrypt(nested, nestedMeta, em, {
              syncOriginal: true,
              seen: visited,
              fallbackScope: nextFallback
            });
          }
          continue;
        }
        if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(kind)) {
          const items = extractEntities(value);
          for (const item of items) {
            if (!item || typeof item !== "object") continue;
            const nestedMeta = this.resolveMeta(item.__meta ?? item.__helper?.__meta, item, em);
            await this.decrypt(item, nestedMeta, em, {
              syncOriginal: true,
              seen: visited,
              fallbackScope: nextFallback
            });
          }
        }
      }
    } catch (err) {
      debug("\u26A0\uFE0F subscriber.deep_decrypt.error", {
        entityId,
        message: err?.message ?? String(err)
      });
    }
  }
  async beforeCreate(args) {
    await this.encrypt(args.entity, args.meta, args.em, args.changeSet);
  }
  async beforeUpdate(args) {
    await this.decrypt(args.entity, args.meta, args.em);
    await this.encrypt(args.entity, args.meta, args.em, args.changeSet);
  }
  async afterCreate(args) {
    await this.decrypt(args.entity, args.meta, args.em, { syncOriginal: true });
  }
  async afterUpdate(args) {
    await this.decrypt(args.entity, args.meta, args.em, { syncOriginal: true });
  }
  async afterUpsert(args) {
    await this.decrypt(args.entity, args.meta, args.em, { syncOriginal: true });
  }
  async onLoad(args) {
    await this.decrypt(args.entity, args.meta, args.em, { syncOriginal: true });
  }
  async afterFind(args) {
    const entities = Array.isArray(args.entities) ? args.entities : [];
    for (const entity of entities) {
      await this.decrypt(entity, args.meta, args.em, { syncOriginal: true });
    }
  }
}
function registerTenantEncryptionSubscriber(em, service) {
  const eventManager = em?.getEventManager?.();
  if (!eventManager || typeof eventManager.registerSubscriber !== "function") return;
  if (registeredEventManagers.has(eventManager)) return;
  eventManager.registerSubscriber(new TenantEncryptionSubscriber(service));
  registeredEventManagers.add(eventManager);
}
async function decryptEntitiesWithFallbackScope(targets, {
  em,
  tenantId,
  organizationId,
  encryptionService
}) {
  if (!isTenantDataEncryptionEnabled()) return;
  const list = Array.isArray(targets) ? targets : [targets];
  if (!list.length) return;
  const service = encryptionService ?? resolveTenantEncryptionService(em);
  if (!service || !service.isEnabled()) return;
  const subscriber = getSubscriberForService(service);
  const fallback = tenantId || organizationId ? {
    tenantId: tenantId ?? null,
    organizationId: organizationId ?? null
  } : void 0;
  for (const entity of list) {
    if (!entity || typeof entity !== "object") continue;
    const meta = entity.__meta ?? entity.__helper?.__meta;
    await subscriber.decryptEntityGraph(entity, meta, em, {
      syncOriginal: true,
      fallbackScope: fallback
    });
  }
}
export {
  TenantEncryptionSubscriber,
  decryptEntitiesWithFallbackScope,
  registerTenantEncryptionSubscriber
};
//# sourceMappingURL=subscriber.js.map
