import { createWriteStream, createReadStream, existsSync, mkdirSync, rmSync, renameSync, statSync, readdirSync, unlinkSync } from 'fs';
import { readFile, writeFile } from 'fs/promises';
import { resolve } from 'path';
import { createHash } from 'crypto';
import archiver from 'archiver';
import AdmZip from 'adm-zip';
import cron from 'node-cron';
import { config } from '../../utils/config.js';
import { createModuleLogger } from '../../utils/logger.js';
import { ensureDir } from './file-ops.js';

const log = createModuleLogger('backup');

/** Max number of automatic backups to keep. Oldest are deleted. */
const MAX_AUTO_BACKUPS = 7;

/** Directories included in backup (everything under data/ except backups). */
const BACKUP_DIRS = ['config', 'characters', 'templates', 'queue', 'analytics', 'stars', 'translations', 'logs'];

export interface BackupInfo {
  filename: string;
  path: string;
  checksum: string;
  createdAt: string;
  sizeBytes: number;
}

/**
 * Creates a ZIP backup of the entire data directory (hot-backup safe).
 * Stores a .sha256 checksum file alongside the archive.
 */
export async function createBackup(dataDir?: string): Promise<BackupInfo> {
  const dir = dataDir || config.dataDir;
  const backupDir = resolve(dir, 'backups');
  await ensureDir(backupDir);

  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const filename = `backup-${timestamp}.zip`;
  const zipPath = resolve(backupDir, filename);

  return new Promise((resolvePromise, reject) => {
    const output = createWriteStream(zipPath);
    const archive = archiver('zip', { zlib: { level: 9 } });

    output.on('close', async () => {
      try {
        const checksum = await calculateChecksum(zipPath);
        await writeFile(`${zipPath}.sha256`, checksum, 'utf-8');

        const stats = statSync(zipPath);
        const info: BackupInfo = {
          filename,
          path: zipPath,
          checksum,
          createdAt: new Date().toISOString(),
          sizeBytes: stats.size,
        };
        log.info({ filename, sizeBytes: stats.size }, 'Backup created');
        resolvePromise(info);
      } catch (err) {
        reject(err);
      }
    });

    archive.on('error', reject);
    archive.pipe(output);

    for (const d of BACKUP_DIRS) {
      const fullPath = resolve(dir, d);
      if (existsSync(fullPath)) {
        archive.directory(fullPath, d);
      }
    }

    archive.finalize();
  });
}


/**
 * Restores data from a ZIP backup.
 * Verifies checksum when a .sha256 file exists alongside the archive.
 * Performs atomic restore: current data is moved to a temp location first,
 * and rolled back if extraction fails.
 */
export async function restoreBackup(zipPath: string, dataDir?: string): Promise<void> {
  const dir = dataDir || config.dataDir;

  // Verify checksum if .sha256 file exists
  const checksumPath = `${zipPath}.sha256`;
  if (existsSync(checksumPath)) {
    const expected = (await readFile(checksumPath, 'utf-8')).trim();
    const actual = await calculateChecksum(zipPath);
    if (expected !== actual) {
      throw new Error('Backup checksum verification failed');
    }
    log.info('Checksum verified');
  }

  // Temp directory for rollback safety
  const tempBackupDir = resolve(dir, '..', '.data-pre-restore');
  if (existsSync(tempBackupDir)) rmSync(tempBackupDir, { recursive: true, force: true });

  // Move current dirs to temp
  mkdirSync(tempBackupDir, { recursive: true });
  for (const d of BACKUP_DIRS) {
    const src = resolve(dir, d);
    if (existsSync(src)) {
      renameSync(src, resolve(tempBackupDir, d));
    }
  }

  try {
    const zip = new AdmZip(zipPath);
    zip.extractAllTo(dir, true);

    // Clean up temp backup on success
    rmSync(tempBackupDir, { recursive: true, force: true });
    log.info({ zipPath }, 'Backup restored successfully');
  } catch (err) {
    // Rollback: restore original dirs from temp
    log.error({ err }, 'Restore failed, rolling back');
    for (const d of BACKUP_DIRS) {
      const tempSrc = resolve(tempBackupDir, d);
      const dest = resolve(dir, d);
      if (existsSync(tempSrc)) {
        if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
        renameSync(tempSrc, dest);
      }
    }
    rmSync(tempBackupDir, { recursive: true, force: true });
    throw err;
  }
}

/**
 * Lists all available backups sorted by creation date (newest first).
 */
export async function listBackups(dataDir?: string): Promise<BackupInfo[]> {
  const dir = dataDir || config.dataDir;
  const backupDir = resolve(dir, 'backups');
  if (!existsSync(backupDir)) return [];

  const files = readdirSync(backupDir).filter(f => f.endsWith('.zip'));

  const backups: BackupInfo[] = [];
  for (const file of files) {
    const filePath = resolve(backupDir, file);
    const stats = statSync(filePath);
    const checksumPath = `${filePath}.sha256`;
    let checksum = '';
    if (existsSync(checksumPath)) {
      checksum = (await readFile(checksumPath, 'utf-8')).trim();
    }
    backups.push({
      filename: file,
      path: filePath,
      checksum,
      createdAt: stats.mtime.toISOString(),
      sizeBytes: stats.size,
    });
  }

  return backups.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}

/**
 * Verifies a backup's integrity by comparing its SHA-256 checksum.
 * Returns true if valid, false if checksum file is missing or mismatch.
 */
export async function verifyBackup(zipPath: string): Promise<boolean> {
  const checksumPath = `${zipPath}.sha256`;
  if (!existsSync(checksumPath)) return false;

  const expected = (await readFile(checksumPath, 'utf-8')).trim();
  const actual = await calculateChecksum(zipPath);
  return expected === actual;
}

/**
 * Computes SHA-256 checksum of a file via streaming.
 */
async function calculateChecksum(filePath: string): Promise<string> {
  return new Promise((resolve, reject) => {
    const hash = createHash('sha256');
    const stream = createReadStream(filePath);
    stream.on('data', (data) => hash.update(data));
    stream.on('end', () => resolve(hash.digest('hex')));
    stream.on('error', reject);
  });
}

// ── Automatic Backup Scheduling ──

let scheduledTask: cron.ScheduledTask | null = null;

/**
 * Prunes old automatic backups, keeping only the newest `MAX_AUTO_BACKUPS`.
 */
async function pruneOldBackups(dataDir?: string): Promise<void> {
  const backups = await listBackups(dataDir);
  if (backups.length <= MAX_AUTO_BACKUPS) return;

  const toDelete = backups.slice(MAX_AUTO_BACKUPS);
  for (const b of toDelete) {
    try {
      if (existsSync(b.path)) unlinkSync(b.path);
      const csPath = `${b.path}.sha256`;
      if (existsSync(csPath)) unlinkSync(csPath);
      log.info({ filename: b.filename }, 'Pruned old backup');
    } catch (err: any) {
      log.warn({ filename: b.filename, err: err.message }, 'Failed to prune old backup');
    }
  }
}

/**
 * Starts an automatic daily backup cron job (runs at 03:00 every day).
 * Old backups beyond `MAX_AUTO_BACKUPS` are automatically pruned.
 */
export function startAutoBackup(cronExpr = '0 3 * * *'): void {
  if (scheduledTask) {
    log.warn('Auto-backup already running, skipping duplicate start');
    return;
  }

  scheduledTask = cron.schedule(cronExpr, async () => {
    try {
      log.info('Running scheduled automatic backup...');
      const info = await createBackup();
      log.info({ filename: info.filename, sizeBytes: info.sizeBytes }, 'Automatic backup completed');
      await pruneOldBackups();
    } catch (err: any) {
      log.error({ err: err.message }, 'Automatic backup failed');
    }
  });

  log.info({ cronExpr }, 'Automatic backup scheduler started');
}

/**
 * Stops the automatic backup cron job.
 */
export function stopAutoBackup(): void {
  if (scheduledTask) {
    scheduledTask.stop();
    scheduledTask = null;
    log.info('Automatic backup scheduler stopped');
  }
}
