// SPDX-License-Identifier: AGPL-3.0-or-later

import type {UserID} from '../../BrandedTypes';
import {BatchBuilder, fetchMany, fetchManyInChunks, fetchOne, upsertOne} from '../../database/CassandraQueryExecution';
import {Db} from '../../database/CassandraTypes';
import type {AdminApiKeyRow} from '../../database/types/AdminAuthTypes';
import {AdminApiKey} from '../../models/AdminApiKey';
import {AdminApiKeys, AdminApiKeysByCreator} from '../../Tables';
import {hashPassword} from '../../utils/PasswordUtils';
import type {CreateAdminApiKeyData, IAdminApiKeyRepository} from './IAdminApiKeyRepository';

function computeTtlSeconds(expiresAt: Date): number {
	const diffSeconds = Math.floor((expiresAt.getTime() - Date.now()) / 1000);
	return Math.max(diffSeconds, 1);
}

export class AdminApiKeyRepository implements IAdminApiKeyRepository {
	async create(data: CreateAdminApiKeyData, createdBy: UserID, keyId: bigint, rawKey: string): Promise<AdminApiKey> {
		const keyHash = await hashPassword(rawKey);
		const createdAt = new Date();
		const row: AdminApiKeyRow = {
			key_id: keyId,
			key_hash: keyHash,
			name: data.name,
			created_by_user_id: createdBy,
			created_at: createdAt,
			last_used_at: null,
			expires_at: data.expiresAt,
			version: 1,
			acls: data.acls,
		};
		const batch = new BatchBuilder();
		if (data.expiresAt) {
			const ttlSeconds = computeTtlSeconds(data.expiresAt);
			batch.addPrepared(AdminApiKeys.insertWithTtl(row, ttlSeconds));
			batch.addPrepared(
				AdminApiKeysByCreator.insertWithTtl(
					{
						created_by_user_id: row.created_by_user_id,
						key_id: row.key_id,
						created_at: row.created_at,
						name: row.name,
						expires_at: row.expires_at,
						last_used_at: row.last_used_at,
						version: row.version,
						acls: row.acls,
					},
					ttlSeconds,
				),
			);
		} else {
			batch.addPrepared(AdminApiKeys.upsertAll(row));
			batch.addPrepared(
				AdminApiKeysByCreator.upsertAll({
					created_by_user_id: row.created_by_user_id,
					key_id: row.key_id,
					created_at: row.created_at,
					name: row.name,
					expires_at: row.expires_at,
					last_used_at: row.last_used_at,
					version: row.version,
					acls: row.acls,
				}),
			);
		}
		await batch.execute();
		return new AdminApiKey(row);
	}

	async findById(keyId: bigint): Promise<AdminApiKey | null> {
		const query = AdminApiKeys.select({
			where: AdminApiKeys.where.eq('key_id'),
			limit: 1,
		});
		const row = await fetchOne<AdminApiKeyRow>(query.bind({key_id: keyId}));
		if (!row) {
			return null;
		}
		return new AdminApiKey(row);
	}

	async listByCreator(createdBy: UserID): Promise<Array<AdminApiKey>> {
		const query = AdminApiKeysByCreator.select({
			where: AdminApiKeysByCreator.where.eq('created_by_user_id'),
		});
		const indexRows = await fetchMany<{
			created_by_user_id: UserID;
			key_id: bigint;
		}>(query.bind({created_by_user_id: createdBy}));
		if (indexRows.length === 0) {
			return [];
		}
		const keyIds = indexRows.map((row) => row.key_id);
		const rows = await fetchManyInChunks<AdminApiKeyRow>(
			AdminApiKeys.selectCql({where: AdminApiKeys.where.in('key_id', 'key_ids')}),
			keyIds,
			(chunk) => ({key_ids: chunk}),
		);
		return rows.map((row) => new AdminApiKey(row));
	}

	async updateLastUsed(keyId: bigint, expiresAt: Date | null): Promise<void> {
		if (expiresAt) {
			const remainingTtl = Math.max(Math.floor((expiresAt.getTime() - Date.now()) / 1000), 1);
			await upsertOne(AdminApiKeys.patchByPkWithTtl({key_id: keyId}, {last_used_at: Db.set(new Date())}, remainingTtl));
		} else {
			await upsertOne(AdminApiKeys.patchByPk({key_id: keyId}, {last_used_at: Db.set(new Date())}));
		}
	}

	async revoke(keyId: bigint, createdBy: UserID): Promise<void> {
		const batch = new BatchBuilder();
		batch.addPrepared(AdminApiKeys.deleteByPk({key_id: keyId}));
		batch.addPrepared(AdminApiKeysByCreator.deleteByPk({created_by_user_id: createdBy, key_id: keyId}));
		await batch.execute();
	}
}
