import { eq } from "drizzle-orm";
import { generateOpaqueToken, hashToken, verifyToken } from "@platform-modules/util/tokens";
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
//#region src/api-keys/errors.ts
var InvalidApiKeyOwnerError = class extends Error {
	owner;
	name = "InvalidApiKeyOwnerError";
	code = "INVALID_API_KEY_OWNER";
	constructor(owner) {
		super("issueApiKey: owner must be a non-empty string");
		this.owner = owner;
	}
};
var InvalidApiKeyScopesError = class extends Error {
	name = "InvalidApiKeyScopesError";
	code = "INVALID_API_KEY_SCOPES";
	constructor() {
		super("issueApiKey: scopes must contain only non-empty strings");
	}
};
var ApiKeyOwnerMismatchError = class extends Error {
	owner;
	issuedByOwner;
	name = "ApiKeyOwnerMismatchError";
	code = "API_KEY_OWNER_MISMATCH";
	constructor(owner, issuedByOwner) {
		super("issueApiKey: owner must match the authenticated issuer");
		this.owner = owner;
		this.issuedByOwner = issuedByOwner;
	}
};
var ApiKeyRevokedError = class extends Error {
	id;
	name = "ApiKeyRevokedError";
	code = "API_KEY_REVOKED";
	constructor(id) {
		super("verifyApiKey: key is revoked");
		this.id = id;
	}
};
var ApiKeyExpiredError = class extends Error {
	id;
	name = "ApiKeyExpiredError";
	code = "API_KEY_EXPIRED";
	constructor(id) {
		super("verifyApiKey: key is expired");
		this.id = id;
	}
};
var ApiKeyLastUsedAtUpdateError = class extends Error {
	id;
	cause;
	name = "ApiKeyLastUsedAtUpdateError";
	code = "API_KEY_LAST_USED_AT_UPDATE_FAILED";
	constructor(id, cause) {
		super("verifyApiKey: lastUsedAt update failed");
		this.id = id;
		this.cause = cause;
	}
};
function isInvalidApiKeyOwnerError(error) {
	return typeof error === "object" && error !== null && error.name === "InvalidApiKeyOwnerError" && error.code === "INVALID_API_KEY_OWNER";
}
function isInvalidApiKeyScopesError(error) {
	return typeof error === "object" && error !== null && error.name === "InvalidApiKeyScopesError" && error.code === "INVALID_API_KEY_SCOPES";
}
function isApiKeyOwnerMismatchError(error) {
	return typeof error === "object" && error !== null && error.name === "ApiKeyOwnerMismatchError" && error.code === "API_KEY_OWNER_MISMATCH";
}
function isApiKeyRevokedError(error) {
	return typeof error === "object" && error !== null && error.name === "ApiKeyRevokedError" && error.code === "API_KEY_REVOKED";
}
function isApiKeyExpiredError(error) {
	return typeof error === "object" && error !== null && error.name === "ApiKeyExpiredError" && error.code === "API_KEY_EXPIRED";
}
function isApiKeyLastUsedAtUpdateError(error) {
	return typeof error === "object" && error !== null && error.name === "ApiKeyLastUsedAtUpdateError" && error.code === "API_KEY_LAST_USED_AT_UPDATE_FAILED";
}
function isApiKeyError(error) {
	return isInvalidApiKeyOwnerError(error) || isInvalidApiKeyScopesError(error) || isApiKeyOwnerMismatchError(error) || isApiKeyRevokedError(error) || isApiKeyExpiredError(error) || isApiKeyLastUsedAtUpdateError(error);
}
//#endregion
//#region src/api-keys/schema.ts
const apiKeys = pgTable("api_keys", {
	id: text("id").primaryKey(),
	prefix: text("prefix").notNull().unique(),
	secretHash: text("secret_hash").notNull(),
	owner: text("owner").notNull(),
	scopes: text("scopes").notNull(),
	createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
	lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
	revokedAt: timestamp("revoked_at", { withTimezone: true }),
	expiresAt: timestamp("expires_at", { withTimezone: true })
});
const apiKeysCreationSql = () => `CREATE TABLE IF NOT EXISTS api_keys (
    id text PRIMARY KEY,
    prefix text NOT NULL UNIQUE,
    secret_hash text NOT NULL,
    owner text NOT NULL,
    scopes text NOT NULL,
    created_at timestamptz NOT NULL,
    last_used_at timestamptz,
    revoked_at timestamptz,
    expires_at timestamptz
  )`;
const apiKeysSchema = { apiKeys };
//#endregion
//#region src/api-keys/index.ts
const LAST_USED_THROTTLE_MS = 36e5;
const PREFIX_BYTES = 9;
const SECRET_BYTES = 32;
const MAX_ISSUE_ATTEMPTS = 5;
let dummySecretHash = null;
function normalizeOwner(owner) {
	if (typeof owner !== "string" || owner.trim().length === 0) throw new InvalidApiKeyOwnerError(owner);
	return owner;
}
function normalizeScopes(scopes) {
	if (!Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string" || scope.trim().length === 0)) throw new InvalidApiKeyScopesError();
	return [...scopes];
}
function parsePresentedKey(presented) {
	if (typeof presented !== "string") return null;
	const dot = presented.indexOf(".");
	if (dot <= 0 || dot !== presented.lastIndexOf(".") || dot === presented.length - 1) return null;
	return {
		prefix: presented.slice(0, dot),
		secret: presented.slice(dot + 1)
	};
}
function parseScopes(raw) {
	try {
		const parsed = JSON.parse(raw);
		if (!Array.isArray(parsed) || parsed.some((scope) => typeof scope !== "string")) return null;
		return [...parsed];
	} catch {
		return null;
	}
}
function getDummySecretHash() {
	dummySecretHash ??= hashToken("api-key-dummy-secret");
	return dummySecretHash;
}
function isUniqueConflict(error) {
	if (typeof error !== "object" || error === null) return false;
	if (error.code === "23505") return true;
	const message = error.message;
	return typeof message === "string" && /unique|duplicate/i.test(message);
}
async function issueApiKey(db, input) {
	const owner = normalizeOwner(input.owner);
	const issuedByOwner = normalizeOwner(input.issuedBy.owner);
	if (owner !== issuedByOwner) throw new ApiKeyOwnerMismatchError(owner, issuedByOwner);
	const scopes = normalizeScopes(input.scopes);
	const expiresAt = input.expiresAt ?? null;
	for (let attempt = 0; attempt < MAX_ISSUE_ATTEMPTS; attempt++) {
		const id = crypto.randomUUID();
		const prefix = generateOpaqueToken(PREFIX_BYTES);
		const secret = generateOpaqueToken(SECRET_BYTES);
		const secretHash = await hashToken(secret);
		const now = /* @__PURE__ */ new Date();
		try {
			await db.insert(apiKeys).values({
				id,
				prefix,
				secretHash,
				owner,
				scopes: JSON.stringify(scopes),
				createdAt: now,
				lastUsedAt: null,
				revokedAt: null,
				expiresAt
			});
			return {
				id,
				key: `${prefix}.${secret}`
			};
		} catch (error) {
			if (!isUniqueConflict(error) || attempt === 4) throw error;
		}
	}
	throw new Error("issueApiKey: exhausted prefix generation attempts");
}
async function verifyApiKey(db, presented, options = {}) {
	const parsed = parsePresentedKey(presented);
	if (!parsed) return null;
	const [row] = await db.select({
		id: apiKeys.id,
		owner: apiKeys.owner,
		scopes: apiKeys.scopes,
		secretHash: apiKeys.secretHash,
		lastUsedAt: apiKeys.lastUsedAt,
		revokedAt: apiKeys.revokedAt,
		expiresAt: apiKeys.expiresAt
	}).from(apiKeys).where(eq(apiKeys.prefix, parsed.prefix)).limit(1);
	const secretMatches = await verifyToken(parsed.secret, row?.secretHash ?? await getDummySecretHash());
	if (!row || !secretMatches) return null;
	const now = options.now ?? /* @__PURE__ */ new Date();
	if (row.revokedAt) throw new ApiKeyRevokedError(row.id);
	if (row.expiresAt && new Date(row.expiresAt).getTime() <= now.getTime()) throw new ApiKeyExpiredError(row.id);
	const scopes = parseScopes(row.scopes);
	if (!scopes) return null;
	const lastUsedAt = row.lastUsedAt ? new Date(row.lastUsedAt).getTime() : 0;
	if (now.getTime() - lastUsedAt >= LAST_USED_THROTTLE_MS) try {
		await db.update(apiKeys).set({ lastUsedAt: now }).where(eq(apiKeys.id, row.id));
	} catch (error) {
		options.onLastUsedAtUpdateError?.(new ApiKeyLastUsedAtUpdateError(row.id, error));
	}
	return {
		id: row.id,
		owner: row.owner,
		scopes
	};
}
//#endregion
export { ApiKeyExpiredError, ApiKeyLastUsedAtUpdateError, ApiKeyOwnerMismatchError, ApiKeyRevokedError, InvalidApiKeyOwnerError, InvalidApiKeyScopesError, apiKeys, apiKeysCreationSql, apiKeysSchema, isApiKeyError, isApiKeyExpiredError, isApiKeyLastUsedAtUpdateError, isApiKeyOwnerMismatchError, isApiKeyRevokedError, isInvalidApiKeyOwnerError, isInvalidApiKeyScopesError, issueApiKey, verifyApiKey };
