import { StructuredError } from '@agentuity/core';
const InputJSONParseError = StructuredError('InputJSONParseError')();
const InputJSONTypeError = StructuredError('InputJSONTypeError')();
function unwrapSchema(schema) {
    let current = schema;
    while (current?._def) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const typeId = current._def.typeName || current._def.type || current.type;
        if ((typeId === 'ZodEffects' || typeId === 'effects') && current._def.schema) {
            current = current._def.schema;
        }
        else if ((typeId === 'ZodOptional' || typeId === 'optional') && current._def.innerType) {
            current = current._def.innerType;
        }
        else if ((typeId === 'ZodNullable' || typeId === 'nullable') && current._def.innerType) {
            current = current._def.innerType;
        }
        else if ((typeId === 'ZodDefault' || typeId === 'default') && current._def.innerType) {
            current = current._def.innerType;
        }
        else if ((typeId === 'ZodReadonly' || typeId === 'readonly') && current._def.innerType) {
            current = current._def.innerType;
        }
        else {
            break;
        }
    }
    return current;
}
/**
 * Check if a schema is a union of boolean and string (for optional string flags like --org [value])
 * This pattern is used when a flag can be used as a boolean (--org) or with a value (--org=myOrgId)
 */
function isBooleanStringUnion(schema) {
    const unwrapped = unwrapSchema(schema);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const def = unwrapped?._def;
    // Zod 3 uses typeName, Zod 4 uses type
    const typeId = def?.typeName || def?.type;
    if (typeId !== 'ZodUnion' && typeId !== 'union') {
        return false;
    }
    // Zod 3 uses _def.options, Zod 4 uses .options directly on the schema or _def.options
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const options = def?.options || unwrapped?.options;
    if (!Array.isArray(options) || options.length !== 2) {
        return false;
    }
    const types = new Set();
    for (const opt of options) {
        // Zod 4: type is directly on the object as .type
        // Zod 3: type is _def.typeName
        const optUnknown = opt;
        const optDef = optUnknown?._def;
        const optType = optUnknown?.type || optDef?.typeName || optDef?.type;
        types.add(optType);
    }
    return ((types.has('boolean') || types.has('ZodBoolean')) &&
        (types.has('string') || types.has('ZodString')));
}
function getShape(schema) {
    const unwrapped = unwrapSchema(schema);
    const typeId = unwrapped?._def?.typeName || unwrapped?._def?.type;
    if (typeId === 'ZodObject' || typeId === 'object') {
        const shape = unwrapped._def.shape;
        return typeof shape === 'function' ? shape() : shape || {};
    }
    if (typeId === 'ZodIntersection' || typeId === 'intersection') {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const def = unwrapped._def;
        const leftShape = def.left ? getShape(def.left) : {};
        const rightShape = def.right ? getShape(def.right) : {};
        return { ...leftShape, ...rightShape };
    }
    if (typeId === 'ZodTuple' || typeId === 'tuple') {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const items = unwrapped._def?.items;
        if (Array.isArray(items)) {
            const result = {};
            for (let i = 0; i < items.length; i++) {
                const item = items[i];
                // Try to extract a name from the description — check the item directly (Zod 4),
                // the _def (Zod 3), and the unwrapped inner schema
                const desc = item?.description ||
                    item?._def?.description ||
                    unwrapSchema(item)?._def?.description ||
                    unwrapSchema(item)?.description;
                let name = `arg${i}`;
                if (desc) {
                    // Slugify: lowercase, replace non-alphanumeric with dashes, trim
                    name = desc
                        .toLowerCase()
                        .replace(/[^a-z0-9]+/g, '-')
                        .replace(/^-|-$/g, '');
                }
                result[name] = item;
            }
            return result;
        }
    }
    return {};
}
export function parseArgsSchema(schema) {
    const shape = getShape(schema);
    const names = [];
    const metadata = [];
    for (const [key, value] of Object.entries(shape)) {
        names.push(key);
        /* eslint-disable @typescript-eslint/no-explicit-any */
        const typeId = value?._def?.typeName ||
            value._def?.type ||
            value.type;
        const unwrapped = unwrapSchema(value);
        const unwrappedTypeId = unwrapped?._def?.typeName || unwrapped?._def?.type || unwrapped?.type;
        /* eslint-enable @typescript-eslint/no-explicit-any */
        const isOptional = typeId === 'ZodOptional' || typeId === 'optional';
        const isVariadic = unwrappedTypeId === 'ZodArray' || unwrappedTypeId === 'array';
        metadata.push({ name: key, optional: isOptional, variadic: isVariadic });
    }
    return { names, metadata };
}
/**
 * Extract default value information from a Zod schema by walking the wrapper chain
 */
function extractDefaultInfo(schema) {
    let current = schema;
    while (current?._def) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const typeId = current._def.typeName || current._def?.type;
        if (typeId === 'ZodDefault' || typeId === 'default') {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const rawDefaultValue = current._def?.defaultValue;
            const defaultIsFunction = typeof rawDefaultValue === 'function';
            return {
                hasDefault: true,
                defaultValue: rawDefaultValue,
                defaultIsFunction,
            };
        }
        // Continue through wrapper chain
        if ((typeId === 'ZodOptional' ||
            typeId === 'optional' ||
            typeId === 'ZodNullable' ||
            typeId === 'nullable' ||
            typeId === 'ZodEffects' ||
            typeId === 'effects' ||
            typeId === 'ZodReadonly' ||
            typeId === 'readonly') &&
            current._def.innerType) {
            current = current._def.innerType;
        }
        else if ((typeId === 'ZodEffects' || typeId === 'effects') && current._def.schema) {
            current = current._def.schema;
        }
        else {
            break;
        }
    }
    return { hasDefault: false, defaultIsFunction: false };
}
export function parseOptionsSchema(schema) {
    const shape = getShape(schema);
    const options = [];
    for (const [key, value] of Object.entries(shape)) {
        const unwrapped = unwrapSchema(value);
        const description = unwrapped?._def?.description ??
            value?.description ??
            value?._def?.description;
        /* eslint-disable @typescript-eslint/no-explicit-any */
        const typeId = unwrapped?._def?.typeName || unwrapped?._def?.type || unwrapped?.type;
        /* eslint-enable @typescript-eslint/no-explicit-any */
        // Extract default info using helper that walks the wrapper chain
        const defaultInfo = extractDefaultInfo(value);
        // Evaluate function defaults at parse-time for actual default value
        // Wrapped in try/catch to handle non-pure default functions gracefully
        let defaultValue = defaultInfo.defaultValue;
        if (defaultInfo.defaultIsFunction) {
            try {
                defaultValue = defaultInfo.defaultValue();
            }
            catch {
                defaultValue = '<function>';
            }
        }
        let type = 'string';
        let enumValues;
        if (isBooleanStringUnion(value)) {
            // z.union([z.boolean(), z.string()]) - flag can be used as --flag or --flag=value
            type = 'optionalString';
        }
        else if (typeId === 'ZodNumber' || typeId === 'number') {
            type = 'number';
        }
        else if (typeId === 'ZodBoolean' || typeId === 'boolean') {
            type = 'boolean';
        }
        else if (typeId === 'ZodArray' || typeId === 'array') {
            type = 'array';
        }
        else if (typeId === 'ZodEnum' || typeId === 'enum') {
            // Extract enum values from Zod 4's def.entries
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const def = unwrapped?._def;
            if (def?.entries && typeof def.entries === 'object') {
                enumValues = Object.values(def.entries);
            }
        }
        options.push({
            name: key,
            type,
            description,
            hasDefault: defaultInfo.hasDefault,
            defaultValue,
            enumValues,
            // Descriptions prefixed with `Internal:` mark options that
            // exist for inter-process plumbing (e.g. forked child mode)
            // and should not appear in `--help`.
            hidden: typeof description === 'string' && /^internal:/i.test(description.trim()),
        });
    }
    return options;
}
// Cache for stdin confirmation detection
let stdinConfirmationChecked = false;
let stdinConfirmation = null;
/**
 * Check if stdin contains a piped "yes" confirmation.
 * This allows commands to be run with `echo "yes" | agentuity <command>` pattern.
 * Only works when stdin is not a TTY (piped input) and command doesn't use stdin.
 */
async function checkStdinConfirmation() {
    if (stdinConfirmationChecked) {
        return stdinConfirmation === true;
    }
    stdinConfirmationChecked = true;
    // Only check if stdin is not a TTY (meaning input is piped)
    if (process.stdin.isTTY) {
        stdinConfirmation = null;
        return false;
    }
    try {
        // Read stdin with a short timeout to avoid blocking
        const reader = process.stdin;
        let timeoutId = null;
        let resolved = false;
        const MAX_BYTES = 4096;
        // Define handlers outside so we can remove them in all paths
        let data = '';
        const onData = (chunk) => {
            data += chunk.toString();
            if (data.length >= MAX_BYTES) {
                data = data.slice(0, MAX_BYTES);
            }
        };
        let onEnd = null;
        let onError = null;
        const cleanup = () => {
            reader.removeListener('data', onData);
            if (onEnd) {
                reader.removeListener('end', onEnd);
            }
            if (onError) {
                reader.removeListener('error', onError);
            }
            reader.pause();
        };
        const readPromise = new Promise((resolve) => {
            onEnd = () => {
                if (resolved)
                    return;
                resolved = true;
                if (timeoutId !== null) {
                    clearTimeout(timeoutId);
                    timeoutId = null;
                }
                cleanup();
                resolve(data.trim().toLowerCase());
            };
            onError = (_err) => {
                if (resolved)
                    return;
                resolved = true;
                stdinConfirmation = null;
                if (timeoutId !== null) {
                    clearTimeout(timeoutId);
                    timeoutId = null;
                }
                cleanup();
                resolve('');
            };
            reader.on('data', onData);
            reader.on('end', onEnd);
            reader.on('error', onError);
        });
        // Use a short timeout to avoid blocking
        const timeoutPromise = new Promise((resolve) => {
            timeoutId = setTimeout(() => {
                if (resolved)
                    return;
                resolved = true;
                // Clean up listeners and pause stdin when timeout wins
                cleanup();
                resolve('');
            }, 100);
        });
        const input = await Promise.race([readPromise, timeoutPromise]);
        // Take first token/line to handle inputs like "yes\nanything"
        const firstToken = input.split(/\s+/)[0] ?? '';
        stdinConfirmation = firstToken === 'yes' || firstToken === 'y';
        return stdinConfirmation;
    }
    catch {
        stdinConfirmation = null;
        return false;
    }
}
/**
 * Reset stdin confirmation cache (for testing)
 */
export function resetStdinConfirmationCache() {
    stdinConfirmationChecked = false;
    stdinConfirmation = null;
}
export function buildValidationInput(schemas, rawArgs, rawOptions, _options, inputJson) {
    const result = { args: {}, options: {} };
    if (schemas.args) {
        // Check if the schema is a tuple — tuples need array input, not object
        const unwrapped = unwrapSchema(schemas.args);
        const typeId = unwrapped?._def?.typeName || unwrapped?._def?.type;
        if (typeId === 'ZodTuple' || typeId === 'tuple') {
            // Tuple schemas — map each item to a named key from the schema
            // (rawArgs may include trailing Commander.js options object)
            const parsed = parseArgsSchema(schemas.args);
            for (let i = 0; i < parsed.names.length; i++) {
                const name = parsed.names[i];
                if (name !== undefined) {
                    result.args[name] = rawArgs[i];
                }
            }
        }
        else {
            const parsed = parseArgsSchema(schemas.args);
            for (let i = 0; i < parsed.names.length; i++) {
                const name = parsed.names[i];
                if (name !== undefined) {
                    result.args[name] = rawArgs[i];
                }
            }
        }
    }
    if (schemas.options) {
        const parsed = parseOptionsSchema(schemas.options);
        for (const opt of parsed) {
            // Only include the option if it has a value - omitting undefined allows Zod to apply defaults
            // Commander.js converts kebab-case to camelCase, so we need to check both
            const camelCaseName = opt.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
            let value = rawOptions[opt.name] ?? rawOptions[camelCaseName];
            // Handle --yes and --force aliases for --confirm: if confirm is not set but yes/force is, use that value
            // Only treat --force as a --confirm alias if the schema does NOT declare a separate 'force' option
            if (opt.name === 'confirm' &&
                value === undefined &&
                (rawOptions.yes === true ||
                    (rawOptions.force === true && !parsed.some((o) => o.name === 'force')))) {
                value = true;
            }
            // Handle --org alias for --org-id: if orgId is not set but org is, use that value
            // Only treat --org as an --org-id alias if the schema does NOT declare a separate 'org' option
            if ((opt.name === 'orgId' || opt.name === 'org-id') &&
                value === undefined &&
                rawOptions.org !== undefined &&
                !parsed.some((o) => o.name === 'org')) {
                value = rawOptions.org;
            }
            if (value !== undefined) {
                result.options[opt.name] = value;
            }
        }
    }
    // Merge --input JSON values: CLI flags take precedence over --input values
    if (inputJson !== undefined) {
        let parsed;
        try {
            parsed = JSON.parse(inputJson);
        }
        catch (e) {
            throw new InputJSONParseError({
                message: `Invalid JSON in --input flag: ${e instanceof Error ? e.message : String(e)}`,
                flag: '--input',
                errorType: 'json_parse',
            });
        }
        if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
            throw new InputJSONTypeError({
                message: `Invalid JSON in --input flag: expected a JSON object, got ${parsed === null ? 'null' : Array.isArray(parsed) ? 'array' : typeof parsed}`,
                flag: '--input',
                expected: 'object',
                actual: parsed === null ? 'null' : Array.isArray(parsed) ? 'array' : typeof parsed,
            });
        }
        if (schemas.args) {
            const argsMeta = parseArgsSchema(schemas.args);
            for (const name of argsMeta.names) {
                if (result.args[name] === undefined && parsed[name] !== undefined) {
                    result.args[name] = parsed[name];
                }
            }
        }
        if (schemas.options) {
            const optsMeta = parseOptionsSchema(schemas.options);
            for (const opt of optsMeta) {
                if (result.options[opt.name] === undefined && parsed[opt.name] !== undefined) {
                    result.options[opt.name] = parsed[opt.name];
                }
            }
        }
    }
    return result;
}
/**
 * Async version of buildValidationInput that also checks stdin for "yes" confirmation.
 * Use this when the command has a confirm option and doesn't use stdin for other purposes.
 */
export async function buildValidationInputAsync(schemas, rawArgs, rawOptions, options, inputJson) {
    const result = buildValidationInput(schemas, rawArgs, rawOptions, options, inputJson);
    // Check for stdin confirmation if:
    // 1. Command has a confirm option in schema
    // 2. Command doesn't use stdin for other purposes
    // 3. confirm is not already set via flags
    if (schemas.options && !options?.usesStdin) {
        // Use getShape() instead of parseOptionsSchema() to avoid re-evaluating function defaults
        const shape = getShape(schemas.options);
        const hasConfirmOption = Object.hasOwn(shape, 'confirm');
        const confirmValue = result.options.confirm;
        if (hasConfirmOption && confirmValue === undefined) {
            const stdinConfirmed = await checkStdinConfirmation();
            if (stdinConfirmed) {
                result.options.confirm = true;
            }
        }
    }
    return result;
}
//# sourceMappingURL=schema-parser.js.map