// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * WU-1650: Validation helpers for wu:edit command
 *
 * Extracted from wu-edit.ts to dedicated module.
 * All validation logic used by the wu:edit orchestrator lives here.
 */
import { die } from '@lumenflow/core/error-handler';
import { existsSync, readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { parseYAML } from '@lumenflow/core/wu-yaml';
import { defaultWorktreeFrom, WU_PATHS } from '@lumenflow/core/wu-paths';
import { resolve, join, relative, isAbsolute } from 'node:path';
import { createGitForPath } from '@lumenflow/core/git-adapter';
import { getConfig } from '@lumenflow/core/config';
import { WU_STATUS, WU_EXPOSURE_VALUES } from '@lumenflow/core/wu-constants';
import { INIT_PATTERNS } from '@lumenflow/initiatives/constants';
import { INIT_PATHS } from '@lumenflow/initiatives/paths';
import { BRANCH_PR_EDIT_MODE, BLOCKED_EDIT_MODE, resolveInProgressEditMode, } from './wu-state-cloud.js';
/**
 * Edit modes for WU editing
 * WU-1365: Worktree-aware editing support
 */
export const EDIT_MODE = {
    /** Ready WUs: Use micro-worktree on main (existing behavior) */
    MICRO_WORKTREE: 'micro_worktree',
    /** In-progress worktree WUs: Apply edits directly in active worktree (WU-1365) */
    WORKTREE: 'worktree',
    /** In-progress branch-pr WUs: apply edits directly on the claimed branch */
    BRANCH_PR: BRANCH_PR_EDIT_MODE,
};
const AUTHORITATIVE_MAIN_REF = 'origin/main';
function isWuDocument(value) {
    return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function getWuStatus(wu) {
    return typeof wu.status === 'string' ? wu.status : null;
}
function resolveClaimedWorktreePath(doc, id) {
    if (typeof doc.worktree_path === 'string' && doc.worktree_path.trim().length > 0) {
        return resolve(doc.worktree_path);
    }
    const fallbackPath = defaultWorktreeFrom({
        id: typeof doc.id === 'string' ? doc.id : id,
        lane: typeof doc.lane === 'string' ? doc.lane : undefined,
    });
    return fallbackPath ? resolve(fallbackPath) : null;
}
function readWuDocumentFromPath(filePath) {
    if (!existsSync(filePath)) {
        return null;
    }
    const content = readFileSync(filePath, 'utf8');
    const wu = parseYAML(content);
    return isWuDocument(wu) ? wu : null;
}
function readWuDocumentFromWorktree(id, doc) {
    const worktreePath = resolveClaimedWorktreePath(doc, id);
    if (!worktreePath) {
        return null;
    }
    return readWuDocumentFromPath(join(worktreePath, WU_PATHS.WU(id)));
}
function readWuDocumentFromRef(id, ref, repoRoot) {
    try {
        // eslint-disable-next-line sonarjs/no-os-command-from-path -- git is required workflow infrastructure and runs inside the active repo checkout
        const content = execFileSync('git', ['show', `${ref}:${WU_PATHS.WU(id)}`], {
            cwd: repoRoot,
            encoding: 'utf8',
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        const wu = parseYAML(content);
        return isWuDocument(wu) ? wu : null;
    }
    catch {
        return null;
    }
}
export function buildAuthoritativeStateMismatchMessage(id, localStatus, authoritativeStatus, sourceLabel) {
    return (`Cannot edit WU ${id}: local checkout is stale relative to authoritative WU state.\n\n` +
        `Local status: ${localStatus}\n` +
        `${sourceLabel} status: ${authoritativeStatus}\n\n` +
        `wu:edit refuses to continue from the stale local copy because that can clobber claim metadata.\n` +
        `Run pnpm wu:status --id ${id} to inspect the active lifecycle state.\n` +
        `If the WU is claimed, retry from the claimed worktree or branch.\n` +
        `If the mismatch persists, run pnpm wu:recover --id ${id}.`);
}
function resolveInProgressEditableWu(id, wu) {
    const editMode = resolveInProgressEditMode(typeof wu.claimed_mode === 'string' ? wu.claimed_mode : undefined);
    if (editMode === BLOCKED_EDIT_MODE) {
        die(`Cannot edit branch-only WU ${id} via wu:edit.\n\n` +
            `WUs claimed with claimed_mode='branch-only' cannot be edited via wu:edit.\n` +
            `To modify the spec, edit the file directly on the lane branch and commit.`);
    }
    if (editMode === EDIT_MODE.BRANCH_PR) {
        return { wu, editMode: EDIT_MODE.BRANCH_PR, isDone: false };
    }
    const worktreeWu = readWuDocumentFromWorktree(id, wu);
    if (worktreeWu && getWuStatus(worktreeWu) === WU_STATUS.IN_PROGRESS) {
        return { wu: worktreeWu, editMode: EDIT_MODE.WORKTREE, isDone: false };
    }
    return { wu, editMode: EDIT_MODE.WORKTREE, isDone: false };
}
function resolveAuthoritativeDisagreement(id, localWu) {
    const localStatus = getWuStatus(localWu);
    if (localStatus !== WU_STATUS.READY) {
        return null;
    }
    const worktreeWu = readWuDocumentFromWorktree(id, localWu);
    const worktreeStatus = worktreeWu ? getWuStatus(worktreeWu) : null;
    if (worktreeWu && worktreeStatus && worktreeStatus !== localStatus) {
        return { wu: worktreeWu, sourceLabel: 'claimed worktree' };
    }
    const originMainWu = readWuDocumentFromRef(id, AUTHORITATIVE_MAIN_REF, process.cwd());
    const originMainStatus = originMainWu ? getWuStatus(originMainWu) : null;
    if (originMainWu && originMainStatus && originMainStatus !== localStatus) {
        return { wu: originMainWu, sourceLabel: AUTHORITATIVE_MAIN_REF };
    }
    return null;
}
/**
 * WU-1039: Validate which edits are allowed on done WUs
 *
 * Done WUs only allow metadata reassignment: initiative, phase, and exposure.
 * All other edits are blocked to preserve WU immutability after completion.
 *
 * @param opts - Parsed CLI options
 * @returns { valid: boolean, disallowedEdits: string[] }
 */
export function validateDoneWUEdits(opts) {
    const disallowedEdits = [];
    // Check for disallowed edits on done WUs
    if (opts.specFile)
        disallowedEdits.push('--spec-file');
    // WU-2423: Block title changes on done WUs
    if (opts.title)
        disallowedEdits.push('--title');
    if (opts.description)
        disallowedEdits.push('--description');
    if (opts.acceptance && Array.isArray(opts.acceptance) && opts.acceptance.length > 0) {
        disallowedEdits.push('--acceptance');
    }
    if (opts.notes)
        disallowedEdits.push('--notes');
    if (opts.codePaths && Array.isArray(opts.codePaths) && opts.codePaths.length > 0) {
        disallowedEdits.push('--code-paths');
    }
    if (opts.removeCodePaths &&
        Array.isArray(opts.removeCodePaths) &&
        opts.removeCodePaths.length > 0) {
        disallowedEdits.push('--remove-code-paths');
    }
    if (opts.renameCodePaths &&
        Array.isArray(opts.renameCodePaths) &&
        opts.renameCodePaths.length > 0) {
        disallowedEdits.push('--rename-code-paths');
    }
    if (opts.risks && Array.isArray(opts.risks) && opts.risks.length > 0) {
        disallowedEdits.push('--risks');
    }
    if (opts.lane)
        disallowedEdits.push('--lane');
    if (opts.type)
        disallowedEdits.push('--type');
    if (opts.priority)
        disallowedEdits.push('--priority');
    if (opts.testPathsManual &&
        Array.isArray(opts.testPathsManual) &&
        opts.testPathsManual.length > 0) {
        disallowedEdits.push('--test-paths-manual');
    }
    if (opts.removeTestPathsManual &&
        Array.isArray(opts.removeTestPathsManual) &&
        opts.removeTestPathsManual.length > 0) {
        disallowedEdits.push('--remove-test-paths-manual');
    }
    if (opts.renameTestPathsManual &&
        Array.isArray(opts.renameTestPathsManual) &&
        opts.renameTestPathsManual.length > 0) {
        disallowedEdits.push('--rename-test-paths-manual');
    }
    if (opts.testPathsUnit && Array.isArray(opts.testPathsUnit) && opts.testPathsUnit.length > 0) {
        disallowedEdits.push('--test-paths-unit');
    }
    if (opts.removeTestPathsUnit &&
        Array.isArray(opts.removeTestPathsUnit) &&
        opts.removeTestPathsUnit.length > 0) {
        disallowedEdits.push('--remove-test-paths-unit');
    }
    if (opts.renameTestPathsUnit &&
        Array.isArray(opts.renameTestPathsUnit) &&
        opts.renameTestPathsUnit.length > 0) {
        disallowedEdits.push('--rename-test-paths-unit');
    }
    if (opts.testPathsE2e && Array.isArray(opts.testPathsE2e) && opts.testPathsE2e.length > 0) {
        disallowedEdits.push('--test-paths-e2e');
    }
    if (opts.removeTestPathsE2e &&
        Array.isArray(opts.removeTestPathsE2e) &&
        opts.removeTestPathsE2e.length > 0) {
        disallowedEdits.push('--remove-test-paths-e2e');
    }
    if (opts.renameTestPathsE2e &&
        Array.isArray(opts.renameTestPathsE2e) &&
        opts.renameTestPathsE2e.length > 0) {
        disallowedEdits.push('--rename-test-paths-e2e');
    }
    return {
        valid: disallowedEdits.length === 0,
        disallowedEdits,
    };
}
/**
 * WU-1039: Validate exposure value against schema
 *
 * Uses WU_EXPOSURE_VALUES from core constants (Library-First, no magic strings).
 *
 * @param exposure - Exposure value to validate
 * @returns { valid: boolean, error?: string }
 */
export function validateExposureValue(exposure) {
    // WU_EXPOSURE_VALUES is readonly array, need to cast for includes check
    const validValues = WU_EXPOSURE_VALUES;
    if (!validValues.includes(exposure)) {
        return {
            valid: false,
            error: `Invalid exposure value: "${exposure}"\n\nValid values: ${WU_EXPOSURE_VALUES.join(', ')}`,
        };
    }
    return { valid: true };
}
/**
 * WU-1929: Validate initiative ID format
 * @param {string} initId - Initiative ID to validate
 */
export function validateInitiativeFormat(initId) {
    if (!INIT_PATTERNS.INIT_ID.test(initId)) {
        die(`Invalid Initiative ID format: "${initId}"\n\n` +
            `Expected format: INIT-<number> or INIT-<NAME> (e.g., INIT-001, INIT-TOOLING)`);
    }
}
/**
 * WU-1929: Validate initiative exists on disk
 * @param {string} initId - Initiative ID to check
 * @returns {string} Path to initiative file
 */
export function validateInitiativeExists(initId) {
    const initPath = INIT_PATHS.INITIATIVE(initId);
    if (!existsSync(initPath)) {
        die(`Initiative not found: ${initId}\n\nFile does not exist: ${initPath}`);
    }
    return initPath;
}
/**
 * Check WU exists and determine edit mode
 * WU-1365: Now supports worktree-aware editing for in_progress WUs
 *
 * @param {string} id - WU ID
 * @returns {{ wu: object, editMode: string, isDone: boolean }} WU object and edit mode
 */
export function validateWUEditable(id) {
    const wuPath = WU_PATHS.WU(id);
    if (!existsSync(wuPath)) {
        die(`WU ${id} not found at ${wuPath}\n\nEnsure the WU exists and you're in the repo root.`);
    }
    const wu = readWuDocumentFromPath(wuPath);
    if (!wu) {
        die(`WU ${id} YAML is invalid at ${wuPath}\n\nFix the YAML syntax and retry.`);
    }
    // WU-1929: Done WUs allow initiative/phase edits only (metadata reassignment)
    // WU-1365: Other fields on done WUs are immutable
    if (wu.status === WU_STATUS.DONE) {
        // Return done status - main() will validate allowed fields
        return { wu, editMode: EDIT_MODE.MICRO_WORKTREE, isDone: true };
    }
    // Handle in_progress WUs based on claimed_mode (WU-1365)
    if (wu.status === WU_STATUS.IN_PROGRESS) {
        return resolveInProgressEditableWu(id, wu);
    }
    // Ready WUs use micro-worktree (existing behavior)
    if (wu.status === WU_STATUS.READY) {
        const authoritativeWu = resolveAuthoritativeDisagreement(id, wu);
        if (authoritativeWu) {
            const authoritativeStatus = getWuStatus(authoritativeWu.wu);
            if (authoritativeStatus === WU_STATUS.IN_PROGRESS) {
                return resolveInProgressEditableWu(id, authoritativeWu.wu);
            }
            die(buildAuthoritativeStateMismatchMessage(id, WU_STATUS.READY, authoritativeStatus ?? 'unknown', authoritativeWu.sourceLabel));
        }
        return { wu, editMode: EDIT_MODE.MICRO_WORKTREE, isDone: false };
    }
    // Block other statuses (blocked, etc.)
    die(`Cannot edit WU ${id}: status is '${wu.status}'.\n\n` +
        `Only WUs in '${WU_STATUS.READY}' or '${WU_STATUS.IN_PROGRESS}' (worktree mode) can be edited.`);
}
/**
 * Validate worktree exists on disk
 * WU-1365: Required check before worktree editing
 *
 * @param {string} worktreePath - Absolute path to worktree
 * @param {string} id - WU ID (for error messages)
 */
export function validateWorktreeExists(worktreePath, id) {
    if (!existsSync(worktreePath)) {
        die(`Cannot edit WU ${id}: worktree path missing from disk.\n\n` +
            `Expected worktree at: ${worktreePath}\n\n` +
            `The worktree may have been removed or the path is incorrect.\n` +
            `If the worktree was accidentally deleted, you may need to re-claim the WU.`);
    }
}
/**
 * Validate worktree has no uncommitted changes
 * WU-1365: Required check to prevent edit conflicts
 *
 * @param {string} worktreePath - Absolute path to worktree
 * @param {string} id - WU ID (for error messages)
 */
export async function validateWorktreeClean(worktreePath, id) {
    try {
        const gitAdapter = createGitForPath(worktreePath);
        const status = (await gitAdapter.raw(['status', '--porcelain'])).trim();
        if (status !== '') {
            die(`Cannot edit WU ${id}: worktree has uncommitted changes.\n\n` +
                `Uncommitted changes in ${worktreePath}:\n${status}\n\n` +
                `Commit or discard your changes before editing the WU spec:\n` +
                `  cd ${worktreePath}\n` +
                `  git add . && git commit -m "wip: save progress"\n\n` +
                `Then retry wu:edit.`);
        }
    }
    catch (err) {
        die(`Cannot edit WU ${id}: failed to check worktree status.\n\n` +
            `Error: ${err.message}\n\n` +
            `Worktree path: ${worktreePath}`);
    }
}
/**
 * Validate worktree is on expected lane branch
 * WU-1365: Prevents editing WUs in worktrees with mismatched branches
 *
 * @param {string} worktreePath - Absolute path to worktree
 * @param {string} expectedBranch - Expected branch name (e.g., lane/operations-tooling/wu-1365)
 * @param {string} id - WU ID (for error messages)
 */
export async function validateWorktreeBranch(worktreePath, expectedBranch, id) {
    try {
        const gitAdapter = createGitForPath(worktreePath);
        const actualBranch = (await gitAdapter.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
        if (actualBranch !== expectedBranch) {
            die(`Cannot edit WU ${id}: worktree branch does not match expected lane branch.\n\n` +
                `Expected branch: ${expectedBranch}\n` +
                `Actual branch:   ${actualBranch}\n\n` +
                `This may indicate a corrupted worktree state.\n` +
                `Verify the worktree is correctly set up for this WU.`);
        }
    }
    catch (err) {
        die(`Cannot edit WU ${id}: failed to check worktree branch.\n\n` +
            `Error: ${err.message}\n\n` +
            `Worktree path: ${worktreePath}`);
    }
}
function isPathWithin(rootPath, candidatePath) {
    const relativePath = relative(rootPath, candidatePath);
    return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath));
}
/**
 * Validate command execution context for in-progress worktree WUs.
 * WU-2290: Block wu:edit invocations from main checkout for worktree-mode WUs.
 *
 * @param currentCwd - Current process working directory
 * @param worktreePath - Claimed worktree absolute path
 * @param id - WU ID
 * @param retryCommand - Optional copy-paste retry command
 */
export function validateWorktreeExecutionContext(currentCwd, worktreePath, id, retryCommand) {
    const resolvedCwd = resolve(currentCwd);
    const resolvedWorktreePath = resolve(worktreePath);
    if (isPathWithin(resolvedWorktreePath, resolvedCwd)) {
        return;
    }
    const safeRetryCommand = typeof retryCommand === 'string' && retryCommand.trim().length > 0
        ? retryCommand.trim()
        : `pnpm wu:edit --id ${id} <edit-flags>`;
    die(`Cannot edit in_progress WU ${id} from this checkout.\n\n` +
        `Current directory: ${resolvedCwd}\n` +
        `Claimed worktree: ${resolvedWorktreePath}\n\n` +
        `Run from the claimed worktree and retry:\n` +
        `  cd ${resolvedWorktreePath}\n` +
        `  ${safeRetryCommand}`);
}
function toPosixPath(filePath) {
    return filePath.replace(/\\/g, '/');
}
function getRuntimePathClassification(projectRoot) {
    const config = getConfig({ projectRoot });
    const stateEventsPath = toPosixPath(`${config.state.stateDir}/wu-events.jsonl`);
    const backlogPath = toPosixPath(config.directories.backlogPath);
    const statusPath = toPosixPath(config.directories.statusPath);
    const wuDirPrefix = toPosixPath(config.directories.wuDir).replace(/\/?$/, '/');
    return {
        nonScopeRelevantPaths: new Set([stateEventsPath, backlogPath, statusPath]),
        wuDirPrefix,
    };
}
/**
 * WU-1618: Treat backlog/state bookkeeping files as non-scope signals.
 */
export function hasScopeRelevantBranchChanges(changedFiles) {
    const { nonScopeRelevantPaths, wuDirPrefix } = getRuntimePathClassification(process.cwd());
    return changedFiles.some((filePath) => {
        const normalized = toPosixPath(filePath.trim());
        if (!normalized) {
            return false;
        }
        if (nonScopeRelevantPaths.has(normalized)) {
            return false;
        }
        return !normalized.startsWith(wuDirPrefix);
    });
}
/**
 * WU-2386: Fields that can be edited on dirty worktrees without conflict risk.
 *
 * These fields only modify the WU YAML spec (committed via micro-worktree or
 * worktree commit), not source files. Edits limited to these fields skip the
 * clean-worktree check so agents can fill metadata mid-implementation.
 */
const METADATA_ONLY_FIELDS = new Set([
    'notes',
    'replaceNotes',
    'acceptance',
    'replaceAcceptance',
    'risks',
    'replaceRisks',
    'testPathsManual',
    'replaceTestPathsManual',
    'testPathsUnit',
    'replaceTestPathsUnit',
    'testPathsE2e',
    'replaceTestPathsE2e',
    'removeTestPathsManual',
    'renameTestPathsManual',
    'removeTestPathsUnit',
    'renameTestPathsUnit',
    'removeTestPathsE2e',
    'renameTestPathsE2e',
    'escalation_triggers',
]);
/**
 * WU-2386: Structural fields that affect code scope or WU identity.
 *
 * Edits touching any of these fields require a clean worktree because they
 * can interact with branch merge state or change the WU's scope.
 */
const STRUCTURAL_FIELDS = new Set([
    'specFile',
    'title',
    'description',
    'codePaths',
    'removeCodePaths',
    'renameCodePaths',
    'replaceCodePaths',
    'lane',
    'type',
    'priority',
    'initiative',
    'phase',
    'blockedBy',
    'replaceBlockedBy',
    'addDep',
    'replaceDependencies',
    'exposure',
    'plan',
]);
/**
 * WU-2386: Determine whether the requested edits are metadata-only.
 *
 * Returns true when at least one metadata field is being edited and no
 * structural fields are present. This allows skipping the dirty-worktree
 * check for safe, non-conflicting edits like --notes and --acceptance.
 */
export function isMetadataOnlyEdit(opts) {
    let hasMetadataEdit = false;
    for (const [key, value] of Object.entries(opts)) {
        if (STRUCTURAL_FIELDS.has(key)) {
            if (Array.isArray(value) ? value.length > 0 : Boolean(value)) {
                return false;
            }
        }
        if (METADATA_ONLY_FIELDS.has(key)) {
            if (Array.isArray(value) ? value.length > 0 : Boolean(value)) {
                hasMetadataEdit = true;
            }
        }
    }
    return hasMetadataEdit;
}
/**
 * WU-1618: Support `--replace-code-paths <paths>` shorthand by normalizing to
 * `--replace-code-paths --code-paths <paths>` before Commander parsing.
 */
export function normalizeReplaceCodePathsArgv(argv) {
    const normalized = [...argv];
    for (let i = 0; i < normalized.length; i += 1) {
        if (normalized[i] !== '--replace-code-paths') {
            continue;
        }
        const next = normalized[i + 1];
        if (next && !next.startsWith('-')) {
            normalized.splice(i + 1, 0, '--code-paths');
            i += 1;
        }
    }
    return normalized;
}
//# sourceMappingURL=wu-edit-validators.js.map