#!/usr/bin/env node
import { createWriteStream, unlinkSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { spawn } from 'node:child_process';
import {
  commandToString,
  composePrefix,
  ensureParentDir,
  flag,
  option,
  parseArgs,
  readEnvFile,
  timestampSlug,
} from './ops-utils.mjs';

function usage() {
  return `Usage: node scripts/backup-postgres.mjs [options]

Options:
  --env-file <path>       Compose env file (default: .env.production)
  --compose-file <path>   Compose file (default: docker-compose.prod.yml)
  --service <name>        Postgres service name (default: postgres)
  --user <name>           Database user (default: POSTGRES_USER or postgres)
  --db <name>             Database name (default: POSTGRES_DB or ftb_pm)
  --backup-dir <path>     Directory for generated backups (default: backups/postgres)
  --output <path>         Exact output dump path
  --dry-run               Print the pg_dump command without writing a file
  --help                  Show this help
`;
}

function buildOptions(argv) {
  const args = parseArgs(argv);
  if (flag(args, 'help')) return { help: true };

  const envFile = option(args, 'env-file', '.env.production');
  const env = readEnvFile(envFile, { optional: flag(args, 'dry-run') });
  const db = option(args, 'db', env.POSTGRES_DB || 'ftb_pm');
  const user = option(args, 'user', env.POSTGRES_USER || 'postgres');
  const backupDir = option(args, 'backup-dir', 'backups/postgres');
  const output = option(
    args,
    'output',
    join(backupDir, `${db}-postgres-${timestampSlug()}.dump`),
  );

  return {
    help: false,
    dryRun: flag(args, 'dry-run'),
    envFile,
    composeFile: option(args, 'compose-file', 'docker-compose.prod.yml'),
    service: option(args, 'service', 'postgres'),
    db,
    user,
    output: resolve(output),
  };
}

export function buildPgDumpCommand(options) {
  return [
    ...composePrefix(options),
    'exec',
    '-T',
    options.service,
    'pg_dump',
    '-U',
    options.user,
    '-d',
    options.db,
    '--format=custom',
    '--no-owner',
    '--no-acl',
  ];
}

async function writeBackup(command, output) {
  ensureParentDir(output);
  await new Promise((resolveWrite, rejectWrite) => {
    const file = createWriteStream(output, { flags: 'wx' });
    const child = spawn(command[0], command.slice(1), {
      cwd: process.cwd(),
      stdio: ['ignore', 'pipe', 'inherit'],
    });

    let settled = false;
    const rejectOnce = (error) => {
      if (settled) return;
      settled = true;
      try {
        unlinkSync(output);
      } catch {
        // Best effort cleanup of a partial dump.
      }
      rejectWrite(error);
    };

    child.stdout.pipe(file);
    child.on('error', rejectOnce);
    file.on('error', rejectOnce);
    child.on('close', (code) => {
      file.end(() => {
        if (settled) return;
        settled = true;
        if (code === 0) {
          resolveWrite();
        } else {
          rejectOnce(new Error(`pg_dump failed with exit code ${code}`));
        }
      });
    });
  });
}

export async function main(argv = process.argv.slice(2)) {
  const options = buildOptions(argv);
  if (options.help) {
    process.stdout.write(usage());
    return;
  }

  const command = buildPgDumpCommand(options);
  if (options.dryRun) {
    process.stdout.write(`[dry-run] PostgreSQL backup would write: ${options.output}\n`);
    process.stdout.write(`${commandToString(command, { stdout: options.output })}\n`);
    return;
  }

  await writeBackup(command, options.output);
  process.stdout.write(`PostgreSQL backup written: ${options.output}\n`);
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  main().catch((error) => {
    process.stderr.write(`${error.message}\n`);
    process.exit(1);
  });
}

