/**
 * @ai-summary Service for restoring backups
 * @ai-context Handles preview and execution of backup restoration with safety backup
 */

import { exec } from 'child_process';
import { promisify } from 'util';
import * as fsp from 'fs/promises';
import * as path from 'path';
import * as crypto from 'crypto';
import { Pool } from 'pg';
import { logger } from '../../../core/logging/logger';
import { appConfig } from '../../../core/config/config-loader';
import { BackupRepository } from '../data/backup.repository';
import { BackupArchiveService } from './backup-archive.service';
import { BackupService } from './backup.service';
import {
  BACKUP_CONFIG,
  BackupManifest,
  RestoreOptions,
  RestoreResult,
  RestorePreviewResponse,
} from './backup.types';

const execAsync = promisify(exec);

export class BackupRestoreService {
  private repository: BackupRepository;
  private archiveService: BackupArchiveService;
  private backupService: BackupService;
  private restoreInProgress: boolean = false;

  constructor(pool: Pool) {
    this.repository = new BackupRepository(pool);
    this.archiveService = new BackupArchiveService();
    this.backupService = new BackupService(pool);
  }

  /**
   * Previews what will be restored (dry run)
   */
  async previewRestore(backupId: string): Promise<RestorePreviewResponse> {
    const backup = await this.repository.getBackupById(backupId);
    if (!backup) {
      throw new Error('Backup not found');
    }

    if (backup.status !== 'completed') {
      throw new Error('Cannot restore from incomplete backup');
    }

    const filePath = (backup.metadata as any)?.archivePath || backup.filePath;
    if (!filePath) {
      throw new Error('Backup file path not found');
    }

    // Validate archive and get manifest
    const validation = await this.archiveService.validateArchive(filePath);
    if (!validation.valid) {
      throw new Error(`Invalid backup archive: ${validation.error}`);
    }

    const manifest = validation.manifest!;
    const warnings: string[] = [];

    // Check for potential issues
    if (manifest.version !== BACKUP_CONFIG.archiveVersion) {
      warnings.push(`Backup version (${manifest.version}) differs from current (${BACKUP_CONFIG.archiveVersion})`);
    }

    // Estimate duration based on file sizes
    const totalBytes = backup.fileSizeBytes;
    const estimatedSeconds = Math.max(30, Math.ceil(totalBytes / (10 * 1024 * 1024))); // ~10MB/s
    const estimatedDuration = this.formatDuration(estimatedSeconds);

    return {
      backupId,
      manifest,
      warnings,
      estimatedDuration,
    };
  }

  /**
   * Executes a backup restoration
   */
  async executeRestore(options: RestoreOptions): Promise<RestoreResult> {
    if (this.restoreInProgress) {
      throw new Error('A restore operation is already in progress');
    }

    this.restoreInProgress = true;
    let safetyBackupId: string | undefined;

    try {
      const backup = await this.repository.getBackupById(options.backupId);
      if (!backup) {
        throw new Error('Backup not found');
      }

      if (backup.status !== 'completed') {
        throw new Error('Cannot restore from incomplete backup');
      }

      const filePath = (backup.metadata as any)?.archivePath || backup.filePath;
      if (!filePath) {
        throw new Error('Backup file path not found');
      }

      logger.info('Starting restore operation', {
        backupId: options.backupId,
        createSafetyBackup: options.createSafetyBackup,
      });

      // Create safety backup before restore
      if (options.createSafetyBackup !== false) {
        logger.info('Creating safety backup before restore');
        const safetyResult = await this.backupService.createBackup({
          name: `Pre-restore safety backup`,
          backupType: 'manual',
          includeDocuments: true,
        });

        if (!safetyResult.success) {
          throw new Error(`Failed to create safety backup: ${safetyResult.error}`);
        }

        safetyBackupId = safetyResult.backupId;
        logger.info('Safety backup created', { safetyBackupId });
      }

      // Extract archive to temp directory
      const workDir = path.join(BACKUP_CONFIG.tempPath, `restore-${options.backupId}`);
      await fsp.mkdir(workDir, { recursive: true });

      try {
        logger.info('Extracting backup archive');
        const manifest = await this.archiveService.extractArchive(filePath, workDir);

        // Restore database
        logger.info('Restoring database');
        await this.restoreDatabase(workDir, manifest);

        // Restore documents
        logger.info('Restoring documents');
        await this.restoreDocuments(workDir, manifest);

        logger.info('Restore completed successfully', {
          backupId: options.backupId,
          safetyBackupId,
        });

        return {
          success: true,
          safetyBackupId,
        };
      } finally {
        // Cleanup work directory
        await fsp.rm(workDir, { recursive: true, force: true }).catch(() => {});
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);

      logger.error('Restore failed', {
        backupId: options.backupId,
        error: errorMessage,
        safetyBackupId,
      });

      return {
        success: false,
        safetyBackupId,
        error: errorMessage,
      };
    } finally {
      this.restoreInProgress = false;
    }
  }

  /**
   * Gets the current restore status
   */
  getRestoreStatus(): { inProgress: boolean } {
    return { inProgress: this.restoreInProgress };
  }

  /**
   * Restores the database from a backup
   */
  private async restoreDatabase(workDir: string, manifest: BackupManifest): Promise<void> {
    const dbFilename = manifest.contents.database.filename;
    const sqlPath = path.join(workDir, 'database', dbFilename);

    // Verify the SQL file exists
    await fsp.access(sqlPath);

    // Verify checksum
    const sqlContent = await fsp.readFile(sqlPath);
    const actualChecksum = `sha256:${crypto.createHash('sha256').update(sqlContent).digest('hex')}`;

    if (actualChecksum !== manifest.contents.database.checksum) {
      throw new Error('Database checksum mismatch - backup may be corrupted');
    }

    // Database connection details from app config
    const dbConfig = appConfig.config.database;
    const dbPassword = appConfig.secrets.postgres_password;
    const dbHost = dbConfig.host;
    const dbPort = dbConfig.port.toString();
    const dbUser = dbConfig.user;
    const dbName = dbConfig.name;

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

    try {
      // Note: We no longer terminate connections before restore.
      // The --clean flag in pg_dump generates DROP statements that handle existing data.
      // Terminating connections would kill the backend's own pool and break the HTTP response.

      // Restore the database using psql
      await execAsync(
        `psql -h ${dbHost} -p ${dbPort} -U ${dbUser} -d ${dbName} -f "${sqlPath}"`,
        { env: pgEnv }
      );

      logger.info('Database restored successfully', {
        tablesCount: manifest.contents.database.tablesCount,
      });
    } catch (error) {
      throw new Error(`Database restoration failed: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  /**
   * Restores documents from a backup
   */
  private async restoreDocuments(workDir: string, manifest: BackupManifest): Promise<void> {
    const docsBackupPath = path.join(workDir, 'documents');
    const docsTargetPath = BACKUP_CONFIG.documentsPath;

    // Check if documents were included in backup
    const docsExist = await fsp.access(docsBackupPath).then(() => true).catch(() => false);
    if (!docsExist || manifest.contents.documents.totalFiles === 0) {
      logger.info('No documents to restore');
      return;
    }

    try {
      // Ensure target directory exists
      await fsp.mkdir(docsTargetPath, { recursive: true });

      // Clear existing documents (we're doing a full restore)
      const existingEntries = await fsp.readdir(docsTargetPath);
      for (const entry of existingEntries) {
        await fsp.rm(path.join(docsTargetPath, entry), { recursive: true, force: true });
      }

      // Copy documents from backup
      await this.copyDirRecursive(docsBackupPath, docsTargetPath);

      logger.info('Documents restored successfully', {
        totalFiles: manifest.contents.documents.totalFiles,
        usersCount: manifest.contents.documents.usersCount,
      });
    } catch (error) {
      throw new Error(`Documents restoration failed: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  /**
   * Recursively copies a directory
   */
  private async copyDirRecursive(src: string, dest: string): Promise<void> {
    const entries = await fsp.readdir(src, { withFileTypes: true });

    for (const entry of entries) {
      const srcPath = path.join(src, entry.name);
      const destPath = path.join(dest, entry.name);

      if (entry.isDirectory()) {
        await fsp.mkdir(destPath, { recursive: true });
        await this.copyDirRecursive(srcPath, destPath);
      } else {
        await fsp.copyFile(srcPath, destPath);
      }
    }
  }

  /**
   * Formats duration in human-readable format
   */
  private formatDuration(seconds: number): string {
    if (seconds < 60) {
      return `${seconds} seconds`;
    }
    const minutes = Math.floor(seconds / 60);
    const remainingSeconds = seconds % 60;
    if (remainingSeconds === 0) {
      return `${minutes} minute${minutes > 1 ? 's' : ''}`;
    }
    return `${minutes} minute${minutes > 1 ? 's' : ''} ${remainingSeconds} seconds`;
  }
}
