#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const crypto_1 = tslib_1.__importDefault(require("crypto"));
const fs_1 = tslib_1.__importDefault(require("fs"));
const path_1 = tslib_1.__importDefault(require("path"));
const await_spawn_1 = tslib_1.__importDefault(require("await-spawn"));
const lodash_flattendeep_1 = tslib_1.__importDefault(require("lodash.flattendeep"));
const typescript_1 = tslib_1.__importDefault(require("typescript"));
const commander_1 = require("commander");
const shell_quote_1 = tslib_1.__importDefault(require("shell-quote"));
const utils_1 = require("./lib/utils");
const make_fake_expression_1 = require("./lib/make_fake_expression");
const directiveParser_1 = require("./lib/directiveParser");
commander_1.program
    .option("-w, --watch", `watch mode of cli`)
    .option("-p, --project <string>", "ts project path where tsconfig.json is in", "./")
    .option("-e, --exclude <regexp>", "regexp to exclude files", "node_modules")
    .option("--emit_dir <path>", "where sqls will be emitted")
    // ! --mock, --error_cost, --warn_cost, --info_cost, --cost_pattern, --tags, --command are the same as plugin options
    // ! do not use commander default option value, to use the options in tsconfig.json
    .option("-m, --mock <string>", `string to mock the values in the fake sqls, you can override it with: sql.mock<'null'>(any_value) (default: "${utils_1.default_mock}")`)
    .option("--error_cost <number>", `throw error if explain cost exceeds treshold`)
    .option("--warn_cost <number>", `log warning if explain cost exceeds treshold`)
    .option("--info_cost <number>", `log info if explain cost exceeds treshold`)
    .option("--cost_pattern <regexp>", `regexp to extract cost from command stdout, make sure the number is the first capturing group (default: "/${utils_1.default_cost_pattern.source}/")`)
    .option("-t, --tags <string>", `tags you used in you ts file (default: "${Object.entries(utils_1.default_tags)
    .map(it => it.join("="))
    .join(",")}")`, (str, pv) => {
    return Object.fromEntries(str.split(",").map(s => s.split("=")));
    // return [(pv as any) as string, str]
    //   .map((it) => Object.fromEntries(it.split(",").map((s) => s.split("="))))
    //   .reduce((acc, cv) => ({ ...acc, ...cv }), {}) as typeof default_tags;
})
    .option("-c, --command <string>", `command to be run to explain the fake sql (default: "${utils_1.default_command}")` /* default_command */)
    .description(`Explain all your sqls in your code to test them. \n\nEg: ts-sql-plugin -p ./my_ts_projet/tsconfig.json -c 'psql -c'`)
    .action(async () => {
    var _a;
    let cli_config = commander_1.program.opts();
    if (typeof cli_config.command !== "string") {
        cli_config.command = undefined;
    }
    cli_config.exclude = new RegExp(cli_config.exclude);
    const ts_config_path = typescript_1.default.findConfigFile(cli_config.project, typescript_1.default.sys.fileExists, "tsconfig.json");
    if (!ts_config_path) {
        throw new Error("Could not find a valid 'tsconfig.json'.");
    }
    const { config: ts_config } = typescript_1.default.parseConfigFileTextToJson(ts_config_path, await fs_1.default.promises.readFile(ts_config_path, { encoding: "utf8" }));
    let plugin_config = (_a = ts_config.compilerOptions.plugins) === null || _a === void 0 ? void 0 : _a.find(it => it.name === "ts-sql-plugin");
    plugin_config = utils_1.merge_defaults(plugin_config, cli_config);
    const cost_pattern = new RegExp(plugin_config.cost_pattern);
    const initProgram = typescript_1.default.createProgram(utils_1.get_all_ts_files(path_1.default.dirname(ts_config_path)), ts_config.compilerOptions);
    let fake_expression = make_fake_expression_1.make_fake_expression(initProgram.getTypeChecker(), plugin_config.tags);
    if (cli_config.watch) {
        const watchHost = typescript_1.default.createWatchCompilerHost(ts_config_path, undefined, typescript_1.default.sys);
        const watchProgram = typescript_1.default.createWatchProgram(watchHost);
        const onChangeFile = (fileName) => {
            const file = watchProgram.getProgram().getSourceFile(fileName);
            if (file) {
                fake_expression = make_fake_expression_1.make_fake_expression(watchProgram.getProgram().getProgram().getTypeChecker(), plugin_config.tags);
                delint(file);
            }
        };
        const watchFile = (sourceFile) => {
            watchHost.watchFile(sourceFile.fileName, (fileName, event) => {
                if (event !== typescript_1.default.FileWatcherEventKind.Deleted) {
                    onChangeFile(fileName);
                }
            });
        };
        const source_files = watchProgram
            .getProgram()
            .getSourceFiles()
            .filter(it => !cli_config.exclude.test(it.fileName));
        source_files.forEach(watchFile);
        source_files.forEach(it => onChangeFile(it.fileName));
        return;
    }
    let has_error = false;
    let report_errors = [];
    console.log("-- Start init sql check and emit...");
    for (const file of initProgram.getSourceFiles().filter(f => !cli_config.exclude.test(f.fileName))) {
        await delint(file);
    }
    if (has_error) {
        console.error("\n\n-- Your code can not pass all sql test!!!\n");
        report_errors.forEach(args => utils_1.report(...args));
        process.exit(1);
    }
    console.log("\n\n-- Init sql check and emit finished.\n");
    function recursiveAllChildrenNodes(node) {
        return node.getChildren().map(n => [n, ...recursiveAllChildrenNodes(n)]);
    }
    async function delint(sourceFile) {
        const treeNodes = recursiveAllChildrenNodes(sourceFile);
        const sqlTagNodes = lodash_flattendeep_1.default(treeNodes).filter((n) => n.kind === typescript_1.default.SyntaxKind.TaggedTemplateExpression &&
            n.tag.getText() === plugin_config.tags.sql);
        await Promise.all(sqlTagNodes.map(delintNode));
        async function delintNode(node) {
            var _a;
            let query_configs = fake_expression(node);
            for (const qc of query_configs) {
                let s = utils_1.trim_middle_comments(qc.text).replace(/\?\?/gm, plugin_config.mock);
                const directives = directiveParser_1.parseDirectives(s);
                if (cli_config.emit_dir) {
                    const emit_directive = directives.find(d => d.directive === "emit");
                    if (emit_directive) {
                        const fileName = (_a = emit_directive.arg) !== null && _a !== void 0 ? _a : crypto_1.default.createHash("sha1").update(s).digest("hex");
                        const filePath = `${cli_config.emit_dir}/${fileName}.sql`;
                        try {
                            fs_1.default.writeFileSync(filePath, s + ";");
                        }
                        catch (err) {
                            console.log(`-- Emit Error occured, when emitting file "${filePath}"`);
                        }
                    }
                }
                console.log(`\n\n-- EXPLAIN\n${s};`);
                const [_command, ..._command_args] = shell_quote_1.default
                    .parse(plugin_config.command)
                    .concat("EXPLAIN " + s);
                const p = await await_spawn_1.default(_command, _command_args).catch((e) => e);
                const stdout = p.toString();
                if (p instanceof Error) {
                    has_error = true;
                    utils_1.report(sourceFile, node, p.stderr);
                    report_errors.push([sourceFile, node, p.stderr, 2]);
                    break;
                }
                if ((plugin_config.error_cost || plugin_config.warn_cost || plugin_config.info_cost) &&
                    !directives.some(d => d.directive === "ignore-cost")) {
                    const match = stdout.match(cost_pattern);
                    if (match) {
                        const [_, cost_str] = match;
                        const cost = Number(cost_str);
                        if (cost > plugin_config.error_cost) {
                            has_error = true;
                            utils_1.report(sourceFile, node, `Error: explain cost is too high: ${cost}\n${s}`, 2);
                            report_errors.push([sourceFile, node, `Error: explain cost is too high: ${cost}\n${s}`, 2]);
                            break;
                        }
                        else if (cost > plugin_config.warn_cost) {
                            utils_1.report(sourceFile, node, `Warn: explain cost is at warning: ${cost}\n${s}`, 2);
                        }
                        else if (cost > plugin_config.info_cost) {
                            utils_1.report(sourceFile, node, `Info: explain cost is ok: ${cost}\n${s}`, 1);
                        }
                    }
                    else {
                        has_error = true;
                        utils_1.report(sourceFile, node, `Error: can not extract cost with cost_pattern: ${cost_pattern.source}\n${stdout}\n${s}`, 2);
                        report_errors.push([
                            sourceFile,
                            node,
                            `Error: can not extract cost with cost_pattern: ${cost_pattern.source}\n${stdout}\n${s}`,
                            2,
                        ]);
                        break;
                    }
                }
            }
        }
    }
})
    .parseAsync(process.argv);
//# sourceMappingURL=cli.js.map