import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis from 'ioredis';
import * as fs from 'fs';
import * as path from 'path';
import * as archiver from 'archiver';
import { BackupTarget } from '@/domain/enums';
import {
  BackupHandler,
  BackupResult,
  ProgressCallback,
} from '@/domain/interfaces/backup-handler.interface';

@Injectable()
export class RedisBackupHandler implements BackupHandler {
  readonly target = BackupTarget.REDIS;
  private readonly logger = new Logger(RedisBackupHandler.name);

  private readonly redisHost: string;
  private readonly redisPort: number;
  private readonly redisPassword: string;
  private readonly sourceDir = '/backup-source/redis';

  constructor(private readonly configService: ConfigService) {
    this.redisHost = this.configService.get<string>('REDIS_HOST', 'localhost');
    this.redisPort = this.configService.get<number>('REDIS_PORT', 6379);
    this.redisPassword = this.configService.get<string>('REDIS_PASSWORD', '');
  }

  async execute(outputDir: string, onProgress: ProgressCallback): Promise<BackupResult> {
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const fileName = `redis-${timestamp}.tar.gz`;
    const filePath = path.join(outputDir, fileName);

    fs.mkdirSync(outputDir, { recursive: true });

    onProgress(0, 'Redis BGSAVE 触发中...');

    // 1. 连接 Redis 触发 BGSAVE
    const redis = new Redis({
      host: this.redisHost,
      port: this.redisPort,
      password: this.redisPassword || undefined,
      maxRetriesPerRequest: 3,
    });

    try {
      const lastSaveBefore = await redis.lastsave();
      await redis.bgsave();
      this.logger.log('Redis BGSAVE 已触发');

      // 2. 轮询等待 BGSAVE 完成
      onProgress(10, 'Redis BGSAVE 执行中...');
      let attempts = 0;
      const maxAttempts = 120; // 最多等待 60 秒
      while (attempts < maxAttempts) {
        await new Promise((r) => setTimeout(r, 500));
        const lastSaveAfter = await redis.lastsave();
        if (lastSaveAfter > lastSaveBefore) {
          break;
        }
        attempts++;
        onProgress(10 + Math.min(40, Math.floor((attempts / maxAttempts) * 40)), 'Redis BGSAVE 执行中...');
      }

      if (attempts >= maxAttempts) {
        throw new Error('Redis BGSAVE 超时 (60秒)');
      }

      onProgress(50, 'Redis BGSAVE 完成，开始打包文件...');
    } finally {
      redis.disconnect();
    }

    // 3. 先复制文件到临时目录（避免 Redis 写入导致 tar-stream Size mismatch 崩溃）
    const tempCopyDir = path.join(outputDir, `redis-copy-${Date.now()}`);
    fs.mkdirSync(tempCopyDir, { recursive: true });

    onProgress(50, '复制 Redis 数据文件到临时目录...');

    const dumpSrc = path.join(this.sourceDir, 'dump.rdb');
    const dumpDst = path.join(tempCopyDir, 'dump.rdb');
    if (fs.existsSync(dumpSrc)) {
      fs.copyFileSync(dumpSrc, dumpDst);
    }

    const aofSrc = path.join(this.sourceDir, 'appendonly.aof');
    const aofDst = path.join(tempCopyDir, 'appendonly.aof');
    if (fs.existsSync(aofSrc)) {
      fs.copyFileSync(aofSrc, aofDst);
    }

    // AOF 目录（Redis 7 multi-part AOF）
    const aofDirSrc = path.join(this.sourceDir, 'appendonlydir');
    const aofDirDst = path.join(tempCopyDir, 'appendonlydir');
    if (fs.existsSync(aofDirSrc)) {
      this.copyDirSync(aofDirSrc, aofDirDst);
    }

    onProgress(60, '开始打包...');

    // 4. 打包临时目录的快照文件
    return new Promise<BackupResult>((resolve, reject) => {
      const output = fs.createWriteStream(filePath);
      const archive = archiver('tar', { gzip: true });

      output.on('close', () => {
        const fileSize = archive.pointer();
        this.logger.log(`Redis 备份完成: ${fileName}, 大小: ${fileSize} bytes`);
        onProgress(100, 'Redis 备份完成');
        // 清理临时复制目录
        fs.rmSync(tempCopyDir, { recursive: true, force: true });
        resolve({ fileName, filePath, fileSize });
      });

      archive.on('error', (err) => {
        this.logger.error(`Redis 备份打包失败: ${err.message}`);
        fs.rmSync(tempCopyDir, { recursive: true, force: true });
        reject(err);
      });

      archive.pipe(output);

      if (fs.existsSync(dumpDst)) {
        archive.file(dumpDst, { name: 'dump.rdb' });
        onProgress(70, '打包 dump.rdb...');
      }

      if (fs.existsSync(aofDst)) {
        archive.file(aofDst, { name: 'appendonly.aof' });
        onProgress(85, '打包 appendonly.aof...');
      }

      if (fs.existsSync(aofDirDst)) {
        archive.directory(aofDirDst, 'appendonlydir');
        onProgress(85, '打包 appendonlydir/...');
      }

      archive.finalize();
    });
  }

  private copyDirSync(src: string, dst: string): void {
    fs.mkdirSync(dst, { recursive: true });
    for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
      const srcPath = path.join(src, entry.name);
      const dstPath = path.join(dst, entry.name);
      if (entry.isDirectory()) {
        this.copyDirSync(srcPath, dstPath);
      } else {
        fs.copyFileSync(srcPath, dstPath);
      }
    }
  }
}
