/**
 * studiograph r2 — Cloudflare R2 asset storage management
 *
 *   studiograph r2 setup              Set up R2 bucket and credentials
 *   studiograph r2 upload <dir>       Upload a local directory to R2
 *     --prefix <path>                 Key prefix in bucket (e.g. fonts/)
 *   studiograph r2 sync               Upload local assets from tokens.json and
 *                                     markdown files to R2, rewriting URLs in place
 *   studiograph r2 migrate-urls       Stage 2 — rewrite legacy absolute R2 URLs
 *                                     in entity frontmatter and bodies to the
 *                                     proxied /api/assets/* path
 */
import { Command } from 'commander';
import { intro, outro, text, confirm, spinner, log } from '@clack/prompts';
import { readdirSync, lstatSync, readFileSync, writeFileSync, existsSync } from 'fs';
import { join, relative } from 'path';
import { spawnSync } from 'child_process';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { Workspace } from '../../core/workspace.js';
import { loadUserConfig, saveUserConfig } from '../../core/user-config.js';
import { buildR2Client, contentType, createR2Bucket, syncLocalAssetsToR2, walkDir, } from '../../services/assets/r2-sync.js';
// ── Open a URL in the default browser (macOS + Linux) ────────────────────
function openBrowser(url) {
    const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
    spawnSync(cmd, [url], { stdio: 'ignore' });
}
// ── r2 command group ──────────────────────────────────────────────────────
export const r2Command = new Command('r2')
    .description('Manage Cloudflare R2 asset storage');
// ── studiograph r2 setup ──────────────────────────────────────────────────
r2Command
    .command('setup')
    .description('Set up Cloudflare R2 bucket and save credentials')
    .action(async () => {
    const workspace = new Workspace();
    if (!workspace.isWorkspace()) {
        console.error('Not a Studiograph workspace. Run `studiograph init` first.');
        process.exit(1);
    }
    const config = await workspace.loadConfig();
    intro('Set up Cloudflare R2 — 2 steps');
    // ── Step 1: Account ID + credentials ───────────────────────────────
    log.step('Step 1 of 2 — Connect your Cloudflare account');
    log.message('Opening your R2 dashboard (Storage & databases → R2 object storage → Overview)...\n' +
        '  1. Copy your Account ID from the right sidebar\n' +
        '  2. Click API Tokens → Manage → Create API Token\n' +
        '     Permissions: Admin Read & Write\n' +
        '  3. Copy the Access Key ID and Secret Access Key\n\n' +
        'Note: Admin access lets Studiograph create your bucket automatically.\n' +
        'You can replace this with a bucket-scoped token later for tighter security.');
    const openR2 = await confirm({ message: 'Open Cloudflare R2 dashboard in browser?' });
    if (openR2) {
        openBrowser('https://dash.cloudflare.com/?to=/:account/r2/overview');
    }
    else {
        log.info('Open https://dash.cloudflare.com → R2 Object Storage to get your credentials');
    }
    const accountId = await text({
        message: 'Account ID',
        placeholder: '32-character hex string',
        validate: (v) => v.trim() ? undefined : 'Required',
    });
    const accessKeyId = await text({
        message: 'Access Key ID',
        placeholder: 'Paste the Access Key ID',
        validate: (v) => v.trim() ? undefined : 'Required',
    });
    const secretAccessKey = await text({
        message: 'Secret Access Key',
        placeholder: 'Paste the Secret Access Key',
        validate: (v) => v.trim() ? undefined : 'Required',
    });
    // ── Create bucket automatically ─────────────────────────────────────
    const defaultBucket = config.team_name
        .toLowerCase()
        .replace(/\s+/g, '-')
        .replace(/[^a-z0-9-]/g, '')
        + '-assets';
    const bucketInput = await text({
        message: 'Bucket name',
        placeholder: defaultBucket,
    });
    const bucket = bucketInput.trim() || defaultBucket;
    const s = spinner();
    s.start(`Creating bucket "${bucket}"...`);
    try {
        const result = await createR2Bucket({ accountId, accessKeyId, secretAccessKey }, bucket);
        s.stop(result.alreadyExists
            ? `Bucket "${bucket}" already exists — continuing`
            : `Bucket "${bucket}" created`);
    }
    catch (e) {
        s.stop(`Failed to create bucket: ${e?.message ?? 'unknown error'}`);
        process.exit(1);
    }
    // ── Step 2: Enable public access ────────────────────────────────────
    log.step('Step 2 of 2 — Enable public access');
    log.message('Go to Storage & databases → R2 object storage → Overview.\n' +
        `Click "${bucket}" → Settings tab → Public Development URL → Enable.\n` +
        'Then paste the URL that appears below (looks like https://pub-xxxx.r2.dev).');
    const publicUrl = await text({
        message: 'Public URL',
        placeholder: 'https://pub-xxxx.r2.dev',
        validate: (v) => v.trim() ? undefined : 'Required',
    });
    // ── Save config ─────────────────────────────────────────────────────
    config.r2_config = { account_id: accountId, bucket, public_url: publicUrl };
    workspace.writeConfig(config);
    const userConfig = loadUserConfig();
    userConfig.r2 = { access_key_id: accessKeyId, secret_access_key: secretAccessKey };
    saveUserConfig(userConfig);
    outro(`All done. Assets will be served from ${publicUrl}`);
});
// ── studiograph r2 upload <dir> ───────────────────────────────────────────
r2Command
    .command('upload <dir>')
    .description('Upload a local directory to the workspace R2 bucket')
    .option('--prefix <prefix>', 'Key prefix in bucket (e.g. fonts/)', '')
    .action(async (dir, opts) => {
    const workspace = new Workspace();
    if (!workspace.isWorkspace()) {
        console.error('Not a Studiograph workspace. Run `studiograph init` first.');
        process.exit(1);
    }
    const config = await workspace.loadConfig();
    const userConfig = loadUserConfig();
    const r2 = config.r2_config;
    const creds = userConfig.r2;
    if (!r2?.account_id || !r2.bucket) {
        console.error('R2 not configured. Run `studiograph r2 setup` first.');
        process.exit(1);
    }
    if (!creds?.access_key_id || !creds.secret_access_key) {
        console.error('R2 credentials not found in user config. Run `studiograph r2 setup` first.');
        process.exit(1);
    }
    const client = buildR2Client({
        accountId: r2.account_id,
        accessKeyId: creds.access_key_id,
        secretAccessKey: creds.secret_access_key,
    });
    const files = walkDir(dir);
    const prefix = opts.prefix.replace(/\/?$/, '/').replace(/^\//, '');
    const s = spinner();
    s.start(`Uploading ${files.length} file(s) to ${r2.bucket}`);
    let uploaded = 0;
    for (const file of files) {
        const relPath = relative(dir, file);
        const key = prefix ? `${prefix}${relPath}` : relPath;
        const body = readFileSync(file);
        await client.send(new PutObjectCommand({
            Bucket: r2.bucket,
            Key: key,
            Body: body,
            ContentType: contentType(file),
            CacheControl: 'public, max-age=31536000, immutable',
        }));
        uploaded++;
        s.message(`${uploaded}/${files.length} — ${key}`);
    }
    s.stop(`Uploaded ${uploaded} file(s)`);
    if (r2.public_url) {
        log.success(`Files available at ${r2.public_url}/${prefix}`);
    }
});
// ── studiograph r2 sync ───────────────────────────────────────────────────
r2Command
    .command('sync')
    .description('Upload local assets from tokens.json and markdown files to R2, rewriting URLs in place')
    .action(async () => {
    const workspace = new Workspace();
    if (!workspace.isWorkspace()) {
        console.error('Not a Studiograph workspace. Run `studiograph init` first.');
        process.exit(1);
    }
    const config = await workspace.loadConfig();
    const userConfig = loadUserConfig();
    const r2 = config.r2_config;
    const creds = userConfig.r2;
    if (!r2?.account_id || !r2.bucket || !r2.public_url) {
        console.error('R2 not configured. Run `studiograph r2 setup` first.');
        process.exit(1);
    }
    if (!creds?.access_key_id || !creds.secret_access_key) {
        console.error('R2 credentials not found. Run `studiograph r2 setup` first.');
        process.exit(1);
    }
    const cwd = process.cwd();
    const repoPaths = config.repos.map((r) => join(cwd, r.path));
    const s = spinner();
    s.start('Syncing local assets to R2...');
    const result = await syncLocalAssetsToR2({
        workspacePath: cwd,
        repoPaths,
        credentials: {
            accountId: r2.account_id,
            accessKeyId: creds.access_key_id,
            secretAccessKey: creds.secret_access_key,
        },
        bucket: r2.bucket,
        publicUrl: r2.public_url,
        onProgress: (p) => {
            if (p.currentFile)
                s.message(`Uploading ${p.currentFile}`);
        },
    });
    s.stop(result.uploaded > 0
        ? `Uploaded ${result.uploaded} asset(s), rewrote ${result.rewritten} file(s)` +
            (result.failed > 0 ? ` (${result.failed} failed)` : '')
        : 'No local asset references found');
    if (result.uploaded > 0) {
        log.success(`Assets available at ${r2.public_url}/`);
    }
});
// Entity-typed prefixes that the GET /api/assets/:type/:id/:filename
// route can resolve. The route does authz via findEntityById regardless
// of the entity type, so any of these prefixes is safe to rewrite. New
// uploads land under `asset/`; legacy image references uploaded before
// Stage 2 sit under `reference/` (per docs/asset-architecture.md §1.5
// — those `reference` entities were intentionally never migrated to
// the new `asset` type). Both shapes need to migrate from absolute R2
// URLs to proxied paths before the bucket can flip private.
//
// Other top-level prefixes that may appear under the same R2 bucket
// (`fonts/`, `tokens/`, `<app>/theme/...` from `r2 sync`) are NOT
// entity-backed — the proxy can't resolve them. We leave those alone.
const ENTITY_ASSET_PREFIXES = ['asset', 'reference'];
function rewriteR2UrlsInText(text, publicUrlPrefix) {
    const escapedPrefix = publicUrlPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    // One alternation across all entity-asset prefixes:
    // `<public>/(asset|reference)/...` → `/api/assets/(asset|reference)/...`.
    // Capture group preserves the entity-type segment in the rewrite.
    const re = new RegExp(`${escapedPrefix}/(${ENTITY_ASSET_PREFIXES.join('|')})/`, 'g');
    let count = 0;
    const out = text.replace(re, (_, entityType) => {
        count++;
        return `/api/assets/${entityType}/`;
    });
    return { text: out, rewrites: count };
}
async function migrateRepo(repoPath, publicUrlPrefix) {
    const stats = {
        filesScanned: 0, filesRewritten: 0, rewrites: 0, rewrittenFiles: [],
    };
    if (!existsSync(repoPath))
        return stats;
    const mdFiles = walkMarkdown(repoPath);
    for (const mdFile of mdFiles) {
        stats.filesScanned++;
        const original = readFileSync(mdFile, 'utf8');
        if (!original.includes(publicUrlPrefix))
            continue;
        const { text: updated, rewrites } = rewriteR2UrlsInText(original, publicUrlPrefix);
        if (rewrites === 0 || updated === original)
            continue;
        writeFileSync(mdFile, updated);
        stats.filesRewritten++;
        stats.rewrites += rewrites;
        stats.rewrittenFiles.push(mdFile);
    }
    return stats;
}
/**
 * Migration-safe markdown walker: skips VCS / dependency / studiograph
 * internals and refuses to follow symlinks. The shared `walkDir` at the
 * top of this file recurses into everything (it's used for fonts/asset
 * uploads where that's the right behavior); the migration touches every
 * .md file it sees, so it must be more conservative.
 */
function walkMarkdown(dir) {
    const SKIP_DIRS = new Set(['.git', 'node_modules', '.studiograph', '.collaborator', 'dist', 'build']);
    const files = [];
    const visit = (d) => {
        let entries;
        try {
            entries = readdirSync(d);
        }
        catch {
            return;
        }
        for (const entry of entries) {
            if (SKIP_DIRS.has(entry))
                continue;
            const full = join(d, entry);
            let st;
            try {
                st = lstatSync(full);
            }
            catch {
                continue;
            }
            if (st.isSymbolicLink())
                continue;
            if (st.isDirectory())
                visit(full);
            else if (st.isFile() && full.endsWith('.md'))
                files.push(full);
        }
    };
    visit(dir);
    return files;
}
r2Command
    .command('migrate-urls')
    .description('Rewrite legacy absolute R2 URLs in entities to proxied /api/assets/* paths (Stage 2). Idempotent.')
    .option('--dry-run', 'Show what would change without writing or committing')
    .action(async (opts) => {
    const workspace = new Workspace();
    if (!workspace.isWorkspace()) {
        console.error('Not a Studiograph workspace. Run `studiograph init` first.');
        process.exit(1);
    }
    const config = await workspace.loadConfig();
    const r2 = config.r2_config;
    if (!r2?.public_url) {
        console.error('No r2_config.public_url in workspace.json — nothing to migrate.');
        process.exit(1);
    }
    // Anchor strictly on the configured public URL with trailing slash
    // stripped, so a typo like `https://pub-xxx.r2.dev` won't bleed into
    // unrelated `https://pub-xxxabc.example/...` matches.
    const prefix = r2.public_url.replace(/\/+$/, '');
    intro(opts.dryRun ? 'r2 migrate-urls — dry run' : 'r2 migrate-urls');
    const cwd = workspace.getWorkspacePath();
    let totalRewrites = 0;
    let totalFilesRewritten = 0;
    let totalFilesScanned = 0;
    // Per-repo list of files we actually rewrote, so the commit step
    // stages exactly those paths instead of `git add -A` (which would
    // sweep up unrelated dirty changes — agent-staged entries, WIP,
    // anything pending in the workspace at run time).
    const rewrittenByRepo = new Map();
    for (const repo of config.repos) {
        const repoPath = join(cwd, repo.path);
        if (opts.dryRun) {
            const counts = countRepoMatches(repoPath, prefix);
            totalFilesScanned += counts.filesScanned;
            totalFilesRewritten += counts.filesWithMatches;
            totalRewrites += counts.rewrites;
            if (counts.rewrites > 0) {
                log.info(`${repo.name}: ${counts.rewrites} rewrites across ${counts.filesWithMatches} file(s)`);
            }
            continue;
        }
        const stats = await migrateRepo(repoPath, prefix);
        totalFilesScanned += stats.filesScanned;
        totalFilesRewritten += stats.filesRewritten;
        totalRewrites += stats.rewrites;
        if (stats.rewrittenFiles.length > 0) {
            rewrittenByRepo.set(repo.name, stats.rewrittenFiles);
        }
        if (stats.rewrites > 0) {
            log.info(`${repo.name}: ${stats.rewrites} rewrites across ${stats.filesRewritten} file(s)`);
        }
    }
    if (totalRewrites === 0) {
        outro(`Nothing to migrate (scanned ${totalFilesScanned} files, no R2 URLs matching ${prefix}).`);
        return;
    }
    if (opts.dryRun) {
        outro(`Dry run: would rewrite ${totalRewrites} URL(s) in ${totalFilesRewritten} file(s).`);
        return;
    }
    // One commit per repo, staging ONLY the files this command rewrote
    // (never `git add -A`). Uses the local git user.name/user.email
    // already configured for the repo (no override) — this is a
    // maintenance command, not user-attributed.
    for (const repo of config.repos) {
        const files = rewrittenByRepo.get(repo.name);
        if (!files || files.length === 0)
            continue;
        const repoPath = join(cwd, repo.path);
        // Pass repo-relative paths to git add so the same commit message
        // works regardless of where the rewritten file sat in the tree.
        const relFiles = files.map(f => relative(repoPath, f));
        const addRes = spawnSync('git', ['add', '--', ...relFiles], { cwd: repoPath, encoding: 'utf8' });
        if (addRes.status !== 0) {
            log.warn(`${repo.name}: git add failed — skipping commit (${addRes.stderr?.trim()})`);
            continue;
        }
        const commitRes = spawnSync('git', ['commit', '-m', 'chore(assets): migrate R2 URLs to /api/assets/* paths'], { cwd: repoPath, encoding: 'utf8' });
        if (commitRes.status !== 0) {
            log.warn(`${repo.name}: git commit failed (${commitRes.stderr?.trim()})`);
        }
    }
    outro(`Rewrote ${totalRewrites} URL(s) in ${totalFilesRewritten} file(s) across ${config.repos.length} repo(s).`);
});
/** Dry-run counter: count matches without modifying any file. */
function countRepoMatches(repoPath, publicUrlPrefix) {
    const result = { filesScanned: 0, filesWithMatches: 0, rewrites: 0 };
    if (!existsSync(repoPath))
        return result;
    const mdFiles = walkMarkdown(repoPath);
    for (const mdFile of mdFiles) {
        result.filesScanned++;
        const text = readFileSync(mdFile, 'utf8');
        if (!text.includes(publicUrlPrefix))
            continue;
        const { rewrites } = rewriteR2UrlsInText(text, publicUrlPrefix);
        if (rewrites > 0) {
            result.filesWithMatches++;
            result.rewrites += rewrites;
        }
    }
    return result;
}
//# sourceMappingURL=r2.js.map