const CLASS_NAME = 'schedule';
import LoggingService from '#services/file-system/logService';
import schedule, { Job } from 'node-schedule';
import path from 'path';
import PartitionManagementService from '#services/database/partition/partitionManagementService';
import { BackupJob } from './jobs/backupJob.js';
import Config from '#config';
import { NotificationMessageProvider } from '#providers/NotificationMessageProvider';
import { NotificationDeliveryAttemptProvider } from '#providers/NotificationDeliveryAttemptProvider';
import NotificationService from '#services/notification/notificationService';
import { NotificationDeviceProvider } from '#providers/NotificationDeviceProvider';
import { PasswordResetTokenProvider } from '#providers/PasswordResetTokenProvider';
import { Op } from 'sequelize';
import { AuditLogMaintenanceJob } from '#jobs/auditLogMaintenanceJob';
import { retireOldKeys } from '#jobs/secretRotation';
import sequelize from '#services/database/sequelize/sequelizeService';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import customParseFormat from 'dayjs/plugin/customParseFormat';
// import { UserProvider } from "../providers/userProvider"; // Uncomment when you have a user provider

dayjs.extend(utc);
dayjs.extend(customParseFormat);

class ScheduleClass {
	logger: LoggingService;
	job: Record<string, Job> = {};
	PasswordResetTokenProvider: PasswordResetTokenProvider;
	NotificationMessageProvider: NotificationMessageProvider;
	notificationDeviceProvider: NotificationDeviceProvider;
	NotificationDeliveryAttemptProvider: NotificationDeliveryAttemptProvider;
	notificationService: NotificationService;
	partitionManagementService: PartitionManagementService;
	billOverdueJob: any;
	// userProvider: UserProvider; // Uncomment when you have a user provider

	constructor() {
		this.logger = new LoggingService();
		this.PasswordResetTokenProvider = new PasswordResetTokenProvider();
		this.NotificationMessageProvider = NotificationMessageProvider.getInstance();
		this.notificationDeviceProvider = NotificationDeviceProvider.getInstance();
		this.NotificationDeliveryAttemptProvider = NotificationDeliveryAttemptProvider.getInstance();
		this.notificationService = NotificationService.getInstance();
		this.partitionManagementService = PartitionManagementService.getInstance();
		this.billOverdueJob = null;
		// this.userProvider = new UserProvider(); // Uncomment when you have a user provider
	}

	init() {
		const METHOD_NAME = 'Init Job Scheduling';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;
		this.logger.logAsync('SCHEDULE', SOURCE, 'Initialize schedule', null);
		this.job['midnight'] = schedule.scheduleJob('0 0 * * *', async function () {});

		this.job['perMin'] = schedule.scheduleJob('* * * * *', async function () {});

		this.job['cleanupPasswordResetCodes'] = schedule.scheduleJob('*/10 * * * *', async () => {
			await this.cleanupExpiredPasswordResetCodes();
		});

		// Notification system jobs
		this.job['cleanupExpiredNotifications'] = schedule.scheduleJob('0 0 * * *', async () => {
			await this.cleanupExpiredNotifications();
		});

		this.job['retryFailedNotifications'] = schedule.scheduleJob('*/5 * * * *', async () => {
			await this.retryFailedNotifications();
		});

		this.job['cleanupInactiveDevices'] = schedule.scheduleJob('0 2 * * 0', async () => {
			await this.cleanupInactiveDevices();
		});

		// Session cleanup — runs every hour via DB stored procedure
		this.job['cleanupExpiredSessions'] = schedule.scheduleJob('0 * * * *', async () => {
			await this.cleanupExpiredSessions();
		});

		// DEBT-HIGH-03: Automated partition cleanup — runs weekly on Sunday at 3 AM
		this.job['cleanupOldPartitions'] = schedule.scheduleJob('0 3 * * 0', async () => {
			await this.cleanupOldPartitions();
		});

		// MISS-01: Create new partitions monthly — runs on the 1st of each month at 2 AM
		this.job['createNewPartitions'] = schedule.scheduleJob('0 2 1 * *', async () => {
			await this.createNewPartitions();
		});

		// Update overdue bills — runs daily at 1 AM
		this.job['updateOverdueBills'] = schedule.scheduleJob('0 1 * * *', async () => {
			await this.updateOverdueBills();
		});

		// Phase 3d — Database backup via pg_basebackup every 10 minutes
		// The backup job runs on the dedicated backup Postgres container.
		// Runs every 10 minutes: */10 * * * *
		// In production: use pg_basebackup on the dedicated backup server against the Patroni primary.
		this.job['databaseBackup'] = schedule.scheduleJob('*/10 * * * *', async () => {
			await this.runDatabaseBackup();
		});

		// Phase 4 — Audit log maintenance (partitions + cleanup) — runs monthly on the 1st at 3 AM
		this.job['auditLogMaintenance'] = schedule.scheduleJob('0 3 1 * *', async () => {
			await this.runAuditLogMaintenance();
		});

		// Phase 4 — Retire old signing keys every hour (after grace period)
		// Rotation is triggered manually via admin API; this only removes keys past deadline.
		this.job['retireOldKeys'] = schedule.scheduleJob('0 * * * *', async () => {
			await this.runRetireOldKeys();
		});
	}

	/**
	 * Cleanup expired password reset codes
	 */
	async cleanupExpiredPasswordResetCodes() {
		const METHOD_NAME = 'cleanupExpiredPasswordResetCodes';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;

		try {
			const now = new Date();

			const allCodes = await this.PasswordResetTokenProvider.getAll({
				pageSize: 1000,
			});

			const expiredCodes = allCodes.rows.filter((code) => {
				const expiresAt = new Date(code.expires_at);
				const isExpired = expiresAt < now;
				return isExpired;
			});

			if (expiredCodes.length === 0) {
				this.logger.logAsync('SCHEDULE', SOURCE, 'No expired codes to cleanup', null);
				return;
			}

			const transaction = await this.PasswordResetTokenProvider.transaction();
			try {
				for (const code of expiredCodes) {
					await this.PasswordResetTokenProvider.delete(code.id, { transaction });
				}
				await transaction.commit();

				const message = `Successfully deleted ${expiredCodes.length} expired tokens`;
				this.logger.logAsync('SCHEDULE', SOURCE, message, null);
			} catch {
				await transaction.rollback();
				this.logger.logAsync('SCHEDULE', SOURCE, 'Failed to delete expired tokens', null);
			}
		} catch (error) {
			console.error(`[SCHEDULE] Error in cleanup job:`, error);
			this.logger.logAsync('SCHEDULE', SOURCE, 'Error in cleanup job', null);
		}
	}

	/**
	 * Cleanup expired notifications (runs daily at midnight)
	 */
	async cleanupExpiredNotifications() {
		const METHOD_NAME = 'cleanupExpiredNotifications';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;

		try {
			const { CleanupJob } = await import('./jobs/cleanupJob.js');
			const deletedCount = await CleanupJob.cleanupOldNotifications();

			if (deletedCount > 0) {
				const message = `Successfully soft deleted ${deletedCount} expired notifications`;
				this.logger.logAsync('SCHEDULE', SOURCE, message, null);
			} else {
				this.logger.logAsync('SCHEDULE', SOURCE, 'No expired notifications to cleanup', null);
			}
		} catch (error) {
			console.error(`[SCHEDULE] Error in cleanupExpiredNotifications job:`, error);
			this.logger.logAsync('SCHEDULE', SOURCE, 'Error in cleanupExpiredNotifications job', null);
		}
	}

	/**
	 * Retry failed notifications (runs every 5 minutes)
	 */
	async retryFailedNotifications() {
		const METHOD_NAME = 'retryFailedNotifications';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;

		try {
			const retriedCount = await this.notificationService.retryFailedNotifications();

			if (retriedCount > 0) {
				const message = `Successfully retried ${retriedCount} failed notifications`;
				this.logger.logAsync('SCHEDULE', SOURCE, message, null);
			} else {
				this.logger.logAsync('SCHEDULE', SOURCE, 'No failed notifications to retry', null);
			}
		} catch (error) {
			console.error(`[SCHEDULE] Error in retryFailedNotifications job:`, error);
			this.logger.logAsync('SCHEDULE', SOURCE, 'Error in retryFailedNotifications job', null);
		}
	}

	/**
	 * Cleanup inactive devices (runs weekly on Sunday at 2 AM)
	 */
	async cleanupInactiveDevices() {
		const METHOD_NAME = 'cleanupInactiveDevices';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;

		try {
			const ninetyDaysAgo = new Date();
			ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);

			const inactiveDevices = await this.notificationDeviceProvider.getAll({
				where: {
					last_used_at: {
						[Op.lt]: ninetyDaysAgo,
					},
					is_active: true,
				},
			});

			if (inactiveDevices.rows.length === 0) {
				this.logger.logAsync('SCHEDULE', SOURCE, 'No inactive devices to cleanup', null);
				return;
			}

			let deactivatedCount = 0;
			const transaction = await this.notificationDeviceProvider.transaction();
			try {
				for (const device of inactiveDevices.rows) {
					await this.notificationDeviceProvider.put(
						device.id,
						{
							is_active: false,
							updated_at: new Date(),
						} as any,
						{ transaction },
					);
					deactivatedCount++;
				}
				await transaction.commit();

				const message = `Successfully deactivated ${deactivatedCount} inactive devices`;
				this.logger.logAsync('SCHEDULE', SOURCE, message, null);
			} catch {
				await transaction.rollback();
				this.logger.logAsync('SCHEDULE', SOURCE, 'Failed to deactivate inactive devices', null);
			}
		} catch (error) {
			console.error(`[SCHEDULE] Error in cleanupInactiveDevices job:`, error);
			this.logger.logAsync('SCHEDULE', SOURCE, 'Error in cleanupInactiveDevices job', null);
		}
	}

	/**
	 * Cleanup expired user sessions (runs every hour).
	 * Delegates to the `cleanup_expired_sessions()` stored procedure defined in migration 001.
	 */
	async cleanupExpiredSessions() {
		const METHOD_NAME = 'cleanupExpiredSessions';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;

		try {
			await sequelize.query('SELECT cleanup_expired_sessions()');
			this.logger.logAsync('SCHEDULE', SOURCE, 'Expired sessions cleaned up successfully', null);
		} catch (error) {
			console.error(`[SCHEDULE] Error in cleanupExpiredSessions job:`, error);
			this.logger.logAsync('SCHEDULE', SOURCE, 'Error in cleanupExpiredSessions job', null);
		}
	}

	/**
	 * Cleanup old database partitions (runs weekly on Sunday at 3 AM).
	 * Retains partitions for the last 12 months by default.
	 */
	async cleanupOldPartitions() {
		const METHOD_NAME = 'cleanupOldPartitions';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;

		try {
			const retainMonths = Config.partition.retainMonths;
			const droppedCount = await this.partitionManagementService.dropOldPartitions(retainMonths);

			if (droppedCount > 0) {
				const message = `Successfully dropped ${droppedCount} old partitions (retaining ${retainMonths} months)`;
				this.logger.logAsync('SCHEDULE', SOURCE, message, null);
			} else {
				this.logger.logAsync('SCHEDULE', SOURCE, 'No old partitions to cleanup', null);
			}
		} catch (error) {
			console.error(`[SCHEDULE] Error in cleanupOldPartitions job:`, error);
			this.logger.logAsync('SCHEDULE', SOURCE, 'Error in cleanupOldPartitions job', null);
		}
	}

	/**
	 * Create new database partitions (runs monthly on the 1st at 2 AM).
	 * Creates partitions for the next month for audit log tables.
	 */
	async createNewPartitions() {
		const METHOD_NAME = 'createNewPartitions';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;

		try {
			// MISS-01: Create partitions for next 3 months to ensure we're always ahead
			await this.partitionManagementService.ensurePartitions(3);
			this.logger.logAsync('SCHEDULE', SOURCE, 'Successfully ensured partitions for next 3 months', null);
		} catch (error) {
			console.error(`[SCHEDULE] Error in createNewPartitions job:`, error);
			this.logger.logAsync('SCHEDULE', SOURCE, 'Error in createNewPartitions job', null);
		}
	}

	/**
	 * Update overdue bills (runs daily at 1 AM).
	 * NOTE: Disabled in SSO — `bills` table was removed during cleanup. The BillOverdueJob
	 * was deleted; callers of this method should be re-pointed to whatever billing service
	 * takes its place (or removed entirely if no billing domain in SSO).
	 */
	async updateOverdueBills() {
		const METHOD_NAME = 'updateOverdueBills';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;

		this.logger.logAsync(
			'SCHEDULE',
			SOURCE,
			'updateOverdueBills is disabled in SSO (Bill model removed); no-op',
			null,
		);
	}

	/**
	 * Run a scheduled database backup via pg_basebackup.
	 * Runs every 10 minutes via the databaseBackup cron job.
	 * Logs result but does not throw on failure — backup failures should not stop the scheduler.
	 */
	async runDatabaseBackup(): Promise<void> {
		const METHOD_NAME = 'runDatabaseBackup';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;

		try {
			const result = await BackupJob.runBaseBackup();
			if (result.success) {
				this.logger.logAsync(
					'SCHEDULE',
					SOURCE,
					`Database backup completed: ${result.backupPath} (${result.sizeMb}MB, ${result.durationMs}ms)`,
					null,
				);
			} else {
				this.logger.logAsync(
					'error',
					SOURCE,
					`Database backup failed: ${result.error} (${result.durationMs}ms)`,
					null,
				);
			}

			// Cleanup old backups — keep last 10
			if (result.success && result.backupPath) {
				const backupDir = path.dirname(result.backupPath);
				await BackupJob.cleanupOldBackups(backupDir, 10);
			}
		} catch (error) {
			// Log but don't throw — backup failure should not crash the scheduler
			console.error(`[SCHEDULE] Error in runDatabaseBackup:`, error);
			this.logger.logAsync('SCHEDULE', SOURCE, `Error in runDatabaseBackup: ${error}`, null);
		}
	}

	/**
	 * Run audit log maintenance: create partitions + cleanup expired records.
	 * Runs monthly on the 1st at 3 AM via scheduleJobService.
	 * Idempotent — safe to re-run without side effects.
	 */
	async runAuditLogMaintenance(): Promise<void> {
		const METHOD_NAME = 'runAuditLogMaintenance';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;

		try {
			await AuditLogMaintenanceJob.runAll();
			this.logger.logAsync('SCHEDULE', SOURCE, 'Audit log maintenance completed successfully', null);
		} catch (error) {
			console.error(`[SCHEDULE] Error in runAuditLogMaintenance:`, error);
			this.logger.logAsync('SCHEDULE', SOURCE, `Error in runAuditLogMaintenance: ${error}`, null);
		}
	}

	/**
	 * Retire old signing keys past their grace period.
	 * Runs every hour via scheduleJobService.
	 * Safe — only removes keys that have passed the retirement deadline.
	 */
	async runRetireOldKeys(): Promise<void> {
		const METHOD_NAME = 'runRetireOldKeys';
		const SOURCE = `${CLASS_NAME}.${METHOD_NAME}`;

		try {
			const removed = await retireOldKeys();
			if (removed.length > 0) {
				this.logger.logAsync(
					'SCHEDULE',
					SOURCE,
					`Retired signing keys: ${removed.join(', ')}`,
					null,
				);
			}
		} catch (error) {
			console.error(`[SCHEDULE] Error in runRetireOldKeys:`, error);
			this.logger.logAsync('SCHEDULE', SOURCE, `Error in runRetireOldKeys: ${error}`, null);
		}
	}
}
export default ScheduleClass;
