#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * WU Rename Command (WU-2552)
 *
 * Renames a Work Unit: moves the YAML file, rewrites its id field, moves
 * the .done stamp, appends a wu_renamed event to wu-events.jsonl, and
 * regenerates backlog.md and status.md from the store.
 *
 * Modes:
 * - Default: performs the full filesystem rename + event append + view regen.
 * - --replay-only: skips file renames and only appends the wu_renamed event
 *   plus regenerates the generated views. Used for downstream repos that
 *   already canonicalized their YAML on disk but still need the event
 *   history to catch up so backlog/status regeneration honors the new id.
 *
 * Usage:
 *   pnpm wu:rename --from WU-5 --to WU-0015
 *   pnpm wu:rename --from WU-5 --to WU-0015 --reason 'canonicalize'
 *   pnpm wu:rename --from WU-5 --to WU-0015 --replay-only
 *
 * Related:
 *   wu-id-format primitives (canonicalizeWuId, isCanonicalWuId)
 *   wu-state-schema WU_EVENT_TYPE.WU_RENAMED
 *   backlog-generator rename projection
 */
import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
import path from 'node:path';
import { getGitForCwd } from '@lumenflow/core/git-adapter';
import { die } from '@lumenflow/core/error-handler';
import { generateBacklog, generateStatus } from '@lumenflow/core/backlog-generator';
import { WUStateStore } from '@lumenflow/core/wu-state-store';
import { WU_EVENT_TYPE } from '@lumenflow/core/wu-state-schema';
import { WU_PATHS } from '@lumenflow/core/wu-paths';
import { ensureOnMain } from '@lumenflow/core/wu-helpers';
import { withMicroWorktree } from '@lumenflow/core/micro-worktree';
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/arg-parser';
import { FILE_SYSTEM, MICRO_WORKTREE_OPERATIONS, PATTERNS } from '@lumenflow/core/wu-constants';
import { canonicalizeWuId, isCanonicalWuId, loadWuIdConfig } from '@lumenflow/core/wu-id-format';
import { runCLI } from './cli-entry-point.js';
import { resolveStateDir, resolveWuEventsRelativePath } from './state-path-resolvers.js';
const PREFIX = '[wu-rename]';
/**
 * Rewrite the `id:` field inside a WU YAML file without otherwise touching
 * it. A minimal textual replacement is preferred over full parse/stringify
 * so that comments and key ordering are preserved.
 */
function rewriteIdField(yamlPath, newId) {
    const body = readFileSync(yamlPath, { encoding: 'utf-8' });
    const lines = body.split('\n');
    let found = false;
    for (let i = 0; i < lines.length; i += 1) {
        const line = lines[i] ?? '';
        if (/^id:\s*/.test(line)) {
            lines[i] = `id: ${newId}`;
            found = true;
            break;
        }
    }
    if (!found) {
        throw new Error(`${PREFIX} YAML file ${yamlPath} has no id: field`);
    }
    writeFileSync(yamlPath, lines.join('\n'), { encoding: 'utf-8' });
}
/**
 * Move a file atomically (rename). Returns true when the source existed.
 */
function moveIfExists(src, dest) {
    if (!existsSync(src)) {
        return false;
    }
    renameSync(src, dest);
    return true;
}
/**
 * Execute a rename transaction on the given filesystem tree. Pure with
 * respect to git/process state; callers that need a git-committed rename
 * should wrap this in a micro-worktree.
 *
 * Validates input, performs the file operations (unless replayOnly),
 * appends the wu_renamed event, and regenerates backlog.md and status.md
 * from the updated state.
 */
export async function performRenameTransaction(input) {
    const { from, to, reason, replayOnly, wuDir, stampsDir, stateDir, backlogPath, statusPath } = input;
    if (!from || !to) {
        throw new Error(`${PREFIX} from and to are required`);
    }
    if (from === to) {
        throw new Error(`${PREFIX} from and to must differ (got ${from})`);
    }
    const filesTouched = [];
    const warnings = [];
    const fromYaml = path.join(wuDir, `${from}.yaml`);
    const toYaml = path.join(wuDir, `${to}.yaml`);
    const fromStamp = path.join(stampsDir, `${from}.done`);
    const toStamp = path.join(stampsDir, `${to}.done`);
    if (!replayOnly) {
        if (!existsSync(fromYaml)) {
            throw new Error(`${PREFIX} source WU YAML not found: ${fromYaml}`);
        }
        if (existsSync(toYaml)) {
            throw new Error(`${PREFIX} target WU YAML already exists: ${toYaml}`);
        }
        // Move yaml and rewrite id field.
        renameSync(fromYaml, toYaml);
        rewriteIdField(toYaml, to);
        filesTouched.push(fromYaml, toYaml);
        // Move stamp if present.
        if (moveIfExists(fromStamp, toStamp)) {
            filesTouched.push(fromStamp, toStamp);
        }
    }
    // Append wu_renamed event via the state store so the in-memory view is
    // updated atomically with the event log append.
    const store = new WUStateStore(stateDir);
    await store.load();
    await store.renameWu(from, to, reason);
    const eventsPath = path.join(stateDir, 'wu-events.jsonl');
    filesTouched.push(eventsPath);
    // Regenerate backlog.md and status.md. The store's resolveRenamedWuId()
    // is consulted by the backlog generator to skip yaml-scanned entries
    // whose id has been renamed in the log.
    const backlogContent = await generateBacklog(store, { wuDir });
    writeFileSync(backlogPath, backlogContent, { encoding: 'utf-8' });
    filesTouched.push(backlogPath);
    const statusContent = await generateStatus(store);
    writeFileSync(statusPath, statusContent, { encoding: 'utf-8' });
    filesTouched.push(statusPath);
    // Construct the event we just appended for the return value.
    const event = {
        type: WU_EVENT_TYPE.WU_RENAMED,
        wuId: from,
        from,
        to,
        ...(reason ? { reason } : {}),
        // timestamp is set inside store.renameWu; we recover the latest
        // wu_renamed event from the re-loaded store for a faithful echo.
        timestamp: new Date().toISOString(),
    };
    return { filesTouched, warnings, event };
}
/**
 * Determine whether a string is a plausible WU id in either canonical or
 * legacy form. Used for --from validation, which must accept the legacy
 * form so that downstream users can migrate WU-5 to WU-0015.
 */
function assertLooksLikeWuId(label, id) {
    if (!PATTERNS.WU_ID.test(id)) {
        die(`${PREFIX} ${label}: invalid WU id format '${id}' (expected 'WU-\\d+')`);
    }
}
/**
 * Command entry point.
 */
export async function main() {
    const args = createWUParser({
        name: 'wu-rename',
        description: 'Rename a Work Unit (canonical WU IDs). Moves YAML, rewrites id field, ' +
            'moves stamp, appends wu_renamed event, and regenerates backlog/status.',
        options: [WU_OPTIONS.from, WU_OPTIONS.to, WU_OPTIONS.reason, WU_OPTIONS.replayOnly],
        required: ['from', 'to'],
    });
    const from = args.from.toUpperCase();
    const to = args.to.toUpperCase();
    const replayOnly = Boolean(args.replayOnly);
    const reason = args.reason;
    assertLooksLikeWuId('--from', from);
    assertLooksLikeWuId('--to', to);
    if (from === to) {
        die(`${PREFIX} --from and --to must differ (both are ${from})`);
    }
    // Strict mode canonicalization guard: when the configured wu_id.strict
    // is true, refuse a --to that is not already in canonical form so
    // users don't accidentally rename to another legacy id.
    const wuIdConfig = loadWuIdConfig();
    if (wuIdConfig.strict && !isCanonicalWuId(to, wuIdConfig)) {
        const suggested = canonicalizeWuId(to, wuIdConfig);
        die(`${PREFIX} --to '${to}' is not canonical (strict mode is enabled).\n\n` +
            `Canonical form for width=${wuIdConfig.width} and prefix='${wuIdConfig.prefix}'` +
            (suggested ? ` would be '${suggested}'.` : '.') +
            `\n\nOptions:\n` +
            `  1. Use the canonical form: pnpm wu:rename --from ${from} --to ${suggested ?? to}\n` +
            `  2. Disable strict mode: pnpm config:set --key software_delivery.wu_id.strict --value false`);
    }
    await ensureOnMain(getGitForCwd());
    // Wrap in a micro-worktree so the commit lands on origin/main without
    // pre-commit hook interference.
    await withMicroWorktree({
        operation: MICRO_WORKTREE_OPERATIONS.WU_BLOCK, // reuse an existing op id
        id: to,
        logPrefix: PREFIX,
        pushOnly: true,
        execute: async ({ worktreePath }) => {
            const microInput = {
                from,
                to,
                replayOnly,
                ...(reason ? { reason } : {}),
                wuDir: path.join(worktreePath, WU_PATHS.WU_DIR()),
                stampsDir: path.join(worktreePath, WU_PATHS.STAMPS_DIR()),
                stateDir: resolveStateDir(worktreePath),
                backlogPath: path.join(worktreePath, WU_PATHS.BACKLOG()),
                statusPath: path.join(worktreePath, WU_PATHS.STATUS()),
            };
            await performRenameTransaction(microInput);
            return {
                commitMessage: `wu(${to.toLowerCase()}): rename from ${from} (${replayOnly ? 'replay-only' : 'canonical'})${reason ? ' — ' + reason : ''}`,
                files: [
                    WU_PATHS.WU(from),
                    WU_PATHS.WU(to),
                    WU_PATHS.STAMP(from),
                    WU_PATHS.STAMP(to),
                    WU_PATHS.BACKLOG(),
                    WU_PATHS.STATUS(),
                    resolveWuEventsRelativePath(worktreePath),
                ],
            };
        },
    });
    console.log(`\n${PREFIX} WU renamed and pushed.`);
    console.log(`- From: ${from}`);
    console.log(`- To:   ${to}`);
    if (reason)
        console.log(`- Reason: ${reason}`);
    if (replayOnly)
        console.log(`- Mode: --replay-only (yaml and stamp not moved)`);
    // Unused local to silence the linter for imports that only matter in
    // the micro-worktree path above.
    void FILE_SYSTEM;
    void unlinkSync;
}
// WU-1537: Use import.meta.main + runCLI for consistent EPIPE and error handling
if (import.meta.main) {
    void runCLI(main);
}
//# sourceMappingURL=wu-rename.js.map