#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * @file lane-edit.ts
 * WU-1854: Safe in-place lane definition editing via micro-worktree
 *
 * Enables editing lane definitions in workspace.yaml without
 * directly writing to main. Uses the micro-worktree isolation pattern
 * (WU-1262) to commit changes atomically.
 *
 * Supports: --rename, --wip-limit, --add-path, --remove-path, --description
 *
 * Usage:
 *   pnpm lane:edit --name 'Framework: CLI' --rename 'Framework: CLI/Commands'
 *   pnpm lane:edit --name 'Feature: API' --wip-limit 3
 *   pnpm lane:edit --name 'Core: Domain' --add-path 'packages/events/'
 */
import path from 'node:path';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import YAML from 'yaml';
import { findProjectRoot, getWorkspaceInitCommand, WORKSPACE_CONFIG_FILE_NAME, } from '@lumenflow/core/config';
import { WORKSPACE_V2_KEYS } from '@lumenflow/core/config-schema';
import { isValidLaneFormat } from '@lumenflow/core/lane-suggest-prompt';
import { createError, ErrorCodes } from '@lumenflow/core/error-handler';
import { die } from '@lumenflow/core/error-handler';
import { FILE_SYSTEM } from '@lumenflow/core/wu-constants';
import { withMicroWorktree } from '@lumenflow/core/micro-worktree';
import { runCLI } from './cli-entry-point.js';
import { asRecord } from './object-guards.js';
import { formatLaneNotFoundError } from './lane-name-suggest.js';
import { validateLaneArtifacts, classifyLaneLifecycleForProject, LANE_LIFECYCLE_STATUS, } from './lane-lifecycle-process.js';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const LOG_PREFIX = '[lane:edit]';
const OPERATION_NAME = 'lane-edit';
const ARG_NAME = '--name';
const ARG_RENAME = '--rename';
const ARG_WIP_LIMIT = '--wip-limit';
const ARG_ADD_PATH = '--add-path';
const ARG_REMOVE_PATH = '--remove-path';
const ARG_DESCRIPTION = '--description';
const ARG_HELP = '--help';
const COMMIT_PREFIX = 'chore: lane:edit';
const SOFTWARE_DELIVERY_KEY = WORKSPACE_V2_KEYS.SOFTWARE_DELIVERY;
const LANE_NAME_FORMAT = '"Parent: Sublane"';
const LANE_NAME_REGEX = '^[A-Z][a-zA-Z]+: [A-Z][a-zA-Z/]+$';
const LANE_NAME_EXAMPLES = ['Framework: Core', 'Content: Framework Docs'];
function formatLaneNameValidationError(name) {
    return (`${LOG_PREFIX} Invalid lane format: "${name}". ` +
        `Expected ${LANE_NAME_FORMAT}. Regex: ${LANE_NAME_REGEX}. ` +
        `Examples: "${LANE_NAME_EXAMPLES[0]}", "${LANE_NAME_EXAMPLES[1]}".`);
}
// ---------------------------------------------------------------------------
// Help
// ---------------------------------------------------------------------------
const HELP_TEXT = `Usage: pnpm lane:edit --name <lane> [options]

Edit a lane definition in workspace.yaml via micro-worktree commit.

Required:
  ${ARG_NAME} <name>          Target lane name (exact match, e.g. "${LANE_NAME_EXAMPLES[0]}")

Edit flags (at least one required):
  ${ARG_RENAME} <name>        Rename the lane using ${LANE_NAME_FORMAT}
                              Regex: ${LANE_NAME_REGEX}
                              Examples: "${LANE_NAME_EXAMPLES[0]}", "${LANE_NAME_EXAMPLES[1]}"
  ${ARG_WIP_LIMIT} <n>        Set WIP limit (positive integer)
  ${ARG_ADD_PATH} <path>      Add a code path (repeatable)
  ${ARG_REMOVE_PATH} <path>   Remove a code path (repeatable)
  ${ARG_DESCRIPTION} <text>   Set lane description

Examples:
  pnpm lane:edit --name 'Framework: CLI' --rename 'Framework: CLI/Commands'
  pnpm lane:edit --name 'Feature: API' --wip-limit 3
  pnpm lane:edit --name 'Core: Domain' --add-path 'packages/events/'
`;
// ---------------------------------------------------------------------------
// Argument parsing
// ---------------------------------------------------------------------------
export function parseLaneEditArgs(argv) {
    if (argv.includes(ARG_HELP)) {
        console.log(HELP_TEXT);
        process.exit(0);
    }
    let name;
    let rename;
    let wipLimit;
    const addPaths = [];
    const removePaths = [];
    let description;
    for (let i = 0; i < argv.length; i++) {
        const arg = argv[i];
        const next = argv[i + 1];
        switch (arg) {
            case ARG_NAME:
                name = next;
                i++;
                break;
            case ARG_RENAME:
                rename = next;
                i++;
                break;
            case ARG_WIP_LIMIT: {
                const parsed = parseInt(next, 10);
                if (isNaN(parsed) || parsed <= 0) {
                    throw createError(ErrorCodes.INVALID_ARGUMENT, `${ARG_WIP_LIMIT} must be a positive integer, got: ${next}`);
                }
                wipLimit = parsed;
                i++;
                break;
            }
            case ARG_ADD_PATH:
                addPaths.push(next);
                i++;
                break;
            case ARG_REMOVE_PATH:
                removePaths.push(next);
                i++;
                break;
            case ARG_DESCRIPTION:
                description = next;
                i++;
                break;
            default:
                break;
        }
    }
    if (!name) {
        throw createError(ErrorCodes.INVALID_ARGUMENT, `${ARG_NAME} is required. Run with ${ARG_HELP} for usage.`);
    }
    const hasEdits = rename !== undefined ||
        wipLimit !== undefined ||
        addPaths.length > 0 ||
        removePaths.length > 0 ||
        description !== undefined;
    if (!hasEdits) {
        throw createError(ErrorCodes.INVALID_ARGUMENT, `At least one edit flag is required (${ARG_RENAME}, ${ARG_WIP_LIMIT}, ${ARG_ADD_PATH}, ${ARG_REMOVE_PATH}, ${ARG_DESCRIPTION}). Run with ${ARG_HELP} for usage.`);
    }
    return {
        name,
        rename,
        wipLimit,
        addPaths: addPaths.length > 0 ? addPaths : undefined,
        removePaths: removePaths.length > 0 ? removePaths : undefined,
        description,
    };
}
// ---------------------------------------------------------------------------
// Precondition validation
// ---------------------------------------------------------------------------
export function validateLaneEditPreconditions(projectRoot) {
    const configPath = path.join(projectRoot, WORKSPACE_CONFIG_FILE_NAME);
    if (!existsSync(configPath)) {
        return {
            ok: false,
            error: `${LOG_PREFIX} Missing ${WORKSPACE_CONFIG_FILE_NAME}. Run \`${getWorkspaceInitCommand(projectRoot)}\` first.`,
        };
    }
    const classification = classifyLaneLifecycleForProject(projectRoot);
    const { status } = classification;
    if (status !== LANE_LIFECYCLE_STATUS.LOCKED && status !== LANE_LIFECYCLE_STATUS.DRAFT) {
        return {
            ok: false,
            error: `${LOG_PREFIX} Lane lifecycle status is "${status}". ` +
                `lane:edit requires lanes to be in locked or draft status. ` +
                `Run: pnpm lane:setup && pnpm lane:lock`,
        };
    }
    return { ok: true };
}
// ---------------------------------------------------------------------------
// Pure lane mutation logic (no side effects, testable)
// ---------------------------------------------------------------------------
export function applyLaneEdit(definitions, options) {
    const { name, rename, wipLimit, addPaths, removePaths, description } = options;
    // Find the target lane
    const targetIndex = definitions.findIndex((d) => d.name === name);
    if (targetIndex === -1) {
        return {
            ok: false,
            error: formatLaneNotFoundError(LOG_PREFIX, name, definitions.map((d) => d.name)),
        };
    }
    // Deep clone definitions to avoid mutating the input
    const updated = JSON.parse(JSON.stringify(definitions));
    const target = updated[targetIndex];
    // Apply rename
    if (rename !== undefined) {
        if (!isValidLaneFormat(rename)) {
            return {
                ok: false,
                error: formatLaneNameValidationError(rename),
            };
        }
        // Check that new name doesn't already exist
        const nameConflict = updated.some((d, i) => i !== targetIndex && d.name === rename);
        if (nameConflict) {
            return {
                ok: false,
                error: `${LOG_PREFIX} Lane "${rename}" already exists. Cannot rename.`,
            };
        }
        target.name = rename;
    }
    // Apply wip_limit
    if (wipLimit !== undefined) {
        target.wip_limit = wipLimit;
    }
    // Apply add-path
    if (addPaths !== undefined) {
        const currentPaths = target.code_paths ?? [];
        for (const addPath of addPaths) {
            if (currentPaths.includes(addPath)) {
                return {
                    ok: false,
                    error: `${LOG_PREFIX} Path "${addPath}" already exists in code_paths for lane "${target.name}".`,
                };
            }
            currentPaths.push(addPath);
        }
        target.code_paths = currentPaths;
    }
    // Apply remove-path
    if (removePaths !== undefined) {
        const currentPaths = target.code_paths ?? [];
        for (const removePath of removePaths) {
            const pathIndex = currentPaths.indexOf(removePath);
            if (pathIndex === -1) {
                return {
                    ok: false,
                    error: `${LOG_PREFIX} Path "${removePath}" not found in code_paths for lane "${target.name}".`,
                };
            }
            currentPaths.splice(pathIndex, 1);
        }
        target.code_paths = currentPaths;
    }
    // Apply description
    if (description !== undefined) {
        target.description = description;
    }
    return { ok: true, definitions: updated };
}
// ---------------------------------------------------------------------------
// Config I/O helpers
// ---------------------------------------------------------------------------
function readConfigDoc(configPath) {
    const content = readFileSync(configPath, FILE_SYSTEM.UTF8);
    const workspace = asRecord(YAML.parse(content));
    if (!workspace) {
        return {};
    }
    return asRecord(workspace[SOFTWARE_DELIVERY_KEY]) ?? {};
}
function writeConfigDoc(configPath, config) {
    const content = readFileSync(configPath, FILE_SYSTEM.UTF8);
    const workspace = asRecord(YAML.parse(content)) ?? {};
    workspace[SOFTWARE_DELIVERY_KEY] = config;
    const nextContent = YAML.stringify(workspace);
    writeFileSync(configPath, nextContent, FILE_SYSTEM.UTF8);
}
// ---------------------------------------------------------------------------
// Build commit message
// ---------------------------------------------------------------------------
function buildCommitMessage(options) {
    const parts = [];
    if (options.rename) {
        parts.push(`rename '${options.name}' to '${options.rename}'`);
    }
    if (options.wipLimit !== undefined) {
        parts.push(`set wip_limit=${options.wipLimit} on '${options.rename ?? options.name}'`);
    }
    if (options.addPaths) {
        parts.push(`add path(s) ${options.addPaths.join(', ')} to '${options.rename ?? options.name}'`);
    }
    if (options.removePaths) {
        parts.push(`remove path(s) ${options.removePaths.join(', ')} from '${options.rename ?? options.name}'`);
    }
    if (options.description !== undefined) {
        parts.push(`set description on '${options.rename ?? options.name}'`);
    }
    return `${COMMIT_PREFIX} ${parts.join(', ')}`;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
    const userArgs = process.argv.slice(2);
    const options = parseLaneEditArgs(userArgs);
    const projectRoot = findProjectRoot();
    // Step 1: Validate preconditions
    const preconditions = validateLaneEditPreconditions(projectRoot);
    if (!preconditions.ok) {
        die(preconditions.error);
    }
    console.log(`${LOG_PREFIX} Editing lane "${options.name}" via micro-worktree isolation (WU-1854)`);
    // Step 2: Use micro-worktree to make atomic changes
    await withMicroWorktree({
        operation: OPERATION_NAME,
        id: `lane-edit-${Date.now()}`,
        logPrefix: LOG_PREFIX,
        pushOnly: true,
        async execute({ worktreePath }) {
            const configRelPath = WORKSPACE_CONFIG_FILE_NAME;
            const configPath = path.join(worktreePath, configRelPath);
            if (!existsSync(configPath)) {
                die(`${LOG_PREFIX} Config file not found in micro-worktree: ${configRelPath}`);
            }
            // Read current config
            const config = readConfigDoc(configPath);
            const definitions = (config.lanes?.definitions ?? []);
            // Apply edits
            const editResult = applyLaneEdit(definitions, options);
            if (!editResult.ok) {
                die(editResult.error);
            }
            // Write updated config
            if (!config.lanes) {
                config.lanes = {};
            }
            config.lanes.definitions = editResult.definitions;
            writeConfigDoc(configPath, config);
            // Run lane:validate on the updated config to surface errors before commit
            const validation = validateLaneArtifacts(worktreePath);
            if (validation.warnings.length > 0 || validation.invalidLanes.length > 0) {
                const issues = [
                    ...validation.warnings.map((w) => `  - ${w}`),
                    ...validation.invalidLanes.map((l) => `  - Invalid lane: ${l}`),
                ].join('\n');
                die(`${LOG_PREFIX} lane:validate failed after edit. Changes NOT committed.\n${issues}`);
            }
            console.log(`${LOG_PREFIX} lane:validate passed after edit.`);
            return {
                commitMessage: buildCommitMessage(options),
                files: [configRelPath],
            };
        },
    });
    // Step 3: Report success
    const displayName = options.rename ?? options.name;
    console.log(`${LOG_PREFIX} Lane "${displayName}" updated successfully.`);
}
if (import.meta.main) {
    void runCLI(main);
}
//# sourceMappingURL=lane-edit.js.map