/**
 * @ai-summary Service for creating and extracting backup archives
 * @ai-context Creates tar.gz archives containing database dump and documents
 */

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 * as tar from 'tar';
import { logger } from '../../../core/logging/logger';
import { appConfig } from '../../../core/config/config-loader';
import { BACKUP_CONFIG, BackupManifest } from './backup.types';

const execAsync = promisify(exec);

export interface DatabaseExportResult {
  filename: string;
  tablesCount: number;
  sizeBytes: number;
  checksum: string;
  postgresqlVersion: string;
}

export interface DocumentsCopyResult {
  totalFiles: number;
  totalSizeBytes: number;
  usersCount: number;
}

export interface ArchiveResult {
  archivePath: string;
  sizeBytes: number;
  manifest: BackupManifest;
}

export class BackupArchiveService {
  private readonly storagePath: string;
  private readonly documentsPath: string;
  private readonly tempPath: string;

  constructor() {
    this.storagePath = BACKUP_CONFIG.storagePath;
    this.documentsPath = BACKUP_CONFIG.documentsPath;
    this.tempPath = BACKUP_CONFIG.tempPath;
  }

  /**
   * Creates a complete backup archive
   */
  async createArchive(
    backupId: string,
    options: {
      scheduleName?: string;
      backupType: 'scheduled' | 'manual';
      includeDocuments?: boolean;
      createdBy?: string;
    }
  ): Promise<ArchiveResult> {
    const workDir = path.join(this.tempPath, backupId);

    try {
      // Create working directory
      await fsp.mkdir(workDir, { recursive: true });
      await fsp.mkdir(path.join(workDir, 'database'), { recursive: true });

      // Export database
      logger.info('Exporting database for backup', { backupId });
      const dbResult = await this.exportDatabase(workDir);

      // Copy documents
      let docsResult: DocumentsCopyResult = {
        totalFiles: 0,
        totalSizeBytes: 0,
        usersCount: 0,
      };

      if (options.includeDocuments !== false) {
        logger.info('Copying documents for backup', { backupId });
        docsResult = await this.copyDocuments(workDir);
      }

      // Create manifest
      const manifest = await this.createManifest(
        workDir,
        dbResult,
        docsResult,
        options
      );

      // Create tar.gz archive
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
      const archiveFilename = `motovaultpro_backup_${timestamp}.tar.gz`;
      const archivePath = path.join(this.storagePath, archiveFilename);

      logger.info('Creating tar.gz archive', { backupId, archivePath });
      await this.createTarGz(workDir, archivePath);

      const archiveStats = await fsp.stat(archivePath);

      logger.info('Backup archive created successfully', {
        backupId,
        archivePath,
        sizeBytes: archiveStats.size,
      });

      return {
        archivePath,
        sizeBytes: archiveStats.size,
        manifest,
      };
    } finally {
      // Cleanup working directory
      await this.cleanupWorkDir(workDir);
    }
  }

  /**
   * Exports the PostgreSQL database to a SQL file
   * Note: The SQL file is kept uncompressed - tar.gz handles compression
   */
  async exportDatabase(workDir: string): Promise<DatabaseExportResult> {
    const databaseDir = path.join(workDir, 'database');
    const sqlFilename = 'motovaultpro.sql';
    const sqlPath = path.join(databaseDir, sqlFilename);

    // 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 pg_dump/psql
    const pgEnv = { ...process.env, PGPASSWORD: dbPassword };

    try {
      // Get PostgreSQL version
      const { stdout: versionOutput } = await execAsync(
        `psql -h ${dbHost} -p ${dbPort} -U ${dbUser} -d ${dbName} -t -c "SELECT version();"`,
        { env: pgEnv }
      );
      const postgresqlVersion = versionOutput.trim().split(' ')[1] || 'unknown';

      // Export database using pg_dump
      // --clean: Output DROP statements before CREATE (required for restore to work on existing DB)
      // --if-exists: Add IF EXISTS to DROP statements (prevents errors on fresh installs)
      await execAsync(
        `pg_dump -h ${dbHost} -p ${dbPort} -U ${dbUser} -d ${dbName} --format=plain --clean --if-exists -f "${sqlPath}"`,
        { env: pgEnv }
      );

      // Count tables
      const { stdout: tablesOutput } = await execAsync(
        `psql -h ${dbHost} -p ${dbPort} -U ${dbUser} -d ${dbName} -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE';"`,
        { env: pgEnv }
      );
      const tablesCount = parseInt(tablesOutput.trim(), 10) || 0;

      // Calculate checksum of SQL file
      const sqlContent = await fsp.readFile(sqlPath);
      const checksum = crypto.createHash('sha256').update(sqlContent).digest('hex');

      const stats = await fsp.stat(sqlPath);

      return {
        filename: sqlFilename,
        tablesCount,
        sizeBytes: stats.size,
        checksum: `sha256:${checksum}`,
        postgresqlVersion,
      };
    } catch (error) {
      logger.error('Database export failed', {
        error: error instanceof Error ? error.message : String(error),
      });
      throw new Error(`Database export failed: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  /**
   * Copies document files to the backup working directory
   */
  async copyDocuments(workDir: string): Promise<DocumentsCopyResult> {
    const docsDir = path.join(workDir, 'documents');
    await fsp.mkdir(docsDir, { recursive: true });

    let totalFiles = 0;
    let totalSizeBytes = 0;
    const userDirs = new Set<string>();

    try {
      // Check if documents directory exists
      const documentsExists = await fsp.access(this.documentsPath).then(() => true).catch(() => false);

      if (!documentsExists) {
        logger.info('Documents directory does not exist, skipping document backup');
        return { totalFiles: 0, totalSizeBytes: 0, usersCount: 0 };
      }

      // Recursively copy documents
      await this.copyDirRecursive(this.documentsPath, docsDir, {
        onFile: async (srcPath, _destPath) => {
          const stats = await fsp.stat(srcPath);
          totalFiles++;
          totalSizeBytes += stats.size;

          // Track unique user directories
          const relativePath = path.relative(this.documentsPath, srcPath);
          const userId = relativePath.split(path.sep)[0];
          if (userId) {
            userDirs.add(userId);
          }
        },
      });

      return {
        totalFiles,
        totalSizeBytes,
        usersCount: userDirs.size,
      };
    } catch (error) {
      logger.error('Documents copy failed', {
        error: error instanceof Error ? error.message : String(error),
      });
      throw new Error(`Documents copy failed: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  /**
   * Creates the backup manifest file
   */
  async createManifest(
    workDir: string,
    dbResult: DatabaseExportResult,
    docsResult: DocumentsCopyResult,
    options: {
      scheduleName?: string;
      backupType: 'scheduled' | 'manual';
      createdBy?: string;
    }
  ): Promise<BackupManifest> {
    const manifest: BackupManifest = {
      version: BACKUP_CONFIG.archiveVersion,
      createdAt: new Date().toISOString(),
      applicationVersion: process.env.npm_package_version || '1.0.0',
      backupType: options.backupType,
      scheduleName: options.scheduleName || null,
      contents: {
        database: {
          filename: dbResult.filename,
          tablesCount: dbResult.tablesCount,
          postgresqlVersion: dbResult.postgresqlVersion,
          sizeBytes: dbResult.sizeBytes,
          checksum: dbResult.checksum,
        },
        documents: {
          totalFiles: docsResult.totalFiles,
          totalSizeBytes: docsResult.totalSizeBytes,
          usersCount: docsResult.usersCount,
        },
      },
      metadata: {
        hostname: process.env.HOSTNAME || 'mvp-backend',
        createdBy: options.createdBy || 'system',
        compression: 'gzip',
        postgresqlVersion: dbResult.postgresqlVersion,
      },
    };

    const manifestPath = path.join(workDir, 'manifest.json');
    await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2));

    return manifest;
  }

  /**
   * Creates a tar.gz archive from the working directory
   */
  async createTarGz(sourceDir: string, destPath: string): Promise<void> {
    // Ensure destination directory exists
    await fsp.mkdir(path.dirname(destPath), { recursive: true });

    // Create tar.gz using the tar package
    await tar.create(
      {
        gzip: true,
        file: destPath,
        cwd: sourceDir,
      },
      ['.']
    );
  }

  /**
   * Extracts a backup archive to a working directory
   */
  async extractArchive(archivePath: string, workDir: string): Promise<BackupManifest> {
    await fsp.mkdir(workDir, { recursive: true });

    // Extract tar.gz
    await tar.extract({
      file: archivePath,
      cwd: workDir,
    });

    // Read and validate manifest
    const manifestPath = path.join(workDir, 'manifest.json');
    const manifestContent = await fsp.readFile(manifestPath, 'utf-8');
    const manifest = JSON.parse(manifestContent) as BackupManifest;

    // Validate manifest version
    if (!manifest.version) {
      throw new Error('Invalid backup manifest: missing version');
    }

    return manifest;
  }

  /**
   * Validates an archive file without fully extracting it
   */
  async validateArchive(archivePath: string): Promise<{
    valid: boolean;
    manifest?: BackupManifest;
    error?: string;
  }> {
    const tempDir = path.join(this.tempPath, `validate-${Date.now()}`);

    try {
      const manifest = await this.extractArchive(archivePath, tempDir);

      // Verify required files exist
      const dbPath = path.join(tempDir, 'database', manifest.contents.database.filename);
      await fsp.access(dbPath);

      return { valid: true, manifest };
    } catch (error) {
      return {
        valid: false,
        error: error instanceof Error ? error.message : String(error),
      };
    } finally {
      await this.cleanupWorkDir(tempDir);
    }
  }

  /**
   * Gets the manifest from an archive without extracting it fully
   */
  async getManifestFromArchive(archivePath: string): Promise<BackupManifest> {
    const tempDir = path.join(this.tempPath, `manifest-${Date.now()}`);

    try {
      // Extract only manifest.json
      await tar.extract({
        file: archivePath,
        cwd: tempDir,
        filter: (p) => p === 'manifest.json' || p === './manifest.json',
      });

      const manifestPath = path.join(tempDir, 'manifest.json');
      const manifestContent = await fsp.readFile(manifestPath, 'utf-8');
      return JSON.parse(manifestContent) as BackupManifest;
    } finally {
      await this.cleanupWorkDir(tempDir);
    }
  }

  /**
   * Recursively copies a directory
   */
  private async copyDirRecursive(
    src: string,
    dest: string,
    options?: {
      onFile?: (srcPath: string, destPath: string) => Promise<void>;
    }
  ): 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, options);
      } else {
        await fsp.copyFile(srcPath, destPath);
        if (options?.onFile) {
          await options.onFile(srcPath, destPath);
        }
      }
    }
  }

  /**
   * Cleans up a working directory
   */
  private async cleanupWorkDir(workDir: string): Promise<void> {
    try {
      await fsp.rm(workDir, { recursive: true, force: true });
    } catch (error) {
      logger.warn('Failed to cleanup work directory', {
        workDir,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }
}
