#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Initiative Plan Command (WU-1105, renamed in WU-1193)
 *
 * Links plan files to initiatives by setting the `related_plan` field
 * in the initiative YAML.
 *
 * Usage:
 *   pnpm initiative:plan --initiative INIT-001 --plan <plansDir>/my-plan.md
 *   pnpm initiative:plan --initiative INIT-001 --create  # Create new plan template
 *
 * Features:
 * - Validates initiative exists before modifying
 * - Formats plan path as lumenflow:// URI
 * - Idempotent: no error if same plan already linked
 * - Warns if replacing existing plan link
 * - Can create plan templates with --create
 *
 * Context: WU-1105 (INIT-003 Phase 3a), renamed from init:plan in WU-1193
 */
import { getGitForCwd } from '@lumenflow/core/git-adapter';
import { die } from '@lumenflow/core/error-handler';
import { existsSync, writeFileSync, mkdirSync, readFileSync } from 'node:fs';
import { join, basename } from 'node:path';
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/arg-parser';
import { INIT_PATHS } from '@lumenflow/initiatives/paths';
import { isValidConfiguredInitiativeId } from '@lumenflow/initiatives/constants';
import { ensureOnMain } from '@lumenflow/core/wu-helpers';
import { withMicroWorktree, isRetryExhaustionError as coreIsRetryExhaustionError, formatRetryExhaustionError as coreFormatRetryExhaustionError, } from '@lumenflow/core/micro-worktree';
import { readInitiative } from '@lumenflow/initiatives/yaml';
import { parseYAML, stringifyYAML } from '@lumenflow/core/wu-yaml';
import { LOG_PREFIX as CORE_LOG_PREFIX } from '@lumenflow/core/wu-constants';
import { createWuPaths, WU_PATHS } from '@lumenflow/core/wu-paths';
/** Log prefix for console output */
export const LOG_PREFIX = CORE_LOG_PREFIX.INITIATIVE_PLAN;
/** Micro-worktree operation name */
const OPERATION_NAME = 'initiative-plan';
/**
 * WU-1621: operation-level push retry override for initiative:plan.
 */
export const INITIATIVE_PLAN_PUSH_RETRY_OVERRIDE = {
    retries: 8,
    min_delay_ms: 300,
    max_delay_ms: 4000,
};
/** LumenFlow URI scheme for plan references */
const PLAN_URI_SCHEME = 'lumenflow://plans/';
function normalizeToPosix(value) {
    return value.replace(/\\/g, '/');
}
function trimOuterSlashes(value) {
    return normalizeToPosix(value).replace(/^\/+|\/+$/g, '');
}
function resolvePlansDirSegment(projectRoot) {
    try {
        return createWuPaths({ projectRoot }).PLANS_DIR();
    }
    catch {
        return WU_PATHS.PLANS_DIR();
    }
}
function formatPlanUriFromPlansDir(planPath, plansDirSegment) {
    const normalizedPlanPath = normalizeToPosix(planPath);
    const normalizedPlansDir = trimOuterSlashes(plansDirSegment);
    if (normalizedPlansDir.length === 0) {
        return null;
    }
    const plansPrefix = `${normalizedPlansDir}/`;
    if (normalizedPlanPath.startsWith(plansPrefix)) {
        const relativePath = normalizedPlanPath.slice(plansPrefix.length);
        if (relativePath.length > 0) {
            return `${PLAN_URI_SCHEME}${relativePath}`;
        }
    }
    const embeddedPrefix = `/${plansPrefix}`;
    const embeddedIndex = normalizedPlanPath.lastIndexOf(embeddedPrefix);
    if (embeddedIndex !== -1) {
        const relativePath = normalizedPlanPath.slice(embeddedIndex + embeddedPrefix.length);
        if (relativePath.length > 0) {
            return `${PLAN_URI_SCHEME}${relativePath}`;
        }
    }
    return null;
}
/**
 * Custom option for plan file path
 */
const PLAN_OPTION = {
    name: 'plan',
    flags: '--plan <path>',
    description: 'Path to plan file (markdown)',
};
/**
 * Custom option for creating new plan template
 */
const CREATE_OPTION = {
    name: 'create',
    flags: '--create',
    description: 'Create a new plan template instead of linking existing file',
};
/**
 * Check if an error is a push retry exhaustion error.
 */
export function isRetryExhaustionError(error) {
    return coreIsRetryExhaustionError(error);
}
/**
 * Format retry exhaustion error with actionable command guidance.
 */
export function formatRetryExhaustionError(error, initId, opts = {}) {
    const command = opts.create
        ? `pnpm initiative:plan --initiative ${initId} --create`
        : `pnpm initiative:plan --initiative ${initId} --plan ${opts.planPath ?? '<path-to-plan.md>'}`;
    return coreFormatRetryExhaustionError(error, { command });
}
function parseInitiativePlanPayload(rawText, initId) {
    const parsed = parseYAML(rawText);
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        die(`Invalid initiative payload for ${initId}: YAML root must be an object`);
    }
    const doc = parsed;
    if (doc.id !== initId) {
        die(`Initiative YAML id mismatch. Expected ${initId}, found ${doc.id}`);
    }
    const relatedPlan = doc.related_plan;
    if (relatedPlan !== undefined && typeof relatedPlan !== 'string') {
        die(`Invalid related_plan in ${initId}: expected string when present, found ${typeof relatedPlan}`);
    }
    return doc;
}
/**
 * Validate Initiative ID format
 * @param id - Initiative ID to validate
 * @throws Error if format is invalid
 */
export function validateInitIdFormat(id) {
    if (!isValidConfiguredInitiativeId(id)) {
        die(`Invalid Initiative ID format: "${id}"\n\n` +
            `Expected format: INIT-<number> or INIT-NAME (e.g., INIT-001, INIT-TOOLING)`);
    }
}
/**
 * Validate plan file path
 * @param planPath - Path to plan file
 * @throws Error if path is invalid or file doesn't exist
 */
export function validatePlanPath(planPath) {
    if (!planPath.endsWith('.md')) {
        die(`Invalid plan file format: "${planPath}"\n\nPlan files must be markdown (.md)`);
    }
    if (!existsSync(planPath)) {
        die(`Plan file not found: "${planPath}"\n\nUse --create to create a new plan template`);
    }
}
/**
 * Determine whether a plan path points outside the repo's configured plansDir.
 *
 * External paths (e.g. ~/.claude/plans/foo.md or /tmp/plan.md) need to be
 * imported into the repo before linking. Paths already inside plansDir or
 * lumenflow:// URIs are considered internal.
 *
 * @param planPath - Path to plan file
 * @param projectRoot - Project root directory
 * @returns true if the path is external and needs importing
 */
export function isExternalPlanPath(planPath, projectRoot) {
    // lumenflow:// URIs are already resolved references, not external files
    if (planPath.startsWith(PLAN_URI_SCHEME)) {
        return false;
    }
    const normalizedPath = normalizeToPosix(planPath);
    const plansDirSegment = resolvePlansDirSegment(projectRoot);
    const absolutePlansDir = normalizeToPosix(join(projectRoot, plansDirSegment));
    // Check if the path is inside the repo's plansDir (absolute or relative)
    if (normalizedPath.startsWith(absolutePlansDir + '/')) {
        return false;
    }
    // Check relative path match (e.g. "docs/operations/plans/foo.md")
    const relativePlansPrefix = trimOuterSlashes(plansDirSegment) + '/';
    if (normalizedPath.startsWith(relativePlansPrefix)) {
        return false;
    }
    return true;
}
/**
 * Import an external plan file into the repo's configured plansDir.
 *
 * Copies the file content into plansDir with an initiative-prefixed filename.
 * The original external file is not modified or deleted.
 *
 * @param worktreePath - Path to repo root or worktree
 * @param initId - Initiative ID (used as filename prefix)
 * @param externalPath - Absolute path to the external plan file
 * @param title - Initiative title (used for slug in filename)
 * @returns Absolute path to the imported file in plansDir
 * @throws Error if destination file already exists
 */
export function importExternalPlan(worktreePath, initId, externalPath, title) {
    const slug = title
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-|-$/g, '')
        .substring(0, 30);
    const filename = `${initId}-${slug}.md`;
    const plansDir = join(worktreePath, resolvePlansDirSegment(worktreePath));
    const destPath = join(plansDir, filename);
    if (existsSync(destPath)) {
        die(`Plan file already exists: ${destPath}\n\n` +
            `Use --plan ${destPath} to link the existing file, or delete it first.`);
    }
    if (!existsSync(plansDir)) {
        mkdirSync(plansDir, { recursive: true });
    }
    const content = readFileSync(externalPath, { encoding: 'utf-8' });
    writeFileSync(destPath, content, { encoding: 'utf-8' });
    console.log(`${LOG_PREFIX} Imported external plan: ${externalPath} -> ${destPath}`);
    return destPath;
}
/**
 * Format plan path as lumenflow:// URI
 *
 * Extracts the filename (and optional subdirectory) relative to configured
 * `directories.plansDir` and creates a standardized URI for plan references.
 *
 * @param planPath - Path to plan file (can be relative or absolute)
 * @returns lumenflow://plans/<filename> URI
 */
export function formatPlanUri(planPath) {
    const plansDirSegment = resolvePlansDirSegment(process.cwd());
    const uriFromPlansDir = formatPlanUriFromPlansDir(planPath, plansDirSegment);
    if (uriFromPlansDir) {
        return uriFromPlansDir;
    }
    // Fallback: just use the filename
    const filename = basename(planPath);
    return `${PLAN_URI_SCHEME}${filename}`;
}
/**
 * Check if initiative exists and return the document
 * @param initId - Initiative ID to check
 * @returns Initiative document
 * @throws Error if initiative not found
 */
export function checkInitiativeExists(initId) {
    const initPath = INIT_PATHS.INITIATIVE(initId);
    if (!existsSync(initPath)) {
        die(`Initiative not found: ${initId}\n\nFile does not exist: ${initPath}`);
    }
    return readInitiative(initPath, initId);
}
/**
 * Update initiative with plan reference in micro-worktree
 *
 * Uses raw YAML parsing to preserve unknown fields like related_plan
 * that are not in the strict initiative schema.
 *
 * @param worktreePath - Path to micro-worktree
 * @param initId - Initiative ID
 * @param planUri - Plan URI to set
 * @returns True if changes were made, false if already linked
 */
export function updateInitiativeWithPlan(worktreePath, initId, planUri) {
    const initRelPath = INIT_PATHS.INITIATIVE(initId);
    const initAbsPath = join(worktreePath, initRelPath);
    // Read raw YAML to preserve unknown fields like related_plan
    // (readInitiative strips them via zod schema validation)
    const rawText = readFileSync(initAbsPath, { encoding: 'utf-8' });
    const doc = parseInitiativePlanPayload(rawText, initId);
    // Check for existing plan link
    const existingPlan = doc.related_plan;
    if (existingPlan === planUri) {
        // Already linked to same plan - idempotent
        return false;
    }
    if (existingPlan && existingPlan !== planUri) {
        // Different plan already linked - warn but proceed
        console.warn(`${LOG_PREFIX} Replacing existing related_plan: ${existingPlan} -> ${planUri}`);
    }
    // Update related_plan field
    doc.related_plan = planUri;
    const out = stringifyYAML(doc);
    writeFileSync(initAbsPath, out, { encoding: 'utf-8' });
    console.log(`${LOG_PREFIX} Updated ${initId} with related_plan: ${planUri}`);
    return true;
}
/**
 * Create a plan template file, optionally importing content from an external file.
 *
 * When `fromPath` is provided, the external file's content is used instead of
 * generating a blank template. This supports the `--create --plan <path>` workflow
 * where users want to import an existing plan into the repo. (WU-2349)
 *
 * @param worktreePath - Path to repo root or worktree
 * @param initId - Initiative ID
 * @param title - Initiative title
 * @param fromPath - Optional path to external file whose content replaces the template
 * @returns Path to created file
 * @throws Error if file already exists
 */
export function createPlanTemplate(worktreePath, initId, title, fromPath) {
    const slug = title
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-|-$/g, '')
        .substring(0, 30);
    const filename = `${initId}-${slug}.md`;
    const plansDir = join(worktreePath, resolvePlansDirSegment(worktreePath));
    const planPath = join(plansDir, filename);
    if (existsSync(planPath)) {
        die(`Plan file already exists: ${planPath}\n\nUse --plan to link an existing file`);
    }
    // Ensure plans directory exists
    if (!existsSync(plansDir)) {
        mkdirSync(plansDir, { recursive: true });
    }
    let content;
    if (fromPath) {
        content = readFileSync(fromPath, { encoding: 'utf-8' });
        console.log(`${LOG_PREFIX} Imported content from: ${fromPath}`);
    }
    else {
        content = `# ${initId} Plan - ${title}

## Goal

<!-- What is the primary objective of this initiative? -->

## Scope

<!-- What is in scope and out of scope? -->

## Approach

<!-- How will you achieve the goal? Key phases or milestones? -->

## Success Criteria

<!-- How will you know when this is complete? Measurable outcomes? -->

## Risks

<!-- What could go wrong? How will you mitigate? -->

## References

- Initiative: ${initId}
- Created: ${new Date().toISOString().split('T')[0]}
`;
    }
    writeFileSync(planPath, content, { encoding: 'utf-8' });
    console.log(`${LOG_PREFIX} Created plan template: ${planPath}`);
    return planPath;
}
/**
 * Generate commit message for plan link operation
 */
export function getCommitMessage(initId, planUri) {
    const filename = planUri.replace(PLAN_URI_SCHEME, '');
    return `docs: link plan ${filename} to ${initId.toLowerCase()}`;
}
export async function main() {
    const args = createWUParser({
        name: 'init-plan',
        description: 'Link a plan file to an initiative',
        options: [WU_OPTIONS.initiative, PLAN_OPTION, CREATE_OPTION],
        required: ['initiative'],
        allowPositionalId: false,
    });
    const initId = args.initiative;
    const planPath = args.plan;
    const shouldCreate = args.create;
    // Validate inputs
    validateInitIdFormat(initId);
    // Check initiative exists first (before UnsafeAny mutations)
    const initDoc = checkInitiativeExists(initId);
    const initTitle = initDoc.title;
    // Determine plan path and URI
    let targetPlanPath;
    let planUri;
    if (shouldCreate) {
        // Create mode - will create template and link it
        // When --plan is also provided, use its content instead of a blank template (WU-2349)
        const fromPath = planPath;
        if (fromPath) {
            validatePlanPath(fromPath);
            console.log(`${LOG_PREFIX} Creating plan for ${initId} from ${fromPath}...`);
        }
        else {
            console.log(`${LOG_PREFIX} Creating plan template for ${initId}...`);
        }
        // Ensure on main for micro-worktree operations
        await ensureOnMain(getGitForCwd());
        try {
            await withMicroWorktree({
                operation: OPERATION_NAME,
                id: initId,
                logPrefix: LOG_PREFIX,
                pushOnly: true,
                pushRetryOverride: INITIATIVE_PLAN_PUSH_RETRY_OVERRIDE,
                execute: async ({ worktreePath }) => {
                    // Create plan template (with optional imported content from --plan)
                    targetPlanPath = createPlanTemplate(worktreePath, initId, initTitle, fromPath);
                    planUri = formatPlanUri(targetPlanPath);
                    // Update initiative with plan link
                    updateInitiativeWithPlan(worktreePath, initId, planUri);
                    // Return files to commit
                    const planRelPath = targetPlanPath.replace(worktreePath + '/', '');
                    return {
                        commitMessage: getCommitMessage(initId, planUri),
                        files: [planRelPath, INIT_PATHS.INITIATIVE(initId)],
                    };
                },
            });
            console.log(`\n${LOG_PREFIX} Transaction complete!`);
            console.log(`\nPlan Linked:`);
            console.log(`  Initiative: ${initId}`);
            console.log(`  Plan URI:   ${planUri}`);
            console.log(`  File:       ${targetPlanPath}`);
            console.log(`\nNext steps:`);
            console.log(`  1. Edit the plan file with your goals and approach`);
            console.log(`  2. View initiative: pnpm initiative:status ${initId}`);
        }
        catch (error) {
            if (error instanceof Error && isRetryExhaustionError(error)) {
                die(formatRetryExhaustionError(error, initId, { create: true }));
            }
            die(`Transaction failed: ${error.message}\n\n` +
                `Micro-worktree cleanup was attempted automatically.\n` +
                `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
        }
    }
    else if (planPath) {
        // Link existing file mode
        validatePlanPath(planPath);
        // WU-2349: If the plan is external (outside repo plansDir), import it first
        const external = isExternalPlanPath(planPath, process.cwd());
        if (external) {
            console.log(`${LOG_PREFIX} External plan detected, importing into repo...`);
        }
        // For external files, we'll compute planUri after import (inside micro-worktree)
        // For internal files, compute it now for idempotency check
        if (!external) {
            planUri = formatPlanUri(planPath);
            // Check for idempotent case before micro-worktree
            const existingPlan = initDoc.related_plan;
            if (existingPlan === planUri) {
                console.log(`${LOG_PREFIX} Plan already linked (idempotent - no changes needed)`);
                console.log(`\n${LOG_PREFIX} ${initId} already has related_plan: ${planUri}`);
                return;
            }
        }
        console.log(`${LOG_PREFIX} Linking plan to ${initId}...`);
        // Ensure on main for micro-worktree operations
        await ensureOnMain(getGitForCwd());
        try {
            await withMicroWorktree({
                operation: OPERATION_NAME,
                id: initId,
                logPrefix: LOG_PREFIX,
                pushOnly: true,
                pushRetryOverride: INITIATIVE_PLAN_PUSH_RETRY_OVERRIDE,
                execute: async ({ worktreePath }) => {
                    const filesToCommit = [INIT_PATHS.INITIATIVE(initId)];
                    if (external) {
                        // Import external file into plansDir within the micro-worktree
                        const importedPath = importExternalPlan(worktreePath, initId, planPath, initTitle);
                        planUri = formatPlanUri(importedPath);
                        const importedRelPath = importedPath.replace(worktreePath + '/', '');
                        filesToCommit.push(importedRelPath);
                    }
                    else {
                        planUri = formatPlanUri(planPath);
                    }
                    // Update initiative with plan link
                    const changed = updateInitiativeWithPlan(worktreePath, initId, planUri);
                    if (!changed) {
                        console.log(`${LOG_PREFIX} No changes detected (concurrent link operation)`);
                    }
                    return {
                        commitMessage: getCommitMessage(initId, planUri),
                        files: filesToCommit,
                    };
                },
            });
            console.log(`\n${LOG_PREFIX} Transaction complete!`);
            console.log(`\nPlan Linked:`);
            console.log(`  Initiative: ${initId}`);
            console.log(`  Plan URI:   ${planUri}`);
            if (external) {
                console.log(`  Imported:   ${planPath} -> repo plansDir`);
            }
            else {
                console.log(`  File:       ${planPath}`);
            }
            console.log(`\nNext steps:`);
            console.log(`  - View initiative: pnpm initiative:status ${initId}`);
        }
        catch (error) {
            if (error instanceof Error && isRetryExhaustionError(error)) {
                die(formatRetryExhaustionError(error, initId, { planPath }));
            }
            die(`Transaction failed: ${error.message}\n\n` +
                `Micro-worktree cleanup was attempted automatically.\n` +
                `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
        }
    }
    else {
        const planPathExample = `${resolvePlansDirSegment(process.cwd())}/my-plan.md`;
        die('Either --plan or --create is required\n\n' +
            'Usage:\n' +
            `  pnpm init:plan --initiative INIT-001 --plan ${planPathExample}\n` +
            '  pnpm init:plan --initiative INIT-001 --create');
    }
}
// Guard main() for testability - use import.meta.main (WU-1071)
import { runCLI } from './cli-entry-point.js';
if (import.meta.main) {
    void runCLI(main);
}
//# sourceMappingURL=initiative-plan.js.map