// Analyze deployment errors to provide tips to user :)
import c from "chalk";
import format from "string-template";
import { getAllTips } from "./deployTipsList.js";
import { deployErrorsToMarkdown, testFailuresToMarkdown } from "../gitProvider/utilsMarkdown.js";
import { findJsonInString, stripAnsi, uxLog } from "./index.js";
import { AiProvider } from "../aiProvider/index.js";
import { analyzeDeployErrorLogsJson } from "./deployTipJson.js";
let logRes = null;
let errorsAndTips = [];
let alreadyProcessedErrors = [];
const firstYellowChar = c.yellow("*")[0];
// Checks for deploy tips in a log string
// returns formatted and completed error log
export async function analyzeDeployErrorLogs(log, includeInLog = true, options) {
    // New way using json: should be always be used
    const jsonResult = findJsonInString(log);
    if (jsonResult) {
        const resultsFromJson = await analyzeDeployErrorLogsJson(jsonResult, log, includeInLog, options);
        if (resultsFromJson && (resultsFromJson?.errorsAndTips.length > 0 || resultsFromJson?.failedTests?.length > 0)) {
            return resultsFromJson;
        }
    }
    errorsAndTips = []; // reset
    alreadyProcessedErrors = []; // reset
    logRes = returnErrorLines(log).join("\n"); // reset
    const tips = [];
    for (const tipDefinition of getAllTips()) {
        if (await matchesTip(tipDefinition, includeInLog)) {
            tips.push(tipDefinition);
        }
    }
    // Add default error messages for errors without tips
    const logResLines = [];
    const updatedLogLines = returnErrorLines(logRes);
    let index = 0;
    for (const logLine of updatedLogLines) {
        logResLines.push(logLine.trim());
        if (logLine.includes("Deploy failed.")) {
            index++;
            continue;
        }
        if (isErrorLine(logLine) &&
            (updatedLogLines[index + 1] && isErrorLine(updatedLogLines[index + 1]) || !updatedLogLines[index + 1])) {
            const aiTip = await findAiTip(logLine.trim());
            // Complete with AI if possible
            if (aiTip && aiTip.success) {
                logResLines.push(c.yellow(`[AI] ${aiTip.model} suggested the following tip ${c.bold(c.bgRed("(can be good or stupid, this is AI !)"))}:`));
                logResLines.push(c.magenta(c.italic(aiTip.promptResponse)));
                logResLines.push(c.yellow(""));
                errorsAndTips.push({
                    error: { message: stripAnsi(logLine.trim()) },
                    tipFromAi: {
                        promptResponse: aiTip.promptResponse,
                    },
                });
            }
            else {
                const promptText = AiProvider.buildPrompt("PROMPT_SOLVE_DEPLOYMENT_ERROR", { "ERROR": logLine.trim() });
                // No tip found, give the user an AI prompt
                logResLines.push(c.yellow("No sfdx-hardis tip to solve this error. You can try the following prompt:"));
                logResLines.push(c.yellow(promptText));
                logResLines.push(c.yellow(""));
                errorsAndTips.push({
                    error: { message: stripAnsi(logLine.trim()) },
                    tipFromAi: {
                        promptText: promptText,
                    },
                });
            }
        }
        index++;
    }
    // Extract failed test classes
    const logRaw = stripAnsi(log);
    const failedTests = [];
    // sf project deploy output
    extractFailedTestsInfoForSfCommand(logRaw, failedTests);
    if (failedTests.length === 0) {
        // Legacy sfdx force:source:deploy output
        extractFailedTestsInfoForSfdxCommand(logRaw, failedTests);
    }
    // Fallback in case we have not been able to identify errors
    if (errorsAndTips.length === 0 && failedTests.length === 0) {
        errorsAndTips.push(({
            error: { message: "There has been an issue parsing errors, probably because of a SF CLI output format update. Please check console logs." },
            tip: {
                label: "SfdxHardisParseError",
                message: "If you are in CI/CD, please check at the bottom of deployment check job logs. The issue will be fixed ASAP.",
            },
        }));
    }
    await updatePullRequestResult(errorsAndTips, failedTests, options);
    return { tips, errorsAndTips, failedTests, errLog: logResLines.join("\n") };
}
function isErrorLine(str) {
    const strTrim = str.trim();
    if (strTrim.startsWith("Error") || strTrim.startsWith("| Error")) {
        return true;
    }
    return false;
}
function extractFailedTestsInfoForSfdxCommand(logRaw, failedTests) {
    const regexFailedTests = /Test Failures([\S\s]*?)Test Success/gm;
    if (logRaw.match(regexFailedTests)) {
        const failedTestsLines = (regexFailedTests
            .exec(logRaw) || [])[1]
            .split("\n")
            .map((s) => s.trim());
        let failedTest = null;
        // Parse strings to extract main error line then stack
        for (const line of failedTestsLines) {
            const regex = /^(\w+[\d_]*)\s+(\w+[\d_]*)\s*(.*)$/;
            const match = line.match(regex);
            if (match) {
                if (match[1] === "Name") {
                    // header column
                    continue;
                }
                const errSplit = match[3].split("Class.");
                failedTest = {
                    class: match[1],
                    method: match[2],
                    error: (errSplit.shift() || "").trim(),
                };
                if (errSplit.length > 0) {
                    failedTest.stack = "Class." + errSplit.join("\nClass.");
                }
                failedTests.push(failedTest);
            }
        }
    }
}
function extractFailedTestsInfoForSfCommand(logRaw, failedTests) {
    const regexFailedTests = /Test Failures([\S\s]*?)Test Success/gm;
    if (logRaw.match(regexFailedTests)) {
        const failedTestsString = (regexFailedTests.exec(logRaw) || [])[1].split(/\r?\n/).join("\n") + "\n•";
        // Parse strings to extract main error line then stack
        // eslint-disable-next-line no-regex-spaces, no-useless-escape
        const regex = /^• (.*)\n  message: (.*)\n  stacktrace: ([\s\S]*?)(?=\n•|\z)/gm;
        const matches = [...failedTestsString.matchAll(regex)];
        for (const match of matches || []) {
            const failedTest = {
                class: match[1].split(".")[0],
                method: match[1].split(".")[1],
                error: match[2].trim(),
            };
            if (match[3]) {
                failedTest.stack = match[3];
            }
            failedTests.push(failedTest);
        }
    }
}
// Checks if the error string or regex is found in the log
// Adds the fix tip under the line if includeInLog is true
async function matchesTip(tipDefinition, includeInLog = true) {
    const newLogLines = [];
    // string matching
    if (tipDefinition.expressionString &&
        tipDefinition.expressionString.filter((expressionString) => {
            return (logRes || "").includes(expressionString);
        }).length > 0) {
        if (includeInLog) {
            const logLines = returnErrorLines(logRes);
            for (const line of logLines) {
                newLogLines.push(line);
                let found = false;
                for (const expressionString of tipDefinition.expressionString) {
                    if (line.includes(expressionString)) {
                        found = true;
                        newLogLines.push(c.yellow(c.italic("Tip for " + tipDefinition.label + ":")));
                        newLogLines.push(...tipDefinition.tip.split(/\r?\n/).map((str) => c.yellow(str)));
                        newLogLines.push(c.yellow(" "));
                        // Update output list
                        errorsAndTips.push({
                            error: { message: stripAnsi(line) },
                            tip: {
                                label: tipDefinition.label,
                                message: tipDefinition.tip,
                                docUrl: tipDefinition.docUrl
                            },
                        });
                    }
                }
                if (found) {
                    const aiTip = await findAiTip(line.trim());
                    // Complete with AI if possible
                    if (aiTip && aiTip.success) {
                        newLogLines.push(c.yellow(`[AI] ${aiTip.model} suggested the following tip ${c.bold(c.bgRed("(can be good or stupid, this is AI !)"))}:`));
                        newLogLines.push(c.magenta(c.italic(aiTip.promptResponse)));
                        newLogLines.push(c.yellow(""));
                        const lastErrorAndTip = errorsAndTips[errorsAndTips.length - 1];
                        lastErrorAndTip.tipFromAi = {
                            promptResponse: aiTip.promptResponse,
                        };
                        errorsAndTips[errorsAndTips.length - 1] = lastErrorAndTip;
                    }
                }
            }
            logRes = newLogLines.join("\n");
        }
        return true;
    }
    // regex matching
    /* jscpd:ignore-start */
    if (tipDefinition.expressionRegex &&
        tipDefinition.expressionRegex.filter((expressionRegex) => {
            return expressionRegex.test(logRes);
        }).length > 0) {
        if (includeInLog) {
            const newLogLines = [];
            const logLines = returnErrorLines(logRes);
            for (const line of logLines) {
                newLogLines.push(line);
                let found = false;
                for (const expressionRegex of tipDefinition.expressionRegex) {
                    expressionRegex.lastIndex = 0; // reset regex last index to be able to reuse it
                    const matches = [...line.matchAll(expressionRegex)];
                    for (const m of matches) {
                        found = true;
                        const replacements = m.map((str) => c.bold(str.trim().replace(/'/gm, "")));
                        const replacementsMarkdown = m.map((str) => `**${str.trim().replace(/'/gm, "")}**`);
                        newLogLines.push(c.yellow(c.italic(format(tipDefinition.label, replacements))));
                        const tip = tipDefinition.tip;
                        newLogLines.push(...tip.split(/\r?\n/).map((str) => c.yellow(format(str, replacements))));
                        newLogLines.push(c.yellow(" "));
                        // Update output list
                        errorsAndTips.push({
                            error: { message: stripAnsi(format(line, replacementsMarkdown)).replace(/\*\*.\*\*/gm, ".") },
                            tip: {
                                label: tipDefinition.label,
                                docUrl: tipDefinition.docUrl,
                                message: stripAnsi(format(tipDefinition.tip, replacementsMarkdown).replace(/\*\*.\*\*/gm, ".")),
                            },
                        });
                    }
                    if (found) {
                        const aiTip = await findAiTip(line.trim());
                        // Complete with AI if possible
                        if (aiTip && aiTip.success) {
                            newLogLines.push(c.yellow(`[AI] ${aiTip.model} suggested the following tip ${c.bold(c.bgRed("(can be good or stupid, this is AI !)"))}:`));
                            newLogLines.push(c.magenta(c.italic(aiTip.promptResponse)));
                            newLogLines.push(c.yellow(""));
                            const lastErrorAndTip = errorsAndTips[errorsAndTips.length - 1];
                            lastErrorAndTip.tipFromAi = {
                                promptResponse: aiTip.promptResponse,
                            };
                            errorsAndTips[errorsAndTips.length - 1] = lastErrorAndTip;
                        }
                    }
                }
            }
            logRes = newLogLines.join("\n");
        }
        return true;
    }
    return false;
    /* jscpd:ignore-end */
}
function returnErrorLines(strIn) {
    return strIn.split(/\r?\n/).filter((str) => isErrorLine(str) || str.startsWith(firstYellowChar));
}
// This data will be caught later to build a pull request message
export async function updatePullRequestResult(errorsAndTips, failedTests, options) {
    const prData = {
        messageKey: "deployment",
        title: options.check ? "✅ Deployment check success" : "✅ Deployment success",
        deployErrorsMarkdownBody: "No error has been found during the deployment",
        deployStatus: "valid",
    };
    if (errorsAndTips.length > 0) {
        prData.title = options.check ? "❌ Deployment check failure" : "❌ Deployment failure";
        prData.deployErrorsMarkdownBody = deployErrorsToMarkdown(errorsAndTips);
        prData.status = "invalid";
    }
    else if (failedTests.length > 0) {
        prData.title = options.check ? "❌ Deployment check ok, but failing test classes" : "❌ Deployment check ok, but failing test classes";
        prData.deployErrorsMarkdownBody = testFailuresToMarkdown(failedTests);
        prData.status = "invalid";
    }
    globalThis.pullRequestData = Object.assign(globalThis.pullRequestData || {}, prData);
}
async function findAiTip(errorLine) {
    if (alreadyProcessedErrors.includes(errorLine)) {
        return null;
    }
    alreadyProcessedErrors.push(errorLine);
    if (AiProvider.isAiAvailable()) {
        if (alreadyProcessedErrors.length > parseInt(process.env.MAX_DEPLOYMENT_TIPS_AI_CALLS || "20")) {
            uxLog("warning", this, c.yellow(`[AI] Maximum number of AI calls for deployment tips reached. Increase with env var MAX_DEPLOYMENT_TIPS_AI_CALLS`));
            return null;
        }
        const prompt = AiProvider.buildPrompt("PROMPT_SOLVE_DEPLOYMENT_ERROR", { "ERROR": errorLine });
        try {
            const aiResponse = await AiProvider.promptAi(prompt, "PROMPT_SOLVE_DEPLOYMENT_ERROR");
            return aiResponse;
        }
        catch (e) {
            uxLog("warning", this, c.yellow("[AI] Error while calling OpenAI: " + e.message));
        }
    }
    return null;
}
//# sourceMappingURL=deployTips.js.map