import { decryptWithAesGcm, encryptWithAesGcm } from "@open-mercato/shared/lib/encryption/aes";
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { createKmsService } from "@open-mercato/shared/lib/encryption/kms";
import { parseDecryptedFieldValue } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
import {
  getBundle,
  getIntegration,
  resolveIntegrationCredentialsSchema
} from "@open-mercato/shared/modules/integrations/types";
import { EncryptionMap } from "../../entities/data/entities.js";
import { IntegrationCredentials } from "../data/entities.js";
const ENCRYPTED_CREDENTIALS_BLOB_KEY = "__om_encrypted_credentials_blob_v1";
class CredentialsEncryptionUnavailableError extends Error {
  constructor(tenantId) {
    super(
      `Cannot encrypt or decrypt integration credentials for tenant ${tenantId}: no tenant DEK is available. Configure Vault (VAULT_ADDR/VAULT_TOKEN) or set TENANT_DATA_ENCRYPTION_FALLBACK_KEY in the environment.`
    );
    this.code = "CREDENTIALS_ENCRYPTION_UNAVAILABLE";
    this.name = "CredentialsEncryptionUnavailableError";
  }
}
function isCredentialsEncryptionUnavailableError(error) {
  return error instanceof CredentialsEncryptionUnavailableError;
}
function isRecordValue(value) {
  return !!value && typeof value === "object" && !Array.isArray(value);
}
function normalizeCredentialsRecord(value) {
  if (isRecordValue(value)) return value;
  if (typeof value !== "string") return {};
  const parsed = parseDecryptedFieldValue(value);
  return isRecordValue(parsed) ? parsed : {};
}
function buildCredentialsFilter(integrationId, scope) {
  const base = {
    integrationId,
    organizationId: scope.organizationId,
    tenantId: scope.tenantId,
    deletedAt: null
  };
  if (scope.userId) {
    base.userId = scope.userId;
  } else {
    base.userId = null;
  }
  return base;
}
function createCredentialsService(em) {
  const credentialsEncryptionSpec = [{ field: "credentials" }];
  async function ensureCredentialsEncryptionMap(scope) {
    const existing = await findOneWithDecryption(
      em,
      EncryptionMap,
      {
        entityId: "integrations:integration_credentials",
        tenantId: scope.tenantId,
        organizationId: scope.organizationId,
        deletedAt: null
      },
      void 0,
      scope
    );
    if (!existing) {
      const created = em.create(EncryptionMap, {
        entityId: "integrations:integration_credentials",
        tenantId: scope.tenantId,
        organizationId: scope.organizationId,
        fieldsJson: credentialsEncryptionSpec,
        isActive: true,
        createdAt: /* @__PURE__ */ new Date(),
        updatedAt: /* @__PURE__ */ new Date()
      });
      em.persist(created);
      return;
    }
    existing.fieldsJson = credentialsEncryptionSpec;
    existing.isActive = true;
  }
  async function resolveCredentialsDek(scope) {
    const kms = createKmsService();
    const existing = await kms.getTenantDek(scope.tenantId);
    if (existing?.key) return existing.key;
    const created = await kms.createTenantDek(scope.tenantId);
    if (created?.key) return created.key;
    throw new CredentialsEncryptionUnavailableError(scope.tenantId);
  }
  async function encryptCredentialsBlob(credentials, scope) {
    const dek = await resolveCredentialsDek(scope);
    const payload = encryptWithAesGcm(JSON.stringify(credentials), dek);
    return { [ENCRYPTED_CREDENTIALS_BLOB_KEY]: payload.value };
  }
  async function decryptCredentialsBlob(credentialsInput, scope) {
    const credentials = normalizeCredentialsRecord(credentialsInput);
    const encrypted = credentials[ENCRYPTED_CREDENTIALS_BLOB_KEY];
    if (typeof encrypted !== "string" || !encrypted) return credentials;
    const dek = await resolveCredentialsDek(scope);
    const decryptedRaw = decryptWithAesGcm(encrypted, dek);
    if (!decryptedRaw) return {};
    try {
      const parsed = JSON.parse(decryptedRaw);
      return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
    } catch {
      return {};
    }
  }
  return {
    async getRaw(integrationId, scope) {
      let row = await findOneWithDecryption(
        em,
        IntegrationCredentials,
        buildCredentialsFilter(integrationId, scope),
        void 0,
        scope
      );
      if (!row && scope.userId) {
        row = await findOneWithDecryption(
          em,
          IntegrationCredentials,
          buildCredentialsFilter(integrationId, { ...scope, userId: null }),
          void 0,
          scope
        );
      }
      if (!row) return null;
      return decryptCredentialsBlob(row.credentials, scope);
    },
    async getRowUpdatedAt(integrationId, scope) {
      let row = await findOneWithDecryption(
        em,
        IntegrationCredentials,
        buildCredentialsFilter(integrationId, scope),
        void 0,
        scope
      );
      if (!row && scope.userId) {
        row = await findOneWithDecryption(
          em,
          IntegrationCredentials,
          buildCredentialsFilter(integrationId, { ...scope, userId: null }),
          void 0,
          scope
        );
      }
      return row?.updatedAt ?? null;
    },
    /**
     * Resolve the persisted `updated_at` version for the credentials a caller
     * would read via {@link resolve} (direct row first, then the bundle
     * fallthrough). Returns `null` when no credentials row exists yet — the
     * optimistic-lock guard treats a missing current version as "no conflict".
     */
    async resolveUpdatedAt(integrationId, scope) {
      const direct = await this.getRowUpdatedAt(integrationId, scope);
      if (direct) return direct;
      const definition = getIntegration(integrationId);
      if (!definition?.bundleId) return null;
      return this.getRowUpdatedAt(definition.bundleId, scope);
    },
    async resolve(integrationId, scope) {
      const direct = await this.getRaw(integrationId, scope);
      if (direct) return direct;
      const definition = getIntegration(integrationId);
      if (!definition?.bundleId) return null;
      return this.getRaw(definition.bundleId, scope);
    },
    async save(integrationId, credentials, scope) {
      const encryptedCredentials = await encryptCredentialsBlob(credentials, scope);
      await ensureCredentialsEncryptionMap(scope);
      const row = await findOneWithDecryption(
        em,
        IntegrationCredentials,
        buildCredentialsFilter(integrationId, scope),
        void 0,
        scope
      );
      if (row) {
        row.credentials = encryptedCredentials;
        await em.flush();
        return;
      }
      const created = em.create(IntegrationCredentials, {
        integrationId,
        credentials: encryptedCredentials,
        organizationId: scope.organizationId,
        tenantId: scope.tenantId,
        ...scope.userId ? { userId: scope.userId } : {}
      });
      await em.persist(created).flush();
    },
    async saveField(integrationId, fieldKey, value, scope) {
      const current = await this.getRaw(integrationId, scope) ?? {};
      const updated = { ...current, [fieldKey]: value };
      await this.save(integrationId, updated, scope);
      return updated;
    },
    getSchema(integrationId) {
      const definition = getIntegration(integrationId);
      if (!definition) return void 0;
      if (definition.bundleId) {
        const bundle = getBundle(definition.bundleId);
        return bundle?.credentials ?? resolveIntegrationCredentialsSchema(integrationId);
      }
      return definition.credentials ?? resolveIntegrationCredentialsSchema(integrationId);
    }
  };
}
export {
  CredentialsEncryptionUnavailableError,
  buildCredentialsFilter,
  createCredentialsService,
  isCredentialsEncryptionUnavailableError
};
//# sourceMappingURL=credentials-service.js.map
