/**
 * studiograph admin migrate-assets — one-time re-key of legacy asset URLs.
 *
 * Rewrites every legacy `/api/assets/{type}/{id}/{filename}` reference (in
 * entity bodies AND `asset_url` frontmatter) to the immutable-key scheme
 * `/api/assets/{assetId}/{filename}`, copying the blob old→new and recording
 * authoritative ownership in assets.db. After this runs (and the legacy route
 * is dropped), renaming/retyping an entity can never orphan its images.
 *
 * Safety:
 *   - `--dry-run` (default OFF): reports counts + missing blobs, writes nothing.
 *   - Idempotent: a `migration_ledger` keyed by (repo, oldKey) means a re-run
 *     reuses the same assetId (no duplicate blobs). New-scheme (2-segment) URLs
 *     never match the extractor, so already-migrated content is skipped.
 *   - Refuses if a target repo has uncommitted changes (catches half-edits).
 *   - Copies (never moves) blobs, so the legacy route keeps serving until the
 *     separate old-blob cleanup step runs. Missing source blobs are reported
 *     and their URL left legacy (rewriting would just relocate the 404).
 *   - One commit per repo.
 *
 * This is code only — running it against production is a deliberate, backed-up
 * step (see ~/.claude/plans/rename-carries-assets.md §6).
 */
import { Command } from 'commander';
import { intro, outro, log, spinner } from '@clack/prompts';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { randomUUID } from 'node:crypto';
import { glob } from 'glob';
import { Workspace } from '../../../core/workspace.js';
import { WorkspaceManager } from '../../../core/workspace-manager.js';
import { gitHasUncommittedChanges } from '../../../utils/git.js';
import { extractLegacyAssetUrls } from '../../../core/embeds.js';
import { detectContentType, getMediaType, publicUrlForAssetId } from '../../../services/assets/index.js';
const EXCLUDED_DIRS = ['.git', '.studiograph', 'node_modules', '.svelte-kit', 'dist', 'build'];
/**
 * Core migration loop — decoupled from clack/Workspace so it's unit-testable
 * with a real local AssetService + AssetIndexService over a temp workspace.
 */
export async function runAssetMigration(deps) {
    const { cwd, repos, assetService, assetIndex, dryRun } = deps;
    const report = { totalUrls: 0, migrated: 0, missing: 0, filesChanged: 0, missingKeys: [] };
    for (const repo of repos) {
        const repoPath = join(cwd, repo.path);
        if (!existsSync(repoPath))
            continue;
        const files = await glob('**/*.md', {
            cwd: repoPath, absolute: true, dot: false,
            ignore: EXCLUDED_DIRS.map(d => `**/${d}/**`),
        });
        let repoChangedFiles = 0;
        for (const file of files) {
            const raw = readFileSync(file, 'utf-8');
            const refs = extractLegacyAssetUrls(raw);
            if (refs.length === 0)
                continue;
            // Rewrite longest URL first: a shorter URL can be a prefix of a longer
            // one (extensionless filenames), and split/join on the prefix would
            // corrupt the longer match. Length-descending order avoids it.
            refs.sort((a, b) => b.url.length - a.url.length);
            let content = raw;
            for (const ref of refs) {
                report.totalUrls++;
                const [type, id, filename] = ref.oldKey.split('/');
                const present = await assetService.exists(type, id, filename).catch(() => false);
                if (!present) {
                    report.missing++;
                    report.missingKeys.push(ref.oldKey);
                    continue; // leave the URL legacy — rewriting would just relocate the 404
                }
                // Idempotent assetId via the ledger keyed by (repo, oldKey).
                let assetId = assetIndex.ledgerGet(repo.name, ref.oldKey);
                if (!assetId) {
                    assetId = randomUUID();
                    if (!dryRun)
                        assetIndex.ledgerPut(repo.name, ref.oldKey, assetId);
                }
                if (!dryRun) {
                    await assetService.copy(ref.oldKey, `${assetId}/${filename}`);
                    const ctype = detectContentType(filename);
                    assetIndex.put({
                        assetId, repo: repo.name, ownerType: ref.type, ownerId: ref.id,
                        filename, contentType: ctype, mediaType: getMediaType(ctype, filename), name: filename,
                    });
                }
                content = content.split(ref.url).join(publicUrlForAssetId(assetId, filename));
                report.migrated++;
            }
            if (content !== raw) {
                repoChangedFiles++;
                if (!dryRun)
                    writeFileSync(file, content, 'utf-8');
            }
        }
        report.filesChanged += repoChangedFiles;
        if (repoChangedFiles > 0 && !dryRun) {
            deps.commit?.(repo.name, `Migrate ${repoChangedFiles} file(s) to immutable asset keys`);
        }
        deps.onRepo?.(repo.name, repoChangedFiles);
    }
    return report;
}
export const adminMigrateAssetsCommand = new Command('migrate-assets')
    .description('Re-key legacy /api/assets/{type}/{id}/{file} references to immutable {assetId} keys')
    .option('--dry-run', 'Report what would change (files, URLs, missing blobs); write nothing')
    .option('--repos <list>', 'Comma-separated repo names to limit the migration to')
    .action(async (opts) => {
    intro(opts.dryRun ? 'Asset key migration (DRY RUN)' : 'Asset key migration');
    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();
    const repoFilter = opts.repos
        ? new Set(opts.repos.split(',').map(s => s.trim()).filter(Boolean))
        : null;
    const targetRepos = config.repos.filter(r => !repoFilter || repoFilter.has(r.name));
    if (targetRepos.length === 0) {
        outro('No repos matched.');
        process.exit(1);
        return;
    }
    // Uncommitted-changes guard (skip in dry-run — we write nothing).
    if (!opts.dryRun) {
        const dirty = targetRepos
            .map(r => ({ r, p: join(process.cwd(), r.path) }))
            .filter(({ p }) => existsSync(p) && gitHasUncommittedChanges(p))
            .map(({ r }) => r.name);
        if (dirty.length > 0) {
            outro(`Refusing — uncommitted changes in: ${dirty.join(', ')}. Commit/stash first.`);
            process.exit(1);
            return;
        }
    }
    const gitUser = { id: 'migration', name: 'Asset Migration', email: 'migration@studiograph.local' };
    const wm = new WorkspaceManager(process.cwd(), config, gitUser);
    const assetIndex = wm.getAssetIndex();
    // Backend is workspace-global, so any configured graph's service works.
    let assetService;
    for (const r of targetRepos) {
        try {
            assetService = wm.getGraph(r.name).getAssetService();
            if (assetService)
                break;
        }
        catch { /* skip */ }
    }
    if (!assetService) {
        outro('Asset storage is not configured for this workspace — nothing to migrate.');
        process.exit(1);
        return;
    }
    const s = spinner();
    s.start('Migrating asset references…');
    const report = await runAssetMigration({
        cwd: process.cwd(),
        repos: targetRepos.map(r => ({ name: r.name, path: r.path })),
        assetService,
        assetIndex,
        dryRun: !!opts.dryRun,
        commit: (repoName, message) => {
            wm.getGraph(repoName).getGitService()?.commitAll(message, gitUser);
        },
    });
    s.stop('Scan complete');
    assetIndex.close();
    log.info(`URLs found: ${report.totalUrls}\n` +
        `${opts.dryRun ? 'Would migrate' : 'Migrated'}: ${report.migrated}\n` +
        `Files ${opts.dryRun ? 'to change' : 'changed'}: ${report.filesChanged}\n` +
        `Missing source blobs (left legacy): ${report.missing}`);
    if (report.missing > 0) {
        log.warn(`Missing blobs (first 10):\n  ${report.missingKeys.slice(0, 10).join('\n  ')}`);
    }
    // Count-verify: every found URL is either migrated or accounted-for as missing.
    if (report.migrated + report.missing !== report.totalUrls) {
        outro(`COUNT MISMATCH: migrated(${report.migrated}) + missing(${report.missing}) != found(${report.totalUrls}). Investigate before dropping the legacy route.`);
        process.exit(1);
        return;
    }
    outro(opts.dryRun ? 'Dry run complete — no changes written.' : 'Migration complete.');
});
//# sourceMappingURL=migrate-assets.js.map