/**
 * studiograph admin backup — produce a passphrase-wrapped, manifest-aware
 * snapshot of the entire local workspace.
 *
 * Pairs with `admin restore`. Distinct from `clone`:
 *   - `clone` pulls from a running server; strips machine-specific config
 *     (r2_config, railway, master-key, JWT secret); good for setting up
 *     a new dev machine.
 *   - `admin backup` snapshots the local workspace; includes secrets.db,
 *     auth.db, r2_config, railway IDs, and a manifest of the Railway env
 *     vars (master key, JWT secret) the owner provides at backup time;
 *     good for "server is dead, restore me."
 *
 * Bundle contents (encrypted as a single .sgbackup file):
 *   - manifest.json — env-var values + Railway IDs + workspace name + ts
 *   - workspace/ — tar of the workspace directory, minus
 *     node_modules/, .DS_Store, and any *.log files
 *
 * Crypto: passphrase → scrypt → key → xchacha20poly1305(tar.gz).
 * See [src/cli/crypto-bundle.ts] for the wire format.
 *
 * Refuses to write the bundle to a destination directory unless it
 * exists with mode 0700, or is being created (we make it 0700). Bundle
 * file is written with mode 0600. Defense against world-readable backups
 * that contain bcrypt hashes from auth.db.
 */
import { Command } from 'commander';
import { intro, outro, log, spinner, password, isCancel, confirm } from '@clack/prompts';
import { execFileSync } from 'child_process';
import { existsSync, mkdirSync, writeFileSync, statSync, mkdtempSync, rmSync, readFileSync } from 'fs';
import { join, resolve } from 'path';
import { tmpdir } from 'os';
import { Workspace } from '../../../core/workspace.js';
import { sealBundle } from '../../crypto-bundle.js';
export const adminBackupCommand = new Command('backup')
    .description('Snapshot the local workspace into a passphrase-wrapped bundle')
    .option('--out <path>', 'Output directory for the .sgbackup file (default: cwd)')
    .option('--master-key <value>', 'STUDIOGRAPH_MASTER_KEY env value (also reads STUDIOGRAPH_MASTER_KEY env var)')
    .option('--jwt-secret <value>', 'STUDIOGRAPH_JWT_SECRET env value (also reads STUDIOGRAPH_JWT_SECRET env var)')
    .option('--passphrase-stdin', 'Read the bundle passphrase from stdin (single line)')
    .action(async (opts) => {
    intro('Studiograph workspace backup');
    const workspace = new Workspace();
    if (!workspace.isWorkspace()) {
        outro('Not a Studiograph workspace. Run from your workspace root.');
        process.exit(1);
        return;
    }
    const config = await workspace.loadConfig();
    // Step 1: gather the env-var manifest. These are NOT in the workspace
    // dir — they live in Railway's variables. The backup is useless
    // without them, so we make collecting them an explicit, blocking
    // step.
    const masterKey = opts.masterKey?.trim() || process.env.STUDIOGRAPH_MASTER_KEY?.trim();
    const jwtSecret = opts.jwtSecret?.trim() || process.env.STUDIOGRAPH_JWT_SECRET?.trim();
    let mk;
    let jwt;
    if (masterKey && jwtSecret) {
        mk = masterKey;
        jwt = jwtSecret;
        log.info('Using STUDIOGRAPH_MASTER_KEY + STUDIOGRAPH_JWT_SECRET from flags/env');
    }
    else {
        if (!process.stdout.isTTY) {
            outro('No TTY. Pass --master-key and --jwt-secret, or set the matching env vars.');
            process.exit(1);
            return;
        }
        log.message('These two values live in Railway, not in the workspace. Copy them from\n' +
            '  Railway → your service → Variables → STUDIOGRAPH_MASTER_KEY / STUDIOGRAPH_JWT_SECRET\n' +
            'Without them the restored workspace cannot decrypt secrets.db or verify any\n' +
            'existing JWT cookies.');
        const mkInput = await password({ message: 'STUDIOGRAPH_MASTER_KEY:', validate: (v) => v.trim() ? undefined : 'Required' });
        if (isCancel(mkInput)) {
            outro('Cancelled');
            process.exit(0);
            return;
        }
        const jwtInput = await password({ message: 'STUDIOGRAPH_JWT_SECRET:', validate: (v) => v.trim() ? undefined : 'Required' });
        if (isCancel(jwtInput)) {
            outro('Cancelled');
            process.exit(0);
            return;
        }
        mk = String(mkInput).trim();
        jwt = String(jwtInput).trim();
    }
    // Step 2: passphrase for the bundle.
    let passphrase;
    if (opts.passphraseStdin) {
        passphrase = await readPassphraseStdin();
    }
    else if (process.stdout.isTTY) {
        const pp1 = await password({
            message: 'Bundle passphrase (min 12 chars):',
            validate: (v) => (v.length >= 12 ? undefined : 'At least 12 characters'),
        });
        if (isCancel(pp1)) {
            outro('Cancelled');
            process.exit(0);
            return;
        }
        const pp2 = await password({ message: 'Confirm passphrase:' });
        if (isCancel(pp2)) {
            outro('Cancelled');
            process.exit(0);
            return;
        }
        if (String(pp1) !== String(pp2)) {
            outro('Passphrases did not match.');
            process.exit(1);
            return;
        }
        passphrase = String(pp1);
    }
    else {
        outro('No TTY. Pass --passphrase-stdin and pipe the passphrase to stdin.');
        process.exit(1);
        return;
    }
    // Step 3: validate output directory + file.
    const outDir = resolve(opts.out ?? process.cwd());
    if (!existsSync(outDir)) {
        mkdirSync(outDir, { recursive: true, mode: 0o700 });
    }
    else {
        const dirMode = statSync(outDir).mode & 0o777;
        if ((dirMode & 0o077) !== 0) {
            const proceed = await confirm({
                message: `Output directory ${outDir} is mode 0${dirMode.toString(8)} — group/world readable. Continue anyway?`,
                initialValue: false,
            });
            if (!proceed || isCancel(proceed)) {
                outro('Cancelled — chmod 700 the directory and retry.');
                process.exit(0);
                return;
            }
        }
    }
    const fileName = `studiograph-backup-${(config.team_name || 'workspace').toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${new Date().toISOString().slice(0, 10)}.sgbackup`;
    const filePath = join(outDir, fileName);
    if (existsSync(filePath)) {
        outro(`Refusing to overwrite ${filePath}. Move or delete the existing bundle and retry.`);
        process.exit(1);
        return;
    }
    // Step 4: build the bundle. tar the workspace + manifest into a temp
    // file, encrypt, write. Temp lives in os.tmpdir(); we clean up in
    // finally even on errors.
    const tmpRoot = mkdtempSync(join(tmpdir(), 'sg-backup-'));
    const manifestPath = join(tmpRoot, 'manifest.json');
    const tarPath = join(tmpRoot, 'bundle.tar.gz');
    try {
        const manifest = {
            workspace_name: config.team_name || 'unknown',
            created_at: new Date().toISOString(),
            format_version: 1,
            env: {
                STUDIOGRAPH_MASTER_KEY: mk,
                STUDIOGRAPH_JWT_SECRET: jwt,
            },
            railway: config.railway,
        };
        writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), { encoding: 'utf-8', mode: 0o600 });
        const spin = spinner();
        spin.start('Tarring workspace...');
        // -C tmpRoot for manifest.json + -C cwd for workspace contents.
        // Two `-C` flags = tar resets the chdir per following path. The
        // workspace contents land at top-level of the tar so restore is
        // a single expand. The manifest sits beside them.
        // Exclusions: node_modules, .DS_Store, *.log keep the bundle lean
        // without dropping anything load-bearing.
        execFileSync('tar', [
            '-czf', tarPath,
            '--exclude', 'node_modules',
            '--exclude', '.DS_Store',
            '--exclude', '*.log',
            '-C', tmpRoot, 'manifest.json',
            '-C', process.cwd(), '.',
        ], { stdio: 'pipe' });
        spin.stop('Tarred');
        spin.start('Encrypting (scrypt is intentionally slow)...');
        const plaintext = readFileSync(tarPath);
        const ciphertext = sealBundle(plaintext, passphrase);
        spin.stop('Encrypted');
        writeFileSync(filePath, ciphertext, { mode: 0o600 });
        log.success(`Wrote ${filePath} (${(ciphertext.length / 1024).toFixed(1)} KB)`);
        log.message('Keep this file AND its passphrase separately. The bundle contains\n' +
            'bcrypt password hashes, encrypted secrets.db, and your Railway env\n' +
            'vars in plaintext (within the encrypted envelope) — treat it like\n' +
            'a credential.');
        outro('Backup complete.');
    }
    finally {
        rmSync(tmpRoot, { recursive: true, force: true });
    }
});
async function readPassphraseStdin() {
    return new Promise((resolveFn, rejectFn) => {
        let buf = '';
        process.stdin.setEncoding('utf-8');
        process.stdin.on('data', (chunk) => (buf += chunk));
        process.stdin.on('end', () => resolveFn(buf.replace(/\r?\n$/, '')));
        process.stdin.on('error', rejectFn);
    });
}
//# sourceMappingURL=backup.js.map