/**
 * BackupJob — automated backup for SSO PostgreSQL database.
 *
 * Two-layer backup strategy:
 *   Layer 1 — Full base backup: runs via pg_basebackup every 10 minutes.
 *              On the dedicated backup Postgres container (sso-postgres-backup).
 *   Layer 2 — WAL archiving: Patroni streams WAL segments continuously.
 *              Archived to volume postgres{N}-wal on each Patroni node.
 *
 * Architecture (ADR-009):
 *   Patroni primary → WAL segments → volume mount → backup container
 *   Backup container: pg_basebackup every 10 minutes from the primary
 *
 * Audit events are emitted for: backup started, backup completed, backup failed.
 *
 * Phase 3d (PLANS.md)
 */

import { LoggingService } from '#services/file-system/logService';
import { createAuditStrategy } from '#services/audit/strategies/auditStrategyFactoryService.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';

const execAsync = promisify(exec);
const logger = new LoggingService();
const auditStrategy = createAuditStrategy(process.env.AUDIT_MODE ?? 'direct');

export interface BackupResult {
	success: boolean;
	backupPath?: string;
	durationMs: number;
	sizeMb?: number;
	error?: string;
}

export class BackupJob {
	/**
	 * Run a full base backup using pg_basebackup.
	 *
	 * Target: the dedicated backup Postgres container (sso-postgres-backup in docker-compose.yml).
	 * The backup container runs PostgreSQL and receives WAL streams from the Patroni primary.
	 *
	 * In production: run pg_basebackup from the backup server against the primary's replication slot.
	 *
	 * @param backupLabel Custom label for the backup (e.g. timestamp)
	 */
	static async runBaseBackup(backupLabel?: string): Promise<BackupResult> {
		const start = Date.now();
		const label = backupLabel ?? new Date().toISOString().replace(/[:.]/g, '-');
		const backupPath = `/var/lib/postgresql/backups/${label}`;

		void logger.logAsync('info', 'BackupJob', `Starting base backup: ${backupPath}`, null);

		await this.emitEvent('BACKUP_STARTED', {
			backupPath,
			label,
			trigger: 'scheduled',
		});

		try {
			// pg_basebackup connects as replication user to the primary and copies the data dir.
			// In the Docker HA setup, the backup container syncs via streaming replication.
			// For cron-based backups, we run pg_basebackup from the backup container.
			//
			// Command breakdown:
			//   -D <path>       — output directory
			//   -R               — create recovery.conf (sets up replication)
			//   -X stream       — include WAL files via streaming
			//   -P               — show progress
			//   -U replicator    — replication user
			//   -h <host>       — primary host
			const host = process.env.PG_BACKUP_HOST ?? 'sso-postgres-backup';
			const user = process.env.PG_BACKUP_USER ?? 'replicator';
			const password = process.env.PG_BACKUP_PASSWORD ?? 'repl-password-replace-in-prod';

			const cmd = [
				'pg_basebackup',
				`-D ${backupPath}`,
				'-R',
				'-X stream',
				'-P',
				`-U ${user}`,
				`-h ${host}`,
				`-w`,  // no prompt
			].join(' ');

			const { stdout, stderr } = await execAsync(cmd, {
				env: { ...process.env, PGPASSWORD: password },
				timeout: 600_000, // 10 minutes max
			});

			const durationMs = Date.now() - start;

			// Get backup size
			let sizeMb: number | undefined;
			try {
				const sizeOutput = await execAsync(`du -sm ${backupPath} 2>/dev/null || echo "0"`, {
					timeout: 10_000,
				});
				const sizeStr = sizeOutput.stdout.trim().split('\t')[0] ?? '0';
				const parsed = parseInt(sizeStr, 10);
				sizeMb = Number.isNaN(parsed) ? undefined : parsed;
			} catch {
				// Size check is best-effort
			}

			void logger.logAsync('info', 'BackupJob', `Base backup completed: ${backupPath} (${sizeMb}MB, ${durationMs}ms)`, null);
			void logger.logAsync('debug', 'BackupJob', `pg_basebackup stdout: ${stdout}`, null);

			await this.emitEvent('BACKUP_COMPLETED', {
				backupPath,
				label,
				durationMs,
				sizeMb,
			});

			return {
				success: true as const,
				backupPath,
				durationMs,
				...(sizeMb !== undefined ? { sizeMb } : {}),
			};
		} catch (err) {
			const durationMs = Date.now() - start;
			const error = err instanceof Error ? err.message : String(err);

			void logger.logAsync('error', 'BackupJob', `Base backup failed: ${error}`, null);

			await this.emitEvent('BACKUP_FAILED', {
				backupPath,
				label,
				durationMs,
				error,
			});

			return { success: false, backupPath, durationMs, error };
		}
	}

	/**
	 * Verify backup integrity by running pg_checksums on the backup data directory.
	 * Call after runBaseBackup() to ensure the backup is valid.
	 *
	 * @param backupPath Path to the backup data directory
	 */
	static async verifyBackup(backupPath: string): Promise<{ valid: boolean; error?: string }> {
		try {
			await execAsync(`pg_checksums -D ${backupPath} --check`, {
				timeout: 120_000,
			});
			void logger.logAsync('info', 'BackupJob', `Backup verification passed: ${backupPath}`, null);
			return { valid: true };
		} catch (err) {
			const error = err instanceof Error ? err.message : String(err);
			void logger.logAsync('error', 'BackupJob', `Backup verification failed: ${error}`, null);
			return { valid: false, error };
		}
	}

	/**
	 * Cleanup old backups, keeping the last N backups.
	 *
	 * @param backupDir Directory containing backups
	 * @param keep Number of backups to keep (default: 10)
	 */
	static async cleanupOldBackups(backupDir: string, keep = 10): Promise<number> {
		try {
			const { stdout } = await execAsync(
				`ls -1t ${backupDir} 2>/dev/null | tail -n +${keep + 1} | wc -l`,
				{ timeout: 10_000 },
			);
			const count = parseInt(stdout.trim(), 10);
			if (count === 0) return 0;

			// Delete old backup directories (keep the newest N)
			const dirsToDelete = await execAsync(
				`ls -1t ${backupDir} 2>/dev/null | tail -n +${keep + 1}`,
				{ timeout: 10_000 },
			);

			let deleted = 0;
			for (const dir of dirsToDelete.stdout.trim().split('\n').filter(Boolean)) {
				const fullPath = path.join(backupDir, dir);
				await execAsync(`rm -rf ${fullPath}`, { timeout: 60_000 });
				deleted++;
			}

			void logger.logAsync('info', 'BackupJob', `Cleaned up ${deleted} old backups (keeping ${keep})`, null);
			return deleted;
		} catch {
			return 0;
		}
	}

	private static async emitEvent(
		eventType: string,
		metadata: Record<string, unknown>,
	): Promise<void> {
		try {
			await auditStrategy.logSystemAudit({
				action: eventType,
				module: 'backup',
				actor_name: 'system',
				actor_id: 'backup_job',
				severity: eventType === 'BACKUP_FAILED' ? 'HIGH' : 'LOW',
				metadata,
			});
		} catch {
			// Never throw from event emission
		}
	}
}
