import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Cron, CronExpression } from '@nestjs/schedule';
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
import { pipeline } from 'stream/promises';

const execAsync = promisify(exec);

/**
 * Database Backup Service
 *
 * Features:
 * - Automated PostgreSQL database backups
 * - Gzip compression
 * - S3 upload for off-site storage
 * - Retention policy enforcement
 * - MongoDB backup support
 * - Backup verification
 */
@Injectable()
export class BackupService {
  private readonly logger = new Logger(BackupService.name);
  private readonly backupDir: string;
  private readonly isEnabled: boolean;
  private readonly retentionDays: number;

  constructor(private configService: ConfigService) {
    this.isEnabled = this.configService.get<boolean>('backups.enabled', false);
    this.backupDir = this.configService.get<string>('backups.dir', '/var/backups/maternal-app');
    this.retentionDays = this.configService.get<number>('backups.retentionDays', 30);

    // Create backup directory if it doesn't exist
    if (this.isEnabled && !fs.existsSync(this.backupDir)) {
      fs.mkdirSync(this.backupDir, { recursive: true });
      this.logger.log(`Created backup directory: ${this.backupDir}`);
    }
  }

  /**
   * Scheduled backup job - runs daily at 2 AM (configurable via BACKUP_SCHEDULE)
   */
  @Cron(CronExpression.EVERY_DAY_AT_2AM)
  async scheduledBackup() {
    if (!this.isEnabled) {
      return;
    }

    this.logger.log('Starting scheduled database backup...');

    try {
      await this.backupAll();
      this.logger.log('Scheduled backup completed successfully');
    } catch (error) {
      this.logger.error('Scheduled backup failed:', error);
    }
  }

  /**
   * Backup all databases
   */
  async backupAll(): Promise<{ postgres: string; mongodb?: string }> {
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const results: { postgres: string; mongodb?: string } = {
      postgres: '',
    };

    try {
      // Backup PostgreSQL
      results.postgres = await this.backupPostgreSQL(timestamp);
      this.logger.log(`PostgreSQL backup completed: ${results.postgres}`);

      // Backup MongoDB
      const mongoUri = this.configService.get<string>('mongodb.uri');
      if (mongoUri) {
        results.mongodb = await this.backupMongoDB(timestamp);
        this.logger.log(`MongoDB backup completed: ${results.mongodb}`);
      }

      // Clean up old backups
      await this.cleanupOldBackups();

      // Upload to S3 if configured
      const s3Bucket = this.configService.get<string>('backups.s3.bucket');
      if (s3Bucket) {
        await this.uploadToS3(results.postgres);
        if (results.mongodb) {
          await this.uploadToS3(results.mongodb);
        }
      }

      return results;
    } catch (error) {
      this.logger.error('Backup failed:', error);
      throw error;
    }
  }

  /**
   * Backup PostgreSQL database
   */
  private async backupPostgreSQL(timestamp: string): Promise<string> {
    const dbHost = this.configService.get<string>('database.host');
    const dbPort = this.configService.get<number>('database.port');
    const dbName = this.configService.get<string>('database.name');
    const dbUser = this.configService.get<string>('database.user');
    const dbPassword = this.configService.get<string>('database.password');

    const backupFileName = `postgresql_${dbName}_${timestamp}.sql.gz`;
    const backupPath = path.join(this.backupDir, backupFileName);

    // Set password environment variable
    const env = { ...process.env, PGPASSWORD: dbPassword };

    // pg_dump command with compression
    const command = `pg_dump -h ${dbHost} -p ${dbPort} -U ${dbUser} -d ${dbName} --format=plain --no-owner --no-acl | gzip > ${backupPath}`;

    try {
      await execAsync(command, { env, maxBuffer: 1024 * 1024 * 100 }); // 100MB buffer

      // Verify backup file exists and has content
      const stats = fs.statSync(backupPath);
      if (stats.size === 0) {
        throw new Error('Backup file is empty');
      }

      this.logger.log(`PostgreSQL backup created: ${backupPath} (${this.formatBytes(stats.size)})`);
      return backupPath;
    } catch (error) {
      this.logger.error('PostgreSQL backup failed:', error);
      throw error;
    }
  }

  /**
   * Backup MongoDB database
   */
  private async backupMongoDB(timestamp: string): Promise<string> {
    const mongoUri = this.configService.get<string>('mongodb.uri');
    const backupFileName = `mongodb_${timestamp}`;
    const backupPath = path.join(this.backupDir, backupFileName);
    const archivePath = `${backupPath}.tar.gz`;

    // mongodump command
    const command = `mongodump --uri="${mongoUri}" --out="${backupPath}" && tar -czf "${archivePath}" -C "${this.backupDir}" "${backupFileName}" && rm -rf "${backupPath}"`;

    try {
      await execAsync(command, { maxBuffer: 1024 * 1024 * 100 });

      const stats = fs.statSync(archivePath);
      this.logger.log(`MongoDB backup created: ${archivePath} (${this.formatBytes(stats.size)})`);
      return archivePath;
    } catch (error) {
      this.logger.error('MongoDB backup failed:', error);
      throw error;
    }
  }

  /**
   * Upload backup to S3
   */
  private async uploadToS3(backupPath: string): Promise<void> {
    try {
      // TODO: Implement S3 upload
      // Install @aws-sdk/client-s3 to enable this feature

      /*
      const { S3Client, PutObjectCommand } = await import('@aws-sdk/client-s3');
      const { createReadStream } = await import('fs');

      const s3Client = new S3Client({
        region: this.configService.get<string>('backups.s3.region', 'us-east-1'),
        credentials: {
          accessKeyId: this.configService.get<string>('backups.s3.accessKey'),
          secretAccessKey: this.configService.get<string>('backups.s3.secretKey'),
        },
      });

      const bucket = this.configService.get<string>('backups.s3.bucket');
      const key = `backups/${path.basename(backupPath)}`;

      const fileStream = createReadStream(backupPath);

      await s3Client.send(
        new PutObjectCommand({
          Bucket: bucket,
          Key: key,
          Body: fileStream,
          ServerSideEncryption: 'AES256',
          StorageClass: 'STANDARD_IA', // Infrequent Access for cost savings
        })
      );

      this.logger.log(`Backup uploaded to S3: s3://${bucket}/${key}`);
      */

      this.logger.warn(`S3 upload not configured. Backup stored locally: ${backupPath}`);
    } catch (error) {
      this.logger.error('S3 upload failed:', error);
      throw error;
    }
  }

  /**
   * Clean up backups older than retention period
   */
  private async cleanupOldBackups(): Promise<void> {
    try {
      const files = fs.readdirSync(this.backupDir);
      const now = Date.now();
      const retentionMs = this.retentionDays * 24 * 60 * 60 * 1000;

      let deletedCount = 0;

      for (const file of files) {
        const filePath = path.join(this.backupDir, file);
        const stats = fs.statSync(filePath);

        if (now - stats.mtimeMs > retentionMs) {
          fs.unlinkSync(filePath);
          deletedCount++;
          this.logger.log(`Deleted old backup: ${file}`);
        }
      }

      if (deletedCount > 0) {
        this.logger.log(`Cleaned up ${deletedCount} old backup(s)`);
      }
    } catch (error) {
      this.logger.error('Backup cleanup failed:', error);
    }
  }

  /**
   * Restore PostgreSQL database from backup
   */
  async restorePostgreSQL(backupPath: string): Promise<void> {
    const dbHost = this.configService.get<string>('database.host');
    const dbPort = this.configService.get<number>('database.port');
    const dbName = this.configService.get<string>('database.name');
    const dbUser = this.configService.get<string>('database.user');
    const dbPassword = this.configService.get<string>('database.password');

    const env = { ...process.env, PGPASSWORD: dbPassword };

    // Decompress and restore
    const command = `gunzip -c ${backupPath} | psql -h ${dbHost} -p ${dbPort} -U ${dbUser} -d ${dbName}`;

    try {
      this.logger.warn(`Restoring PostgreSQL database from: ${backupPath}`);
      await execAsync(command, { env, maxBuffer: 1024 * 1024 * 100 });
      this.logger.log('PostgreSQL restore completed successfully');
    } catch (error) {
      this.logger.error('PostgreSQL restore failed:', error);
      throw error;
    }
  }

  /**
   * List available backups
   */
  async listBackups(): Promise<Array<{ filename: string; size: number; created: Date }>> {
    try {
      const files = fs.readdirSync(this.backupDir);
      const backups = files
        .filter((file) => file.endsWith('.sql.gz') || file.endsWith('.tar.gz'))
        .map((file) => {
          const filePath = path.join(this.backupDir, file);
          const stats = fs.statSync(filePath);
          return {
            filename: file,
            size: stats.size,
            created: stats.mtime,
          };
        })
        .sort((a, b) => b.created.getTime() - a.created.getTime());

      return backups;
    } catch (error) {
      this.logger.error('Failed to list backups:', error);
      return [];
    }
  }

  /**
   * Format bytes to human-readable string
   */
  private formatBytes(bytes: number): string {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
  }
}
