/**
 * System cleanup
 *
 * Runs periodic maintenance tasks that prevent unbounded accumulation of
 * expired or stale data. Called from cron scheduler ticks and (for latency-
 * sensitive subsystems) inline during relevant requests.
 *
 * Each subsystem cleanup is independent and non-fatal -- if one fails, the
 * rest still run. Failures are logged but never surface to callers.
 */

import { createKyselyAdapter, type AuthTables } from "@emdash-cms/auth/adapters/kysely";
import { sql, type Kysely } from "kysely";

import { cleanupExpiredChallenges } from "./auth/challenge-store.js";
import { MediaRepository } from "./database/repositories/media.js";
import { RevisionRepository } from "./database/repositories/revision.js";
import type { Database } from "./database/types.js";
import type { Storage } from "./storage/types.js";

/**
 * Result of a system cleanup run.
 * Each field is the number of rows deleted, or -1 if the cleanup failed.
 */
export interface CleanupResult {
	challenges: number;
	expiredTokens: number;
	pendingUploads: number;
	pendingUploadFiles: number;
	revisionsPruned: number;
}

/** Max revisions to keep per entry during periodic pruning */
const REVISION_KEEP_COUNT = 50;

/** Only prune entries that exceed this threshold */
const REVISION_PRUNE_THRESHOLD = REVISION_KEEP_COUNT;

/**
 * Run all system cleanup tasks.
 *
 * Safe to call frequently -- each task is a single DELETE with a WHERE clause,
 * so repeated calls with nothing to clean are cheap (no-op queries).
 *
 * @param db - The database instance
 * @param storage - Optional storage backend for deleting orphaned files.
 *   When omitted, pending upload DB rows are still deleted but the
 *   corresponding files in object storage are not removed.
 */
export async function runSystemCleanup(
	db: Kysely<Database>,
	storage?: Storage,
): Promise<CleanupResult> {
	const result: CleanupResult = {
		challenges: -1,
		expiredTokens: -1,
		pendingUploads: -1,
		pendingUploadFiles: -1,
		revisionsPruned: -1,
	};

	// 1. Passkey challenges (expire after 60s, clean anything past 5 min)
	try {
		result.challenges = await cleanupExpiredChallenges(db);
	} catch (error) {
		console.error("[cleanup] Failed to clean expired challenges:", error);
	}

	// 2. Magic link / invite / signup tokens
	try {
		// Cast needed: Database extends AuthTables but uses Generated<> wrappers
		// that confuse structural checks. The adapter casts internally anyway.
		// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Database uses Generated<> wrappers incompatible with AuthTables structurally; safe at runtime
		const authAdapter = createKyselyAdapter(db as unknown as Kysely<AuthTables>);
		await authAdapter.deleteExpiredTokens();
		result.expiredTokens = 0; // deleteExpiredTokens returns void
	} catch (error) {
		console.error("[cleanup] Failed to clean expired tokens:", error);
	}

	// 3. Pending media uploads (abandoned after 1 hour)
	//    Delete DB rows first, then remove corresponding files from storage.
	try {
		const mediaRepo = new MediaRepository(db);
		const orphanedKeys = await mediaRepo.cleanupPendingUploads();
		result.pendingUploads = orphanedKeys.length;

		// Delete orphaned files from object storage
		if (storage && orphanedKeys.length > 0) {
			let filesDeleted = 0;
			for (const key of orphanedKeys) {
				try {
					await storage.delete(key);
					filesDeleted++;
				} catch (error) {
					// Log per-file failures but continue -- storage.delete is
					// documented as idempotent, so this is an unexpected error.
					console.error(`[cleanup] Failed to delete storage file ${key}:`, error);
				}
			}
			result.pendingUploadFiles = filesDeleted;
		} else {
			result.pendingUploadFiles = 0;
		}
	} catch (error) {
		console.error("[cleanup] Failed to clean pending uploads:", error);
	}

	// 4. Revision pruning -- trim entries with excessive revision counts
	try {
		result.revisionsPruned = await pruneExcessiveRevisions(db);
	} catch (error) {
		console.error("[cleanup] Failed to prune revisions:", error);
	}

	return result;
}

/**
 * Find entries with more than REVISION_PRUNE_THRESHOLD revisions and prune
 * them down to REVISION_KEEP_COUNT.
 */
async function pruneExcessiveRevisions(db: Kysely<Database>): Promise<number> {
	const entries = await sql<{ collection: string; entry_id: string; cnt: number }>`
		SELECT collection, entry_id, COUNT(*) as cnt
		FROM revisions
		GROUP BY collection, entry_id
		HAVING cnt > ${REVISION_PRUNE_THRESHOLD}
	`.execute(db);

	if (entries.rows.length === 0) return 0;

	const revisionRepo = new RevisionRepository(db);
	let totalPruned = 0;

	for (const row of entries.rows) {
		try {
			const pruned = await revisionRepo.pruneOldRevisions(
				row.collection,
				row.entry_id,
				REVISION_KEEP_COUNT,
			);
			totalPruned += pruned;
		} catch (error) {
			console.error(
				`[cleanup] Failed to prune revisions for ${row.collection}/${row.entry_id}:`,
				error,
			);
		}
	}

	return totalPruned;
}
