"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Parser = void 0;
/* eslint-disable no-await-in-loop */
const errors_1 = require("./errors");
const readline = require("readline");
const util_1 = require("../util");
let debug;
try {
    // eslint-disable-next-line no-negated-condition
    debug = process.env.CLI_FLAGS_DEBUG !== '1' ? () => { } : require('debug')('../parser');
}
catch {
    debug = () => { };
}
/**
 * Support reading from stdin in Node 14 and older.
 *
 * This generally works for Node 14 and older EXCEPT when it's being
 * run from another process, in which case it will hang indefinitely. Because
 * of that issue we updated this to use AbortController but since AbortController
 * is only available in Node 16 and newer, we have to keep this legacy version.
 *
 * See these issues for more details on the hanging indefinitely bug:
 * https://github.com/oclif/core/issues/330
 * https://github.com/oclif/core/pull/363
 *
 * @returns Promise<string | null>
 */
const readStdinLegacy = async () => {
    const { stdin } = process;
    let result;
    if (stdin.isTTY)
        return null;
    result = '';
    stdin.setEncoding('utf8');
    for await (const chunk of stdin) {
        result += chunk;
    }
    return result;
};
const readStdinWithTimeout = async () => {
    const { stdin, stdout } = process;
    // process.stdin.isTTY is true whenever it's running in a terminal.
    // process.stdin.isTTY is undefined when it's running in a pipe, e.g. echo 'foo' | my-cli command
    // process.stdin.isTTY is undefined when it's running in a spawned process, even if there's no pipe.
    // This means that reading from stdin could hang indefinitely while waiting for a non-existent pipe.
    // Because of this, we have to set a timeout to prevent the process from hanging.
    if (stdin.isTTY)
        return null;
    return new Promise(resolve => {
        let result = '';
        const ac = new AbortController();
        const signal = ac.signal;
        const timeout = setTimeout(() => ac.abort(), 100);
        const rl = readline.createInterface({
            input: stdin,
            output: stdout,
            terminal: false,
        });
        rl.on('line', line => {
            result += line;
        });
        rl.once('close', () => {
            clearTimeout(timeout);
            debug('resolved from stdin', result);
            resolve(result);
        });
        signal.addEventListener('abort', () => {
            debug('stdin aborted');
            clearTimeout(timeout);
            rl.close();
            resolve(null);
        }, { once: true });
    });
};
const readStdin = async () => {
    const { stdin, version } = process;
    debug('stdin.isTTY', stdin.isTTY);
    const nodeMajorVersion = Number(version.split('.')[0].replace(/^v/, ''));
    debug('node version', nodeMajorVersion);
    return nodeMajorVersion > 14 ? readStdinWithTimeout() : readStdinLegacy();
};
function isNegativeNumber(input) {
    return /^-\d/g.test(input);
}
class Parser {
    constructor(input) {
        this.input = input;
        this.raw = [];
        this.context = input.context ?? {};
        this.argv = [...input.argv];
        this._setNames();
        this.booleanFlags = (0, util_1.pickBy)(input.flags, f => f.type === 'boolean');
        this.flagAliases = Object.fromEntries(Object.values(input.flags).flatMap(flag => {
            return (flag.aliases ?? []).map(a => [a, flag]);
        }));
    }
    async parse() {
        this._debugInput();
        const findLongFlag = (arg) => {
            const name = arg.slice(2);
            if (this.input.flags[name]) {
                return name;
            }
            if (this.flagAliases[name]) {
                return this.flagAliases[name].name;
            }
            if (arg.startsWith('--no-')) {
                const flag = this.booleanFlags[arg.slice(5)];
                if (flag && flag.allowNo)
                    return flag.name;
            }
        };
        const findShortFlag = ([_, char]) => {
            if (this.flagAliases[char]) {
                return this.flagAliases[char].name;
            }
            return Object.keys(this.input.flags).find(k => (this.input.flags[k].char === char && char !== undefined && this.input.flags[k].char !== undefined));
        };
        const findFlag = (arg) => {
            const isLong = arg.startsWith('--');
            const short = isLong ? false : arg.startsWith('-');
            const name = isLong ? findLongFlag(arg) : (short ? findShortFlag(arg) : undefined);
            return { name, isLong };
        };
        const parseFlag = (arg) => {
            const { name, isLong } = findFlag(arg);
            if (!name) {
                const i = arg.indexOf('=');
                if (i !== -1) {
                    const sliced = arg.slice(i + 1);
                    this.argv.unshift(sliced);
                    const equalsParsed = parseFlag(arg.slice(0, i));
                    if (!equalsParsed) {
                        this.argv.shift();
                    }
                    return equalsParsed;
                }
                return false;
            }
            const flag = this.input.flags[name];
            if (flag.type === 'option') {
                this.currentFlag = flag;
                const input = isLong || arg.length < 3 ? this.argv.shift() : arg.slice(arg[2] === '=' ? 3 : 2);
                // if the value ends up being one of the command's flags, the user didn't provide an input
                if ((typeof input !== 'string') || findFlag(input).name) {
                    throw new errors_1.CLIError(`Flag --${name} expects a value`);
                }
                this.raw.push({ type: 'flag', flag: flag.name, input: input });
            }
            else {
                this.raw.push({ type: 'flag', flag: flag.name, input: arg });
                // push the rest of the short characters back on the stack
                if (!isLong && arg.length > 2) {
                    this.argv.unshift(`-${arg.slice(2)}`);
                }
            }
            return true;
        };
        let parsingFlags = true;
        const nonExistentFlags = [];
        let dashdash = false;
        const originalArgv = [...this.argv];
        while (this.argv.length > 0) {
            const input = this.argv.shift();
            if (parsingFlags && input.startsWith('-') && input !== '-') {
                // attempt to parse as arg
                if (this.input['--'] !== false && input === '--') {
                    parsingFlags = false;
                    continue;
                }
                if (parseFlag(input)) {
                    continue;
                }
                if (input === '--') {
                    dashdash = true;
                    continue;
                }
                if (this.input['--'] !== false && !isNegativeNumber(input)) {
                    // At this point we have a value that begins with '-' or '--'
                    // but doesn't match up to a flag definition. So we assume that
                    // this is a misspelled flag or a non-existent flag,
                    // e.g. --hekp instead of --help
                    nonExistentFlags.push(input);
                    continue;
                }
            }
            if (parsingFlags && this.currentFlag && this.currentFlag.multiple) {
                this.raw.push({ type: 'flag', flag: this.currentFlag.name, input });
                continue;
            }
            // not a flag, parse as arg
            const arg = Object.keys(this.input.args)[this._argTokens.length];
            this.raw.push({ type: 'arg', arg, input });
        }
        const [{ argv, args }, { flags, metadata }] = await Promise.all([this._args(), this._flags()]);
        this._debugOutput(argv, args, flags);
        const unsortedArgv = (dashdash ? [...argv, ...nonExistentFlags, '--'] : [...argv, ...nonExistentFlags]);
        return {
            argv: unsortedArgv.sort((a, b) => originalArgv.indexOf(a) - originalArgv.indexOf(b)),
            flags,
            args: args,
            raw: this.raw,
            metadata,
            nonExistentFlags,
        };
    }
    async _flags() {
        const validateOptions = (flag, input) => {
            if (flag.options && !flag.options.includes(input))
                throw new errors_1.FlagInvalidOptionError(flag, input);
            return input;
        };
        const parseFlagOrThrowError = async (input, flag, context, token) => {
            if (!flag.parse)
                return input;
            const ctx = {
                ...context,
                token,
                error: context?.error,
                exit: context?.exit,
                log: context?.log,
                logToStderr: context?.logToStderr,
                warn: context?.warn,
            };
            try {
                if (flag.type === 'boolean') {
                    return await flag.parse(input, ctx, flag);
                }
                return await flag.parse(input, ctx, flag);
            }
            catch (error) {
                error.message = `Parsing --${flag.name} \n\t${error.message}\nSee more help with --help`;
                throw error;
            }
        };
        /* Could add a valueFunction (if there is a value/env/default) and could metadata.
        *  Value function can be resolved later.
        */
        const addValueFunction = (fws) => {
            const tokenLength = fws.tokens?.length;
            // user provided some input
            if (tokenLength) {
                // boolean
                if (fws.inputFlag.flag.type === 'boolean' && (0, util_1.last)(fws.tokens)?.input) {
                    return {
                        ...fws,
                        valueFunction: async (i) => parseFlagOrThrowError((0, util_1.last)(i.tokens)?.input !== `--no-${i.inputFlag.name}`, i.inputFlag.flag, this.context, (0, util_1.last)(i.tokens)),
                    };
                }
                // multiple with custom delimiter
                if (fws.inputFlag.flag.type === 'option' && fws.inputFlag.flag.delimiter && fws.inputFlag.flag.multiple) {
                    return {
                        ...fws, valueFunction: async (i) => (await Promise.all(((i.tokens ?? []).flatMap(token => token.input.split(i.inputFlag.flag.delimiter)))
                            // trim, and remove surrounding doubleQuotes (which would hav been needed if the elements contain spaces)
                            .map(v => v.trim().replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1'))
                            .map(async (v) => parseFlagOrThrowError(v, i.inputFlag.flag, this.context, { ...(0, util_1.last)(i.tokens), input: v })))).map(v => validateOptions(i.inputFlag.flag, v)),
                    };
                }
                // multiple in the oclif-core style
                if (fws.inputFlag.flag.type === 'option' && fws.inputFlag.flag.multiple) {
                    return {
                        ...fws,
                        valueFunction: async (i) => Promise.all((fws.tokens ?? []).map(token => parseFlagOrThrowError(validateOptions(i.inputFlag.flag, token.input), i.inputFlag.flag, this.context, token))),
                    };
                }
                // simple option flag
                if (fws.inputFlag.flag.type === 'option') {
                    return {
                        ...fws,
                        valueFunction: async (i) => parseFlagOrThrowError(validateOptions(i.inputFlag.flag, (0, util_1.last)(fws.tokens)?.input), i.inputFlag.flag, this.context, (0, util_1.last)(fws.tokens)),
                    };
                }
            }
            // no input: env flags
            if (fws.inputFlag.flag.env && process.env[fws.inputFlag.flag.env]) {
                const valueFromEnv = process.env[fws.inputFlag.flag.env];
                if (fws.inputFlag.flag.type === 'option' && valueFromEnv) {
                    return {
                        ...fws,
                        valueFunction: async (i) => parseFlagOrThrowError(validateOptions(i.inputFlag.flag, valueFromEnv), i.inputFlag.flag, this.context),
                    };
                }
                if (fws.inputFlag.flag.type === 'boolean') {
                    return {
                        ...fws,
                        valueFunction: async (i) => Promise.resolve((0, util_1.isTruthy)(process.env[i.inputFlag.flag.env] ?? 'false')),
                    };
                }
            }
            // no input, but flag has default value
            if (typeof fws.inputFlag.flag.default !== undefined) {
                return {
                    ...fws, metadata: { setFromDefault: true },
                    valueFunction: typeof fws.inputFlag.flag.default === 'function' ?
                        (i, allFlags = {}) => fws.inputFlag.flag.default({ options: i.inputFlag.flag, flags: allFlags }) :
                        async () => fws.inputFlag.flag.default,
                };
            }
            // base case (no value function)
            return fws;
        };
        const addHelpFunction = (fws) => {
            if (fws.inputFlag.flag.type === 'option' && fws.inputFlag.flag.defaultHelp) {
                return {
                    ...fws, helpFunction: typeof fws.inputFlag.flag.defaultHelp === 'function' ?
                        // @ts-expect-error flag type isn't specific enough to know defaultHelp will definitely be there
                        (i, flags, ...context) => i.inputFlag.flag.defaultHelp({ options: i.inputFlag, flags }, ...context) :
                        // @ts-expect-error flag type isn't specific enough to know defaultHelp will definitely be there
                        (i) => i.inputFlag.flag.defaultHelp,
                };
            }
            return fws;
        };
        const addDefaultHelp = async (fwsArray) => {
            const valueReferenceForHelp = fwsArrayToObject(flagsWithAllValues.filter(fws => !fws.metadata?.setFromDefault));
            return Promise.all(fwsArray.map(async (fws) => {
                try {
                    if (fws.helpFunction) {
                        return {
                            ...fws,
                            metadata: {
                                ...fws.metadata,
                                defaultHelp: await fws.helpFunction?.(fws, valueReferenceForHelp, this.context),
                            },
                        };
                    }
                }
                catch {
                    // no-op
                }
                return fws;
            }));
        };
        const fwsArrayToObject = (fwsArray) => Object.fromEntries(fwsArray.filter(fws => fws.value !== undefined)
            .map(fws => [fws.inputFlag.name, fws.value]));
        const flagTokenMap = this.mapAndValidateFlags();
        const flagsWithValues = await Promise.all(Object.entries(this.input.flags)
            // we check them if they have a token, or might have env, default, or defaultHelp.  Also include booleans so they get their default value
            .filter(([name, flag]) => flag.type === 'boolean' || flag.env || flag.default !== undefined || 'defaultHelp' in flag || flagTokenMap.has(name))
            // match each possible flag to its token, if there is one
            .map(([name, flag]) => ({ inputFlag: { name, flag }, tokens: flagTokenMap.get(name) }))
            .map(fws => addValueFunction(fws))
            .filter(fws => fws.valueFunction !== undefined)
            .map(fws => addHelpFunction(fws))
            // we can't apply the default values until all the other flags are resolved because `flag.default` can reference other flags
            .map(async (fws) => (fws.metadata?.setFromDefault ? fws : { ...fws, value: await fws.valueFunction?.(fws) })));
        const valueReference = fwsArrayToObject(flagsWithValues.filter(fws => !fws.metadata?.setFromDefault));
        const flagsWithAllValues = await Promise.all(flagsWithValues
            .map(async (fws) => (fws.metadata?.setFromDefault ? { ...fws, value: await fws.valueFunction?.(fws, valueReference) } : fws)));
        const finalFlags = (flagsWithAllValues.some(fws => typeof fws.helpFunction === 'function')) ? await addDefaultHelp(flagsWithAllValues) : flagsWithAllValues;
        return {
            // @ts-ignore original version returned an any.  Not sure how to get to the return type for `flags` prop
            flags: fwsArrayToObject(finalFlags),
            metadata: { flags: Object.fromEntries(finalFlags.filter(fws => fws.metadata).map(fws => [fws.inputFlag.name, fws.metadata])) },
        };
    }
    async _args() {
        const argv = [];
        const args = {};
        const tokens = this._argTokens;
        let stdinRead = false;
        const ctx = this.context;
        for (const [name, arg] of Object.entries(this.input.args)) {
            const token = tokens.find(t => t.arg === name);
            ctx.token = token;
            if (token) {
                if (arg.options && !arg.options.includes(token.input)) {
                    throw new errors_1.ArgInvalidOptionError(arg, token.input);
                }
                const parsed = await arg.parse(token.input, ctx, arg);
                argv.push(parsed);
                args[token.arg] = parsed;
            }
            else if (!arg.ignoreStdin && !stdinRead) {
                let stdin = await readStdin();
                if (stdin) {
                    stdin = stdin.trim();
                    const parsed = await arg.parse(stdin, ctx, arg);
                    argv.push(parsed);
                    args[name] = parsed;
                }
                stdinRead = true;
            }
            if (!args[name] && (arg.default || arg.default === false)) {
                if (typeof arg.default === 'function') {
                    const f = await arg.default();
                    argv.push(f);
                    args[name] = f;
                }
                else {
                    argv.push(arg.default);
                    args[name] = arg.default;
                }
            }
        }
        for (const token of tokens) {
            if (args[token.arg])
                continue;
            argv.push(token.input);
        }
        return { argv, args: args };
    }
    _debugOutput(args, flags, argv) {
        if (argv.length > 0) {
            debug('argv: %o', argv);
        }
        if (Object.keys(args).length > 0) {
            debug('args: %o', args);
        }
        if (Object.keys(flags).length > 0) {
            debug('flags: %o', flags);
        }
    }
    _debugInput() {
        debug('input: %s', this.argv.join(' '));
        const args = Object.keys(this.input.args);
        if (args.length > 0) {
            debug('available args: %s', args.join(' '));
        }
        if (Object.keys(this.input.flags).length === 0)
            return;
        debug('available flags: %s', Object.keys(this.input.flags)
            .map(f => `--${f}`)
            .join(' '));
    }
    get _argTokens() {
        return this.raw.filter(o => o.type === 'arg');
    }
    _setNames() {
        for (const k of Object.keys(this.input.flags)) {
            this.input.flags[k].name = k;
        }
        for (const k of Object.keys(this.input.args)) {
            this.input.args[k].name = k;
        }
    }
    mapAndValidateFlags() {
        const flagTokenMap = new Map();
        for (const token of this.raw.filter(o => o.type === 'flag')) {
            // fail fast if there are any invalid flags
            if (!(token.flag in this.input.flags)) {
                throw new errors_1.CLIError(`Unexpected flag ${token.flag}`);
            }
            const existing = flagTokenMap.get(token.flag) ?? [];
            flagTokenMap.set(token.flag, [...existing, token]);
        }
        return flagTokenMap;
    }
}
exports.Parser = Parser;
