// Copyright (c) 2016, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//     * Redistributions of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

import fs from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';

import * as Sentry from '@sentry/node';
import express from 'express';
import Server from 'http-proxy';
import {Counter, Gauge} from 'prom-client';
import _ from 'underscore';
import which from 'which';

import {remove, splitArguments} from '../../shared/common-utils.js';
import {
    ActiveTool,
    BypassCache,
    ExecutionParams,
    FiledataPair,
    LegacyCompatibleActiveTool,
    UnparsedExecutionParams,
} from '../../types/compilation/compilation.interfaces.js';
import {CompilerOverrideOptions} from '../../types/compilation/compiler-overrides.interfaces.js';
import {CompilerInfo, PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import {LanguageKey} from '../../types/languages.interfaces.js';
import {SelectedLibraryVersion} from '../../types/libraries/libraries.interfaces.js';
import {ResultLine} from '../../types/resultline/resultline.interfaces.js';
import {AppArguments} from '../app.interfaces.js';
import {BaseCompiler} from '../base-compiler.js';
import {parseExecutionParameters, parseTools, parseUserArguments} from '../compilation/compilation-request-parser.js';
import {CompilationEnvironment} from '../compilation-env.js';
import {getCompilerTypeByKey} from '../compilers/index.js';
import {logger} from '../logger.js';
import {ClientOptionsType} from '../options-handler.js';
import {PropertyGetter} from '../properties.interfaces.js';
import {SentryCapture} from '../sentry.js';
import {KnownBuildMethod} from '../stats.js';
import * as temp from '../temp.js';
import * as utils from '../utils.js';
import {
    CompileRequestJsonBody,
    CompileRequestQueryArgs,
    CompileRequestTextBody,
    ICompileHandler,
} from './compile.interfaces.js';

let hasSetUpAutoClean = false;

function initialise(compilerEnv: CompilationEnvironment) {
    if (hasSetUpAutoClean) return;
    hasSetUpAutoClean = true;

    new Gauge({
        name: 'ce_compilation_temp_dirs_created',
        help: 'Total number of temporary directories created',
        async collect() {
            this.set(temp.getStats().numCreated);
        },
    });
    new Gauge({
        name: 'ce_compilation_temp_dirs_removed',
        help: 'Total number of temporary directories that had to be removed by the temp system',
        async collect() {
            this.set(temp.getStats().numRemoved);
        },
    });
    const tempDirsBusy = new Counter({
        name: 'ce_compilation_temp_dirs_busy',
        help: 'Total number of times the temp dir system was busy',
    });

    const tempDirCleanupSecs = compilerEnv.ceProps('tempDirCleanupSecs', 30);
    logger.info(`Cleaning temp dirs every ${tempDirCleanupSecs} secs`);

    let cyclesBusy = 0;
    setInterval(async () => {
        const status = compilerEnv.compilationQueue!.status();
        if (status.busy) {
            cyclesBusy++;
            tempDirsBusy.inc();
            logger.warn(
                `temp cleanup skipped, pending: ${status.pending}, waiting: ${status.size}, cycles: ${cyclesBusy}`,
            );
            return;
        }

        cyclesBusy = 0;

        await temp.cleanup();
        logger.debug('Temp cleanup stats', temp.getStats());
    }, tempDirCleanupSecs * 1000);
}

export type ParsedRequest = {
    source: string;
    options: string[];
    backendOptions: Record<string, any>;
    filters: ParseFiltersAndOutputOptions;
    bypassCache: BypassCache;
    tools: ActiveTool[];
    executeParameters: ExecutionParams;
    libraries: SelectedLibraryVersion[];
};

export class CompileHandler implements ICompileHandler {
    private compilersById: Partial<Record<LanguageKey, Record<string, BaseCompiler>>> = {};
    private readonly compilerEnv: CompilationEnvironment;
    private readonly textBanner: string;
    private readonly proxy: Server;
    private readonly awsProps: PropertyGetter;
    private readonly appArgs: AppArguments | undefined;
    private clientOptions: ClientOptionsType | null = null;
    private readonly compileCounter = new Counter({
        name: 'ce_compilations_total',
        help: 'Number of compilations',
        labelNames: ['language'],
    });
    private readonly executeCounter = new Counter({
        name: 'ce_executions_total',
        help: 'Number of executions',
        labelNames: ['language'],
    });
    private readonly cmakeCounter = new Counter({
        name: 'ce_cmake_compilations_total',
        help: 'Number of CMake compilations',
        labelNames: ['language'],
    });
    private readonly cmakeExecuteCounter = new Counter({
        name: 'ce_cmake_executions_total',
        help: 'Number of executions after CMake',
        labelNames: ['language'],
    });

    constructor(compilationEnvironment: CompilationEnvironment, awsProps: PropertyGetter, appArgs?: AppArguments) {
        this.compilerEnv = compilationEnvironment;
        this.textBanner = this.compilerEnv.ceProps<string>('textBanner');
        this.proxy = Server.createProxyServer({});
        this.awsProps = awsProps;
        this.appArgs = appArgs;
        initialise(this.compilerEnv);

        // Mostly cribbed from
        // https://github.com/nodejitsu/node-http-proxy/blob/master/examples/middleware/bodyDecoder-middleware.js
        // We just keep the body as-is though: no encoding using queryString.stringify(), as we don't use a form
        // decoding middleware.
        this.proxy.on('proxyReq', (proxyReq, req) => {
            // TODO ideally I'd work out if this is "ok" - IncomingMessage doesn't have a body, but pragmatically the
            //  object we get here does (introduced by express.json).
            const body = (req as any).body;
            if (!body || Object.keys(body).length === 0) {
                return;
            }
            let contentType: string = proxyReq.getHeader('Content-Type') as string;
            let bodyData;

            if (contentType === 'application/json') {
                bodyData = JSON.stringify(body);
            } else if (contentType === 'application/x-www-form-urlencoded') {
                // Reshape the form body into what a json request looks like
                contentType = 'application/json';
                bodyData = JSON.stringify({
                    lang: body.lang,
                    compiler: body.compiler,
                    source: body.source,
                    options: {
                        userArguments: body.userArguments,
                        filters: Object.fromEntries(
                            [
                                'commentOnly',
                                'directives',
                                'libraryCode',
                                'labels',
                                'demangle',
                                'intel',
                                'execute',
                                'debugCalls',
                                'binary',
                                'binaryObject',
                                'trim',
                            ].map(key => [key, body[key] === 'true']),
                        ),
                    },
                });
            } else {
                SentryCapture(
                    new Error(`Unexpected Content-Type received by /compiler/:compiler/compile: ${contentType}`),
                    'lib/handlers/compile.ts proxyReq contentType',
                );
                proxyReq.write('Unexpected Content-Type');
            }

            try {
                if (bodyData) {
                    proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
                    proxyReq.setHeader('Content-Type', contentType);
                    proxyReq.write(bodyData);
                }
            } catch (e: any) {
                SentryCapture(e, 'lib/handlers/compile.ts proxyReq.write');
                let json = '<json stringify error>';
                try {
                    json = JSON.stringify(bodyData);
                } catch {}
                Sentry.captureMessage(`Unknown proxy bodyData: ${typeof bodyData} ${json}`);
                proxyReq.write('Proxy error');
            }
        });
    }

    hasLanguages(): boolean {
        try {
            return Object.keys(this.compilersById).length > 0;
        } catch {
            return false;
        }
    }

    async create(compiler: PreliminaryCompilerInfo): Promise<BaseCompiler | null> {
        const isPrediscovered = !!compiler.version;

        const type = compiler.compilerType || 'default';
        let compilerClass: ReturnType<typeof getCompilerTypeByKey>;
        try {
            compilerClass = getCompilerTypeByKey(type);
        } catch (e: any) {
            logger.error(`Compiler ID: ${compiler.id}`);
            logger.error(e);
            logger.error(e.stack);
            process.exit(1);
        }

        // attempt to resolve non absolute exe paths
        if (compiler.exe && !path.isAbsolute(compiler.exe)) {
            const exe = await which(compiler.exe).catch(() => null);
            if (exe) {
                logger.debug(`Resolved '${compiler.exe}' to path '${exe}'`);
                compiler.exe = exe;
            } else {
                // errors resolving to absolute path are not fatal for backwards compatibility sake
                logger.error(`Unable to resolve '${compiler.exe}'`);
            }
        }

        if (compiler.exe && path.isAbsolute(compiler.exe)) {
            // Try stat'ing the compiler to cache its mtime and only re-run it if it
            // has changed since the last time.
            try {
                let modificationTime: Date;
                if (isPrediscovered) {
                    modificationTime = compiler.mtime;
                } else {
                    const res = await fs.stat(compiler.exe);
                    modificationTime = res.mtime;
                    const cached = this.findCompiler(compiler.lang, compiler.id);
                    if (cached && cached.getModificationTime() === res.mtime.getTime()) {
                        logger.debug(`${compiler.id} is unchanged`);
                        return cached;
                    }
                }
                const compilerObj = new compilerClass(compiler, this.compilerEnv);
                return compilerObj.initialise(modificationTime, this.clientOptions, isPrediscovered);
            } catch (err) {
                logger.warn(`Unable to stat ${compiler.id} compiler binary: `, err);
                return null;
            }
        } else {
            return new compilerClass(compiler, this.compilerEnv) as BaseCompiler;
        }
    }

    async setCompilers(
        compilers: PreliminaryCompilerInfo[],
        clientOptions: ClientOptionsType,
    ): Promise<CompilerInfo[]> {
        // Be careful not to update this.compilersById until we can replace it entirely.
        const compilersById: Partial<Record<LanguageKey, Record<string, BaseCompiler>>> = {};
        try {
            this.clientOptions = clientOptions;
            const totalCompilers = compilers.length;
            logger.info('Creating compilers: ' + totalCompilers);
            let compilersCreated = 0;
            const createdCompilers = remove(await Promise.all(compilers.map(c => this.create(c))), null);
            for (const compiler of createdCompilers) {
                const langId = compiler.getInfo().lang;
                if (!compilersById[langId]) compilersById[langId] = {};
                compilersById[langId][compiler.getInfo().id] = compiler;
                compilersCreated++;
            }

            const failedCount = totalCompilers - compilersCreated;
            if (failedCount > 0) {
                logger.error(`Failed to create ${failedCount} out of ${totalCompilers} compilers`);

                if (this.appArgs?.exitOnCompilerFailure) {
                    logger.error('Exiting due to compiler creation failures (exitOnCompilerFailure=true)');
                    process.exit(1);
                }
            }

            logger.info('Compilers created: ' + compilersCreated);
            if (this.awsProps) {
                logger.info('Fetching possible arguments from storage');
                await Promise.all(
                    createdCompilers.map(compiler => compiler.possibleArguments.loadFromStorage(this.awsProps)),
                );
            }
            this.compilersById = compilersById;
            return createdCompilers.map(compiler => compiler.getInfo());
        } catch (err) {
            logger.error('Exception while processing compilers:', err);
            return [];
        }
    }

    setPossibleToolchains(toolchains: CompilerOverrideOptions) {
        this.compilerEnv.setPossibleToolchains(toolchains);
    }

    compilerAliasMatch(compiler: BaseCompiler, compilerId: string): boolean {
        return compiler.compiler.alias?.includes(compilerId);
    }

    compilerIdOrAliasMatch(compiler: BaseCompiler, compilerId: string): boolean {
        return compiler.compiler.id === compilerId || this.compilerAliasMatch(compiler, compilerId);
    }

    findCompiler(langId: LanguageKey, compilerId: string): BaseCompiler | undefined {
        if (!compilerId) return;

        const langCompilers: Record<string, BaseCompiler> | undefined = this.compilersById[langId];
        if (langCompilers) {
            if (langCompilers[compilerId]) {
                return langCompilers[compilerId];
            }
            const compiler = _.find(langCompilers, (compiler: BaseCompiler) => {
                return this.compilerAliasMatch(compiler, compilerId);
            });

            if (compiler) return compiler;
        }

        // If the lang is bad, try to find it in every language
        let response: BaseCompiler | undefined;
        _.each(this.compilersById, (compilerInLang: Record<string, BaseCompiler>) => {
            if (!response) {
                response = _.find(compilerInLang, (compiler: BaseCompiler) => {
                    return this.compilerIdOrAliasMatch(compiler, compilerId);
                });
            }
        });

        return response;
    }

    compilerFor(req): BaseCompiler | undefined {
        if (req.is('json')) {
            const lang = req.lang || req.body.lang;
            const compiler = this.findCompiler(lang, req.params.compiler);
            if (!compiler) {
                const withoutBody = _.extend({}, req.body, {source: '<removed>'});
                logger.warn(`Unable to find compiler with lang ${lang} for JSON request`, withoutBody);
            }
            return compiler;
        }
        if (req.body?.compiler) {
            const compiler = this.findCompiler(req.body.lang, req.body.compiler);
            if (!compiler) {
                const withoutBody = _.extend({}, req.body, {source: '<removed>'});
                logger.warn(`Unable to find compiler with lang ${req.body.lang} for request`, withoutBody);
            }
            return compiler;
        }
        const compiler = this.findCompiler(req.lang, req.params.compiler);
        if (!compiler) {
            logger.warn(`Unable to find compiler with lang ${req.lang} for request params`, req.params);
        }
        return compiler;
    }

    static checkRequestRequirements(body: any): CompileRequestJsonBody {
        if (body.options === undefined) throw new Error('Missing options property');
        if (body.source === undefined) throw new Error('Missing source property');
        return body;
    }

    static parseRequestReusable(isJson: boolean, query: any, body: any, compiler: BaseCompiler): ParsedRequest {
        let source: string;
        let options: string;
        let backendOptions: Record<string, any> = {};
        let filters: ParseFiltersAndOutputOptions;
        let bypassCache = BypassCache.None;
        let inputTools: LegacyCompatibleActiveTool[] = [];
        const execReqParams: UnparsedExecutionParams = {};
        let libraries: any[] = [];
        // IF YOU MODIFY ANYTHING HERE PLEASE UPDATE THE DOCUMENTATION!
        if (isJson) {
            // JSON-style request
            const jsonRequest = CompileHandler.checkRequestRequirements(body);
            const requestOptions = jsonRequest.options;
            source = jsonRequest.source;
            if (jsonRequest.bypassCache) bypassCache = jsonRequest.bypassCache;
            options = requestOptions.userArguments;
            const execParams = requestOptions.executeParameters || {};
            execReqParams.args = execParams.args;
            execReqParams.stdin = execParams.stdin;
            execReqParams.runtimeTools = execParams.runtimeTools;
            backendOptions = requestOptions.compilerOptions || {};
            filters = {...compiler.getDefaultFilters(), ...requestOptions.filters};
            inputTools = requestOptions.tools || [];
            libraries = requestOptions.libraries || [];
        } else if (body?.compiler) {
            const textRequest = body as CompileRequestTextBody;
            source = textRequest.source;
            if (textRequest.bypassCache) bypassCache = textRequest.bypassCache;
            options = textRequest.userArguments;
            execReqParams.args = textRequest.executeParametersArgs;
            execReqParams.stdin = textRequest.executeParametersStdin;

            filters = compiler.getDefaultFilters();
            _.each(filters, (value, item) => {
                filters[item] = textRequest[item as keyof CompileRequestTextBody] === 'true';
            });

            backendOptions.filterAnsi = textRequest.filterAnsi === 'true';
            backendOptions.skipAsm = textRequest.skipAsm === 'true';
        } else {
            // API-style
            source = body || '';
            options = query.options || '';
            // By default we get the default filters.
            filters = compiler.getDefaultFilters();
            // If specified exactly, we'll take that with ?filters=a,b,c
            if (query.filters) {
                filters = _.object(
                    query.filters.split(',').map(filter => [filter, true]),
                ) as any as ParseFiltersAndOutputOptions;
            }
            // Add a filter. ?addFilters=binary
            _.each((query.addFilters || '').split(','), filter => {
                if (filter) filters[filter] = true;
            });
            // Remove a filter. ?removeFilter=intel
            _.each((query.removeFilters || '').split(','), filter => {
                if (filter) delete filters[filter as keyof ParseFiltersAndOutputOptions];
            });
            // Ask for asm not to be returned
            backendOptions.skipAsm = query.skipAsm === 'true';
            backendOptions.skipPopArgs = query.skipPopArgs === 'true';
            backendOptions.filterAnsi = query.filterAnsi === 'true';
        }
        // Use shared parsing utilities for consistency with SQS workers
        const executeParameters = parseExecutionParameters(execReqParams);
        const tools = parseTools(inputTools);

        // Backwards compatibility: bypassCache used to be a boolean.
        // Convert a boolean input to an enum's underlying numeric value
        bypassCache = 1 * bypassCache;

        return {
            source,
            options: parseUserArguments(options), // Use shared utility for consistency
            backendOptions,
            filters,
            bypassCache,
            tools,
            executeParameters,
            libraries,
        };
    }

    parseRequest(req: express.Request, compiler: BaseCompiler): ParsedRequest {
        const isJson = !!req.is('json');
        const query = req.query as CompileRequestQueryArgs;
        const body = req.body;

        return CompileHandler.parseRequestReusable(isJson, query, body, compiler);
    }

    handlePopularArguments(req: express.Request, res: express.Response) {
        const compiler = this.compilerFor(req);
        if (!compiler) {
            res.sendStatus(404);
            return;
        }
        res.send(compiler.possibleArguments.getPopularArguments(this.getUsedOptions(req)));
    }

    handleOptimizationArguments(req: express.Request, res: express.Response) {
        const compiler = this.compilerFor(req);
        if (!compiler) {
            res.sendStatus(404);
            return;
        }
        res.send(compiler.possibleArguments.getOptimizationArguments(this.getUsedOptions(req)));
    }

    getUsedOptions(req: express.Request) {
        if (req.body) {
            const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;

            if (data.presplit) {
                return data.usedOptions;
            }
            return splitArguments(data.usedOptions);
        }
        return false;
    }

    handleApiError(error: any, res: express.Response, next: express.NextFunction) {
        if (error.message) {
            res.status(400).send({
                error: true,
                message: error.message,
            });
            return;
        }
        next(error);
    }

    handleCmake(req: express.Request, res: express.Response, next: express.NextFunction) {
        const compiler = this.compilerFor(req);
        if (!compiler) {
            res.sendStatus(404);
            return;
        }

        const remote = compiler.getRemote();
        if (remote) {
            req.url = remote.cmakePath;
            this.proxy.web(req, res, {target: remote.target, changeOrigin: true}, (e: any) => {
                logger.error('Proxy error: ', e);
                next(e);
            });
            return;
        }

        try {
            if (req.body.files === undefined) throw new Error('Missing files property');

            this.cmakeCounter.inc({language: compiler.lang.id});
            const parsedRequest = this.parseRequest(req, compiler);
            this.compilerEnv.statsNoter.noteCompilation(
                compiler.getInfo().id,
                parsedRequest,
                req.body.files as FiledataPair[],
                KnownBuildMethod.CMake,
            );
            compiler
                // Backwards compatibility: bypassCache used to be a boolean.
                // Convert a boolean input to an enum's underlying numeric value
                .cmake(req.body.files, parsedRequest, req.body.bypassCache * 1)
                .then(result => {
                    if (result.didExecute || result.execResult?.didExecute)
                        this.cmakeExecuteCounter.inc({language: compiler.lang.id});
                    delete result.s3Key; // Remove s3Key before sending to user
                    res.send(result);
                })
                .catch(e => {
                    return this.handleApiError(e, res, next);
                });
        } catch (e) {
            this.handleApiError(e, res, next);
            return;
        }
    }

    handle(req: express.Request, res: express.Response, next: express.NextFunction) {
        const compiler = this.compilerFor(req);
        if (!compiler) {
            res.sendStatus(404);
            return;
        }

        const remote = compiler.getRemote();
        if (remote) {
            req.url = remote.path;
            this.proxy.web(req, res, {target: remote.target, changeOrigin: true}, (e: any) => {
                logger.error('Proxy error: ', e);
                next(e);
            });
            return;
        }

        let parsedRequest: ParsedRequest | undefined;
        try {
            parsedRequest = this.parseRequest(req, compiler);
        } catch (error) {
            this.handleApiError(error, res, next);
            return;
        }

        const {source, options, backendOptions, filters, bypassCache, tools, executeParameters, libraries} =
            parsedRequest;

        let files: FiledataPair[] = [];
        if (req.body.files) files = req.body.files;

        if (source === undefined || Object.keys(req.body).length === 0) {
            logger.warn('No body found in request', req);
            return next(new Error('Bad request'));
        }

        function textify(array: ResultLine[] | null, filterAnsi: boolean | undefined) {
            const text = (array || []).map(line => line.text).join('\n');
            if (filterAnsi) {
                // https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
                return text.replaceAll(/(\x9B|\x1B\[)[\d:;<=>?]*[ -/]*[@-~]/g, '');
            }
            return text;
        }

        this.compileCounter.inc({language: compiler.lang.id});
        this.compilerEnv.statsNoter.noteCompilation(
            compiler.getInfo().id,
            parsedRequest,
            files as FiledataPair[],
            KnownBuildMethod.Compile,
        );

        compiler
            .compile(source, options, backendOptions, filters, bypassCache, tools, executeParameters, libraries, files)
            .then(
                result => {
                    if (result.didExecute || result.execResult?.didExecute)
                        this.executeCounter.inc({language: compiler.lang.id});
                    if (req.accepts(['text', 'json']) === 'json') {
                        delete result.s3Key; // Remove s3Key before sending to user
                        res.send(result);
                    } else {
                        res.set('Content-Type', 'text/plain');
                        try {
                            if (!_.isEmpty(this.textBanner)) res.write('# ' + this.textBanner + '\n');
                            res.write(textify(result.asm, backendOptions.filterAnsi));
                            if (result.code !== 0) res.write('\n# Compiler exited with result code ' + result.code);
                            if (!_.isEmpty(result.stdout))
                                res.write('\nStandard out:\n' + textify(result.stdout, backendOptions.filterAnsi));
                            if (!_.isEmpty(result.stderr))
                                res.write('\nStandard error:\n' + textify(result.stderr, backendOptions.filterAnsi));

                            if (result.execResult) {
                                res.write('\n\n# Execution result with exit code ' + result.execResult.code + '\n');
                                if (!_.isEmpty(result.execResult.stdout)) {
                                    res.write(
                                        '# Standard out:\n' +
                                            textify(result.execResult.stdout, backendOptions.filterAnsi),
                                    );
                                }
                                if (!_.isEmpty(result.execResult.stderr)) {
                                    res.write(
                                        '\n# Standard error:\n' +
                                            textify(result.execResult.stderr, backendOptions.filterAnsi),
                                    );
                                }
                            }
                        } catch (ex) {
                            SentryCapture(ex, 'lib/handlers/compile.ts res.write');
                            res.write(`Error handling request: ${ex}`);
                        }
                        res.end('\n');
                    }
                },
                error => {
                    if (typeof error === 'string') {
                        logger.error('Error during compilation 1: ', {error});
                    } else {
                        if (error.stack) {
                            logger.error('Error during compilation 2: ', error);
                            SentryCapture(error, 'compile failed');
                        } else if (error.code) {
                            logger.error('Error during compilation 3: ', error.code);
                            if (typeof error.stderr === 'string') {
                                error.stdout = utils.parseOutput(error.stdout);
                                error.stderr = utils.parseOutput(error.stderr);
                            }
                            res.end(JSON.stringify(error));
                            return;
                        } else {
                            logger.error('Error during compilation 4: ', error);
                        }

                        error = `Internal Compiler Explorer error: ${error.stack || error}`;
                    }
                    res.end(JSON.stringify({code: -1, stdout: [], stderr: [{text: error}]}));
                },
            );
    }
}

export function SetTestMode() {
    hasSetUpAutoClean = true;
}
