import { randomBytes } from "node:crypto";
import { hash, compare } from "bcryptjs";
import { ApiKey } from "../data/entities.js";
import { createKmsService } from "@open-mercato/shared/lib/encryption/kms";
import { encryptWithAesGcm, decryptWithAesGcm } from "@open-mercato/shared/lib/encryption/aes";
import { getSharedApiKeyAuthCache } from "@open-mercato/shared/lib/auth/apiKeyAuthCache";
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("api_keys").child({ component: "api-key-service" });
const BCRYPT_COST = 10;
async function encryptSessionSecret(secret, tenantId) {
  if (!tenantId) return null;
  const kms = createKmsService();
  if (!kms.isHealthy()) return null;
  const dek = await kms.getTenantDek(tenantId);
  if (!dek) {
    const created = await kms.createTenantDek(tenantId);
    if (!created) return null;
    const encrypted2 = encryptWithAesGcm(secret, created.key);
    return encrypted2.value;
  }
  const encrypted = encryptWithAesGcm(secret, dek.key);
  return encrypted.value;
}
async function decryptSessionSecret(encrypted, tenantId) {
  if (!tenantId || !encrypted) return null;
  const kms = createKmsService();
  if (!kms.isHealthy()) return null;
  const dek = await kms.getTenantDek(tenantId);
  if (!dek) return null;
  return decryptWithAesGcm(encrypted, dek.key);
}
function generateApiKeySecret() {
  const short = randomBytes(4).toString("hex");
  const body = randomBytes(24).toString("hex");
  const secret = `omk_${short}.${body}`;
  const prefix = secret.slice(0, 12);
  return { secret, prefix };
}
async function hashApiKey(secret) {
  return hash(secret, BCRYPT_COST);
}
async function verifyApiKey(secret, keyHash) {
  return compare(secret, keyHash);
}
async function createApiKey(em, input, opts = {}) {
  const { secret, prefix } = generateApiKeySecret();
  const keyHash = await hashApiKey(secret);
  const record = em.create(ApiKey, {
    name: input.name,
    description: input.description ?? null,
    tenantId: input.tenantId ?? null,
    organizationId: input.organizationId ?? null,
    keyHash,
    keyPrefix: prefix,
    rolesJson: Array.isArray(input.roles) ? input.roles : [],
    createdBy: input.createdBy ?? null,
    expiresAt: input.expiresAt ?? null,
    createdAt: /* @__PURE__ */ new Date()
  });
  await em.persist(record).flush();
  if (opts.rbac) {
    await opts.rbac.invalidateUserCache(`api_key:${record.id}`);
  }
  return { record, secret };
}
async function deleteApiKey(em, id, opts = {}) {
  const record = await em.findOne(ApiKey, { id });
  if (!record) return;
  record.deletedAt = /* @__PURE__ */ new Date();
  await em.persist(record).flush();
  getSharedApiKeyAuthCache().invalidateByKeyId(record.id);
  if (opts.rbac) {
    await opts.rbac.invalidateUserCache(`api_key:${record.id}`);
  }
}
async function findApiKeyBySecret(em, secret) {
  if (!secret) return null;
  const prefix = secret.slice(0, 12);
  const candidates = await em.find(ApiKey, { keyPrefix: prefix, deletedAt: null });
  for (const candidate of candidates) {
    if (candidate.expiresAt && candidate.expiresAt.getTime() < Date.now()) continue;
    const isValid = await verifyApiKey(secret, candidate.keyHash);
    if (isValid) return candidate;
  }
  return null;
}
function generateSessionToken() {
  return `sess_${randomBytes(16).toString("hex")}`;
}
async function createSessionApiKey(em, input) {
  const { secret, prefix } = generateApiKeySecret();
  const ttl = input.ttlMinutes ?? 30;
  const expiresAt = new Date(Date.now() + ttl * 60 * 1e3);
  const keyHash = await hashApiKey(secret);
  const encryptedSecret = await encryptSessionSecret(secret, input.tenantId ?? null);
  const record = em.create(ApiKey, {
    name: `__session_${input.sessionToken}__`,
    description: "Ephemeral session API key for AI chat",
    tenantId: input.tenantId ?? null,
    organizationId: input.organizationId ?? null,
    keyHash,
    keyPrefix: prefix,
    rolesJson: input.userRoles,
    createdBy: input.userId,
    sessionToken: input.sessionToken,
    sessionUserId: input.userId,
    sessionSecretEncrypted: encryptedSecret,
    expiresAt,
    createdAt: /* @__PURE__ */ new Date()
  });
  await em.persist(record).flush();
  return {
    keyId: record.id,
    secret,
    sessionToken: input.sessionToken
  };
}
async function findApiKeyBySessionToken(em, sessionToken) {
  if (!sessionToken) return null;
  const record = await em.findOne(ApiKey, {
    sessionToken,
    deletedAt: null
  });
  if (!record) return null;
  if (record.expiresAt && record.expiresAt.getTime() < Date.now()) return null;
  return record;
}
async function bindOpencodeSessionToApiKey(em, sessionToken, opencodeSessionId) {
  if (!sessionToken) throw new Error("Session token not found or expired");
  if (!opencodeSessionId) throw new Error("OpenCode session id is required");
  const row = await findApiKeyBySessionToken(em, sessionToken);
  if (!row) throw new Error("Session token not found or expired");
  if (row.opencodeSessionId === opencodeSessionId) return;
  if (row.opencodeSessionId && row.opencodeSessionId !== opencodeSessionId) {
    throw new Error("Session token already bound to a different OpenCode session");
  }
  row.opencodeSessionId = opencodeSessionId;
  await em.persist(row).flush();
}
async function findApiKeyByOpencodeSessionId(em, opencodeSessionId) {
  if (!opencodeSessionId) return null;
  const record = await findOneWithDecryption(
    em,
    ApiKey,
    { opencodeSessionId, deletedAt: null }
  );
  if (!record) return null;
  if (record.expiresAt && record.expiresAt.getTime() < Date.now()) return null;
  return record;
}
async function findSessionApiKeyWithSecret(em, sessionToken) {
  const record = await findApiKeyBySessionToken(em, sessionToken);
  if (!record) return null;
  if (!record.sessionSecretEncrypted) {
    logger.warn("Session key has no encrypted secret", { apiKeyId: record.id });
    return null;
  }
  const secret = await decryptSessionSecret(record.sessionSecretEncrypted, record.tenantId ?? null);
  if (!secret) {
    logger.warn("Failed to decrypt session secret", { apiKeyId: record.id });
    return null;
  }
  return { key: record, secret };
}
async function deleteSessionApiKey(em, sessionToken) {
  const record = await em.findOne(ApiKey, { sessionToken, deletedAt: null });
  if (!record) return;
  record.deletedAt = /* @__PURE__ */ new Date();
  await em.persist(record).flush();
  getSharedApiKeyAuthCache().invalidateByKeyId(record.id);
}
const ONETIME_KEY_MAX_TTL_MS = 5 * 60 * 1e3;
async function withOnetimeApiKey(em, input, fn) {
  const maxExpiresAt = new Date(Date.now() + ONETIME_KEY_MAX_TTL_MS);
  const safeExpiresAt = input.expiresAt && input.expiresAt < maxExpiresAt ? input.expiresAt : maxExpiresAt;
  const { record, secret } = await createApiKey(em, {
    ...input,
    name: input.name || "__onetime__",
    description: input.description || "One-time API key",
    expiresAt: safeExpiresAt
  });
  try {
    const result = await fn(secret);
    return result;
  } finally {
    try {
      record.deletedAt = /* @__PURE__ */ new Date();
      await em.persist(record).flush();
      getSharedApiKeyAuthCache().invalidateByKeyId(record.id);
    } catch (error) {
      logger.error("Failed to soft-delete one-time API key", { err: error });
    }
  }
}
export {
  bindOpencodeSessionToApiKey,
  createApiKey,
  createSessionApiKey,
  deleteApiKey,
  deleteSessionApiKey,
  findApiKeyByOpencodeSessionId,
  findApiKeyBySecret,
  findApiKeyBySessionToken,
  findSessionApiKeyWithSecret,
  generateApiKeySecret,
  generateSessionToken,
  hashApiKey,
  verifyApiKey,
  withOnetimeApiKey
};
//# sourceMappingURL=apiKeyService.js.map
