/**
 * @ai-summary Service for enforcing backup retention policies
 * @ai-context Deletes old backups based on schedule retention counts
 */

import * as fsp from 'fs/promises';
import { Pool } from 'pg';
import { logger } from '../../../core/logging/logger';
import { BackupRepository } from '../data/backup.repository';
import {
  RetentionCleanupResult,
  RetentionCleanupJobResult,
  BackupCategory,
  BackupHistory,
  TIERED_RETENTION,
} from './backup.types';

export class BackupRetentionService {
  private repository: BackupRepository;

  constructor(pool: Pool) {
    this.repository = new BackupRepository(pool);
  }

  /**
   * Processes retention cleanup using tiered classification.
   * A backup can only be deleted if it exceeds the quota for ALL of its categories.
   */
  async processRetentionCleanup(): Promise<RetentionCleanupJobResult> {
    logger.info('Starting tiered backup retention cleanup');

    const results: RetentionCleanupResult[] = [];
    const errors: Array<{ scheduleId: string; error: string }> = [];
    let totalDeleted = 0;
    let totalFreedBytes = 0;

    try {
      const result = await this.processTieredRetentionCleanup();
      results.push(result);
      totalDeleted = result.deletedCount;
      totalFreedBytes = result.freedBytes;
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      logger.error('Tiered retention cleanup failed', { error: errorMessage });
      errors.push({ scheduleId: 'tiered', error: errorMessage });
    }

    // Also cleanup failed backups older than 24 hours
    try {
      const failedCount = await this.cleanupFailedBackups();
      if (failedCount > 0) {
        logger.info('Cleaned up failed backups', { count: failedCount });
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      logger.error('Failed backup cleanup failed', { error: errorMessage });
    }

    logger.info('Backup retention cleanup completed', {
      totalDeleted,
      totalFreedBytes,
      errors: errors.length,
    });

    return {
      processed: 1, // Single tiered process
      totalDeleted,
      totalFreedBytes,
      results,
      errors,
    };
  }

  /**
   * Implements tiered retention: keeps N backups per category.
   * A backup is protected if it's in the top N for ANY of its categories.
   * Only deletes backups that exceed ALL applicable category quotas.
   */
  private async processTieredRetentionCleanup(): Promise<RetentionCleanupResult> {
    const allBackups = await this.repository.getAllCompletedBackups();

    if (allBackups.length === 0) {
      logger.debug('No completed backups to process');
      return {
        scheduleId: 'tiered',
        scheduleName: 'Tiered Retention',
        deletedCount: 0,
        retainedCount: 0,
        freedBytes: 0,
      };
    }

    // Build sets of protected backup IDs for each category
    const protectedIds = new Set<string>();
    const categoryRetained: Record<BackupCategory, string[]> = {
      hourly: [],
      daily: [],
      weekly: [],
      monthly: [],
    };

    // For each category, identify which backups to keep
    const categories: BackupCategory[] = ['hourly', 'daily', 'weekly', 'monthly'];
    for (const category of categories) {
      const limit = TIERED_RETENTION[category];
      const backupsInCategory = allBackups.filter(b =>
        b.categories && b.categories.includes(category)
      );

      // Keep the top N (already sorted by started_at DESC)
      const toKeep = backupsInCategory.slice(0, limit);
      for (const backup of toKeep) {
        protectedIds.add(backup.id);
        categoryRetained[category].push(backup.id);
      }

      logger.debug('Category retention analysis', {
        category,
        limit,
        totalInCategory: backupsInCategory.length,
        keeping: toKeep.length,
      });
    }

    // Find backups to delete (not protected by any category)
    const backupsToDelete = allBackups.filter(b => !protectedIds.has(b.id));

    logger.info('Tiered retention analysis complete', {
      totalBackups: allBackups.length,
      protected: protectedIds.size,
      toDelete: backupsToDelete.length,
      hourlyRetained: categoryRetained.hourly.length,
      dailyRetained: categoryRetained.daily.length,
      weeklyRetained: categoryRetained.weekly.length,
      monthlyRetained: categoryRetained.monthly.length,
    });

    // Delete unprotected backups
    let deletedCount = 0;
    let freedBytes = 0;

    for (const backup of backupsToDelete) {
      try {
        // Log retention decision with category reasoning
        logger.info('Deleting backup - exceeded all category quotas', {
          backupId: backup.id,
          filename: backup.filename,
          categories: backup.categories,
          startedAt: backup.startedAt,
          reason: this.buildDeletionReason(backup, categoryRetained),
        });

        // Delete the file
        const filePath = (backup.metadata as any)?.archivePath || backup.filePath;
        if (filePath) {
          try {
            const stats = await fsp.stat(filePath);
            freedBytes += stats.size;
            await fsp.unlink(filePath);
          } catch (error) {
            logger.warn('Failed to delete backup file', {
              backupId: backup.id,
              filePath,
            });
          }
        }

        // Delete the database record
        await this.repository.deleteBackupRecord(backup.id);
        deletedCount++;
      } catch (error) {
        logger.error('Failed to delete backup during retention cleanup', {
          backupId: backup.id,
          error: error instanceof Error ? error.message : String(error),
        });
      }
    }

    return {
      scheduleId: 'tiered',
      scheduleName: 'Tiered Retention',
      deletedCount,
      retainedCount: protectedIds.size,
      freedBytes,
    };
  }

  /**
   * Builds a human-readable reason for why a backup is being deleted.
   */
  private buildDeletionReason(
    backup: BackupHistory,
    categoryRetained: Record<BackupCategory, string[]>
  ): string {
    const reasons: string[] = [];

    for (const category of (backup.categories || ['hourly']) as BackupCategory[]) {
      const kept = categoryRetained[category];
      const limit = TIERED_RETENTION[category];
      if (!kept.includes(backup.id)) {
        reasons.push(`${category}: not in top ${limit}`);
      }
    }

    return reasons.join('; ') || 'no categories';
  }

  /**
   * Cleans up old backups for a specific schedule
   */
  async cleanupScheduleBackups(
    scheduleId: string,
    scheduleName: string,
    retentionCount: number
  ): Promise<RetentionCleanupResult> {
    logger.debug('Processing retention for schedule', {
      scheduleId,
      scheduleName,
      retentionCount,
    });

    // Get backups that exceed retention count
    const backupsToDelete = await this.repository.getOldestBackupsForSchedule(
      scheduleId,
      retentionCount
    );

    let deletedCount = 0;
    let freedBytes = 0;
    const completedBackups = await this.repository.getBackupsBySchedule(scheduleId);
    const retainedCount = Math.min(completedBackups.length, retentionCount);

    for (const backup of backupsToDelete) {
      try {
        // Delete the file
        const filePath = (backup.metadata as any)?.archivePath || backup.filePath;
        if (filePath) {
          try {
            const stats = await fsp.stat(filePath);
            freedBytes += stats.size;
            await fsp.unlink(filePath);
          } catch (error) {
            // File might not exist, continue
            logger.warn('Failed to delete backup file during retention cleanup', {
              backupId: backup.id,
              filePath,
            });
          }
        }

        // Delete the database record
        await this.repository.deleteBackupRecord(backup.id);
        deletedCount++;

        logger.debug('Deleted old backup during retention cleanup', {
          backupId: backup.id,
          scheduleId,
          startedAt: backup.startedAt,
        });
      } catch (error) {
        logger.error('Failed to delete backup during retention cleanup', {
          backupId: backup.id,
          error: error instanceof Error ? error.message : String(error),
        });
      }
    }

    if (deletedCount > 0) {
      logger.info('Retention cleanup completed for schedule', {
        scheduleId,
        scheduleName,
        deletedCount,
        retainedCount,
        freedBytes,
      });
    }

    return {
      scheduleId,
      scheduleName,
      deletedCount,
      retainedCount,
      freedBytes,
    };
  }

  /**
   * Cleans up failed/incomplete backups older than 24 hours
   */
  async cleanupFailedBackups(): Promise<number> {
    const { items } = await this.repository.listBackups({
      status: 'failed',
      pageSize: 100,
    });

    const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
    let deletedCount = 0;

    for (const backup of items) {
      if (backup.startedAt < oneDayAgo) {
        try {
          // Try to delete file if it exists
          const filePath = (backup.metadata as any)?.archivePath || backup.filePath;
          if (filePath) {
            await fsp.unlink(filePath).catch(() => {});
          }

          await this.repository.deleteBackupRecord(backup.id);
          deletedCount++;
        } catch (error) {
          logger.warn('Failed to cleanup failed backup', {
            backupId: backup.id,
            error: error instanceof Error ? error.message : String(error),
          });
        }
      }
    }

    if (deletedCount > 0) {
      logger.info('Cleaned up failed backups', { deletedCount });
    }

    return deletedCount;
  }

}
