#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * @file gate-conditional.ts
 * WU-2615: Structured CLI for software_delivery.gates.conditional_commands.
 *
 * Mirrors the gate:co-change CLI so downstream agents learn one pattern, not
 * two. --add/--remove/--edit/--list manage per-rule entries. Micro-worktree
 * isolation commits atomically (same as gate:co-change).
 *
 * Usage:
 *   pnpm gate:conditional --add --name check-no-t4a \
 *     --trigger "src/**" \
 *     --command "bash scripts/check-no-t4a.sh" \
 *     --severity error \
 *     --guidance "ADR-0025 forbids t4a_ references in app code"
 *   pnpm gate:conditional --remove --name check-no-t4a
 *   pnpm gate:conditional --edit --name check-no-t4a --severity warn
 *   pnpm gate:conditional --list
 *   pnpm gate:conditional --list --json
 *
 * Complement: `config:set --json-value` (WU-2614) remains the generic escape
 * hatch for any array-of-objects field; this command is the discoverable,
 * structured path for conditional_commands specifically.
 */
import path from 'node:path';
import { createRequire } from 'node:module';
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 { ConditionalCommandConfigSchema } from '@lumenflow/core/config-schema';
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 { buildChoreCommitPrefix, getCurrentBranch } from '@lumenflow/core/wu-helpers';
import { runCLI } from './cli-entry-point.js';
import { asRecord } from './object-guards.js';
// micromatch has no ESM export and no @types; use createRequire (same pattern
// as gate-co-change.ts, WU-2437).
const esmRequire = createRequire(import.meta.url);
const micromatch = esmRequire('micromatch');
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const LOG_PREFIX = '[gate:conditional]';
const OPERATION_NAME = 'gate-conditional';
const ARG_ADD = '--add';
const ARG_REMOVE = '--remove';
const ARG_EDIT = '--edit';
const ARG_LIST = '--list';
const ARG_NAME = '--name';
const ARG_TRIGGER = '--trigger';
const ARG_COMMAND = '--command';
const ARG_SEVERITY = '--severity';
const ARG_GUIDANCE = '--guidance';
const ARG_GUIDANCE_REF = '--guidance-ref';
const ARG_JSON = '--json';
const ARG_HELP = '--help';
const SUBCOMMAND_TAG = 'gate:conditional';
const SOFTWARE_DELIVERY_KEY = WORKSPACE_V2_KEYS.SOFTWARE_DELIVERY;
const VALID_SEVERITIES = ['warn', 'error', 'off'];
// ---------------------------------------------------------------------------
// Help
// ---------------------------------------------------------------------------
const HELP_TEXT = `Usage: pnpm gate:conditional <operation> [options]

Manage software_delivery.gates.conditional_commands in workspace.yaml via
micro-worktree commit. Constraint-9 compatible (no raw Write/Edit on
workspace.yaml). Sibling of gate:co-change — same flag shapes, same error
shapes, same exit codes.

Operations (exactly one required):
  ${ARG_ADD}                  Add a new conditional command
  ${ARG_REMOVE}               Remove a conditional command by name
  ${ARG_EDIT}                 Edit an existing conditional command
  ${ARG_LIST}                 List all conditional commands

Options:
  ${ARG_NAME} <name>          Rule name (required for --add, --remove, --edit)
  ${ARG_TRIGGER} <glob>       Trigger glob pattern (repeatable, required for --add)
  ${ARG_COMMAND} <cmd>        Shell command to run (required for --add)
  ${ARG_SEVERITY} <level>     Severity: warn, error, off (default: error)
  ${ARG_GUIDANCE} <text>      Actionable guidance shown on rule failure
  ${ARG_GUIDANCE_REF} <path>  File path whose content is appended to violation messages
  ${ARG_JSON}                 Output --list as JSON for machine consumption
  ${ARG_HELP}                 Show this help

Examples:
  pnpm gate:conditional --add --name check-no-t4a \\
    --trigger "src/**" \\
    --command "bash scripts/check-no-t4a.sh" \\
    --severity error --guidance "ADR-0025 forbids t4a_ references"

  pnpm gate:conditional --remove --name check-no-t4a
  pnpm gate:conditional --edit --name check-no-t4a --severity warn
  pnpm gate:conditional --list
  pnpm gate:conditional --list --json

Escape hatch for complex shapes not covered by these flags:
  pnpm config:set --key software_delivery.gates.conditional_commands \\
    --json-value '[{"trigger_patterns":["src/**"],"command":"..."}]'
`;
// ---------------------------------------------------------------------------
// Argument parsing
// ---------------------------------------------------------------------------
export function parseGateConditionalArgs(argv) {
    if (argv.includes(ARG_HELP)) {
        console.log(HELP_TEXT);
        process.exit(0);
    }
    let operation;
    let name;
    const triggers = [];
    let command;
    let severity;
    let guidance;
    let guidanceRef;
    let json = false;
    for (let i = 0; i < argv.length; i++) {
        const arg = argv[i];
        const next = argv[i + 1];
        switch (arg) {
            case ARG_ADD:
                operation = 'add';
                break;
            case ARG_REMOVE:
                operation = 'remove';
                break;
            case ARG_EDIT:
                operation = 'edit';
                break;
            case ARG_LIST:
                operation = 'list';
                break;
            case ARG_NAME:
                name = next;
                i++;
                break;
            case ARG_TRIGGER:
                triggers.push(next);
                i++;
                break;
            case ARG_COMMAND:
                command = next;
                i++;
                break;
            case ARG_SEVERITY: {
                if (!VALID_SEVERITIES.includes(next)) {
                    throw createError(ErrorCodes.INVALID_ARGUMENT, `${ARG_SEVERITY} must be one of: ${VALID_SEVERITIES.join(', ')}. Got: ${next}`);
                }
                severity = next;
                i++;
                break;
            }
            case ARG_GUIDANCE:
                guidance = next;
                i++;
                break;
            case ARG_GUIDANCE_REF:
                guidanceRef = next;
                i++;
                break;
            case ARG_JSON:
                json = true;
                break;
            default:
                break;
        }
    }
    if (!operation) {
        throw createError(ErrorCodes.INVALID_ARGUMENT, `An operation is required (${ARG_ADD}, ${ARG_REMOVE}, ${ARG_EDIT}, or ${ARG_LIST}). Run with ${ARG_HELP} for usage.`);
    }
    if (operation === 'add') {
        if (!name) {
            throw createError(ErrorCodes.INVALID_ARGUMENT, `${ARG_NAME} is required for ${ARG_ADD}.`);
        }
        if (triggers.length === 0) {
            throw createError(ErrorCodes.INVALID_ARGUMENT, `At least one ${ARG_TRIGGER} is required for ${ARG_ADD}.`);
        }
        if (!command) {
            throw createError(ErrorCodes.INVALID_ARGUMENT, `${ARG_COMMAND} is required for ${ARG_ADD}.`);
        }
    }
    if (operation === 'remove' && !name) {
        throw createError(ErrorCodes.INVALID_ARGUMENT, `${ARG_NAME} is required for ${ARG_REMOVE}.`);
    }
    if (operation === 'edit') {
        if (!name) {
            throw createError(ErrorCodes.INVALID_ARGUMENT, `${ARG_NAME} is required for ${ARG_EDIT}.`);
        }
        const hasEdits = triggers.length > 0 ||
            command !== undefined ||
            severity !== undefined ||
            guidance !== undefined ||
            guidanceRef !== undefined;
        if (!hasEdits) {
            throw createError(ErrorCodes.INVALID_ARGUMENT, `At least one edit flag is required for ${ARG_EDIT} (${ARG_TRIGGER}, ${ARG_COMMAND}, ${ARG_SEVERITY}, ${ARG_GUIDANCE}, ${ARG_GUIDANCE_REF}).`);
        }
    }
    return {
        operation,
        name,
        triggers: triggers.length > 0 ? triggers : undefined,
        command,
        severity,
        guidance,
        guidanceRef,
        json: json || undefined,
    };
}
// ---------------------------------------------------------------------------
// Glob validation
// ---------------------------------------------------------------------------
export function validateGlobPattern(pattern) {
    try {
        const re = micromatch.makeRe(pattern);
        if (!re) {
            return { ok: false, error: `Invalid glob pattern "${pattern}": unparseable` };
        }
        return { ok: true };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        return { ok: false, error: `Invalid glob pattern "${pattern}": ${message}` };
    }
}
function validateAllGlobs(triggers) {
    const errors = [];
    for (const pattern of triggers ?? []) {
        const result = validateGlobPattern(pattern);
        if (!result.ok)
            errors.push(result.error);
    }
    return { ok: errors.length === 0, errors };
}
// ---------------------------------------------------------------------------
// Pure mutation logic
// ---------------------------------------------------------------------------
export function applyAddCommand(existing, options) {
    const { name, triggers, command, severity, guidance, guidanceRef } = options;
    if (existing.some((c) => c.name === name)) {
        return {
            ok: false,
            error: `${LOG_PREFIX} Conditional command "${name}" already exists. Use ${ARG_EDIT} to modify it.`,
        };
    }
    const globCheck = validateAllGlobs(triggers);
    if (!globCheck.ok) {
        return { ok: false, error: `${LOG_PREFIX} ${globCheck.errors.join('; ')}` };
    }
    const newEntry = {
        name: name,
        trigger_patterns: triggers,
        command: command,
        severity: severity ?? 'error',
    };
    if (guidance !== undefined) {
        newEntry.guidance = guidance;
    }
    if (guidanceRef !== undefined) {
        newEntry.guidance_ref = guidanceRef;
    }
    const parsed = ConditionalCommandConfigSchema.safeParse(newEntry);
    if (!parsed.success) {
        const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
        return { ok: false, error: `${LOG_PREFIX} Schema validation failed: ${issues}` };
    }
    return { ok: true, commands: [...existing, parsed.data] };
}
export function applyRemoveCommand(existing, options) {
    const { name } = options;
    if (existing.length === 0) {
        return {
            ok: false,
            error: `${LOG_PREFIX} No conditional commands configured. Add one with ${ARG_ADD}.`,
        };
    }
    const targetIndex = existing.findIndex((c) => c.name === name);
    if (targetIndex === -1) {
        const known = existing
            .map((c) => c.name)
            .filter((n) => typeof n === 'string')
            .join(', ');
        return {
            ok: false,
            error: `${LOG_PREFIX} Conditional command "${name}" not found.${known ? ` Known names: ${known}` : ''}`,
        };
    }
    const updated = [...existing];
    updated.splice(targetIndex, 1);
    return { ok: true, commands: updated };
}
export function applyEditCommand(existing, options) {
    const { name, triggers, command, severity, guidance, guidanceRef } = options;
    const targetIndex = existing.findIndex((c) => c.name === name);
    if (targetIndex === -1) {
        const known = existing
            .map((c) => c.name)
            .filter((n) => typeof n === 'string')
            .join(', ');
        return {
            ok: false,
            error: `${LOG_PREFIX} Conditional command "${name}" not found.${known ? ` Known names: ${known}` : ''}`,
        };
    }
    const globCheck = validateAllGlobs(triggers);
    if (!globCheck.ok) {
        return { ok: false, error: `${LOG_PREFIX} ${globCheck.errors.join('; ')}` };
    }
    const updated = JSON.parse(JSON.stringify(existing));
    const target = updated[targetIndex];
    if (triggers !== undefined)
        target.trigger_patterns = triggers;
    if (command !== undefined)
        target.command = command;
    if (severity !== undefined)
        target.severity = severity;
    if (guidance !== undefined)
        target.guidance = guidance;
    if (guidanceRef !== undefined)
        target.guidance_ref = guidanceRef;
    const parsed = ConditionalCommandConfigSchema.safeParse(target);
    if (!parsed.success) {
        const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
        return { ok: false, error: `${LOG_PREFIX} Schema validation failed: ${issues}` };
    }
    updated[targetIndex] = parsed.data;
    return { ok: true, commands: updated };
}
// ---------------------------------------------------------------------------
// List formatting
// ---------------------------------------------------------------------------
export function formatCommandList(commands, json) {
    if (json) {
        return JSON.stringify(commands, null, 2);
    }
    const lines = [];
    lines.push('Conditional commands:');
    if (commands.length === 0) {
        lines.push('  (none)');
        return lines.join('\n');
    }
    for (const entry of commands) {
        lines.push(formatEntry(entry));
    }
    return lines.join('\n');
}
function formatEntry(entry) {
    const lines = [];
    const label = entry.name ?? '(unnamed)';
    lines.push(`  ${label} (severity: ${entry.severity})`);
    lines.push(`    triggers: ${entry.trigger_patterns.join(', ')}`);
    lines.push(`    command:  ${entry.command}`);
    if (entry.guidance) {
        lines.push(`    guidance: ${entry.guidance}`);
    }
    if (entry.guidance_ref) {
        lines.push(`    guidance_ref: ${entry.guidance_ref}`);
    }
    return lines.join('\n');
}
// ---------------------------------------------------------------------------
// Config I/O
// ---------------------------------------------------------------------------
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);
}
// ---------------------------------------------------------------------------
// Commit message
// ---------------------------------------------------------------------------
/**
 * Build the auto-commit message for gate:conditional micro-worktree commits.
 *
 * WU-2656: Branch is a parameter (not resolved internally) so this stays a
 * pure function — testable without git mocks. On a lane/<lane>/wu-N worktree
 * the prefix becomes `chore(wu-N):`; on main or any non-lane branch the
 * historic `chore:` form is preserved.
 *
 * Exported for regression testing (WU-2656 AC#4/#5).
 */
export function buildCommitMessage(options, branch) {
    const prefix = buildChoreCommitPrefix(SUBCOMMAND_TAG, branch);
    switch (options.operation) {
        case 'add':
            return `${prefix} add '${options.name}'`;
        case 'remove':
            return `${prefix} remove '${options.name}'`;
        case 'edit': {
            const edits = [];
            if (options.triggers)
                edits.push('triggers');
            if (options.command)
                edits.push('command');
            if (options.severity)
                edits.push(`severity=${options.severity}`);
            if (options.guidance)
                edits.push('guidance');
            if (options.guidanceRef)
                edits.push('guidance_ref');
            return `${prefix} edit '${options.name}' (${edits.join(', ')})`;
        }
        default:
            return `${prefix} update`;
    }
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
    const userArgs = process.argv.slice(2);
    const options = parseGateConditionalArgs(userArgs);
    const projectRoot = findProjectRoot();
    // --list reads from main — no micro-worktree needed
    if (options.operation === 'list') {
        const configPath = path.join(projectRoot, WORKSPACE_CONFIG_FILE_NAME);
        if (!existsSync(configPath)) {
            die(`${LOG_PREFIX} Missing ${WORKSPACE_CONFIG_FILE_NAME}. Run \`${getWorkspaceInitCommand(projectRoot)}\` first.`);
        }
        const config = readConfigDoc(configPath);
        const commands = (config.gates?.conditional_commands ?? []);
        console.log(formatCommandList(commands, options.json === true));
        return;
    }
    console.log(`${LOG_PREFIX} ${options.operation} conditional command "${options.name}" via micro-worktree isolation`);
    await withMicroWorktree({
        operation: OPERATION_NAME,
        id: `gate-conditional-${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}`);
            }
            const config = readConfigDoc(configPath);
            if (!config.gates) {
                config.gates = {};
            }
            const existing = (config.gates.conditional_commands ?? []);
            let result;
            switch (options.operation) {
                case 'add':
                    result = applyAddCommand(existing, options);
                    break;
                case 'remove':
                    result = applyRemoveCommand(existing, options);
                    break;
                case 'edit':
                    result = applyEditCommand(existing, options);
                    break;
                default:
                    die(`${LOG_PREFIX} Unexpected operation: ${options.operation}`);
                    return { commitMessage: '', files: [] };
            }
            if (!result.ok) {
                die(result.error);
            }
            config.gates.conditional_commands = result.commands;
            writeConfigDoc(configPath, config);
            console.log(`${LOG_PREFIX} Conditional command "${options.name}" ${options.operation}ed successfully.`);
            return {
                commitMessage: buildCommitMessage(options, getCurrentBranch()),
                files: [configRelPath],
            };
        },
    });
    console.log(`${LOG_PREFIX} Done.`);
}
if (import.meta.main) {
    void runCLI(main);
}
//# sourceMappingURL=gate-conditional.js.map