"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const stream_1 = require("stream");
const serverMetrics_1 = require("../lambda/serverMetrics");
const constants_1 = require("../constants");
const log_1 = __importDefault(require("../log"));
/**
 * Helper class useful for cases when:
 *    - response is not available in traditional sense (eg. lambda environment, fiddle, tests)
 *    - we need to run code that is dependant on having an editable response (cache revalidation)
 */
class LambdaResponse {
    constructor(responseStream, isDirectStream = false, originalStream = undefined) {
        this.isDirectStream = isDirectStream;
        this.originalStream = originalStream;
        // chunks queued for processing + streaming
        this.chunks = [];
        // we remember processed / encoded chunks in order to save them to the cache / dont have to reencode
        this.encodedChunks = [];
        this.onStream = () => { };
        this.onEnd = () => { };
        this.onHeaders = () => { };
        this.onFlush = () => { };
        this.isHeadersStreamed = false;
        this.isEnded = false;
        this.isDownstreamInit = false;
        this.isStreaming = false;
        // the streaming of response is disabled by default,
        // and it's enabled later in modStream if certain conditions are met
        this.isStreamableCallback = () => false;
        this.statusCode = 200;
        this.statusMessage = 'OK';
        this.headers = {};
        this.byteLength = 0;
        this.maxByteLength = 128 * 1000 * 1000;
        this.initDownstream();
        this.isDownstreamInit = false; // reseting the flag so that it can be called one more time in mod
        this.responseStream = responseStream;
    }
    sendResponse(data) {
        this === null || this === void 0 ? void 0 : this.write(data);
    }
    static fromAwsResponseStream(awsResponseStream) {
        if (!awslambda) {
            throw new Error('StreamingAwsResponse can only be used in an AWS Lambda environment.');
        }
        let awsResponseStreamProxy = awsResponseStream;
        const passThrough = new stream_1.PassThrough()
            .on('data', (chunk) => awsResponseStreamProxy.write(chunk))
            .on('end', () => awsResponseStreamProxy.end());
        const res = new LambdaResponse(passThrough, false, awsResponseStream);
        res.setOnHeaders(() => {
            const httpResponseMetadata = {
                statusCode: res.statusCode,
                headers: res.headers,
            };
            awsResponseStreamProxy = awslambda.HttpResponseStream.from(awsResponseStreamProxy, httpResponseMetadata);
        });
        return res;
    }
    static fromNodeResponse(response) {
        const res = new LambdaResponse(response);
        res.setOnHeaders(() => {
            response.writeHead(res.statusCode, res.statusMessage, res.headers);
        });
        return res;
    }
    setHeader(name, value) {
        this.headers[name.toLowerCase()] = value;
    }
    removeHeader(name) {
        delete this.headers[name.toLowerCase()];
    }
    getHeader(name) {
        return this.headers[name.toLowerCase()];
    }
    getHeaders() {
        return this.headers;
    }
    clear() {
        this.chunks = [];
        this.byteLength = 0;
    }
    clearHeaders() {
        Object.keys(this.headers).forEach(header => this.removeHeader(header));
    }
    isCachable() {
        return !this.isDirectStream && this.byteLength < this.maxByteLength;
    }
    async write(chunk) {
        // execute isStreamableCallback only once when we receive the first chunk
        // and store the result for performance reasons
        if (this.chunks.length === 0)
            this.setIsStreamable(this.isStreamable());
        this.chunks.push(chunk);
        this.isStreamable() && (await this.onStream());
    }
    async end() {
        // Add Edgio Metrics into headers
        this.addMetrics();
        // Make sure body and buffer content are in sync
        // when streaming is disabled as users can do various transformations on the response
        // in compute, proxy with transformResponse, etc.
        if (!this.isStreamable() && this.chunks.length > 0 && this.body && this.body.length > 0) {
            // If we have both chunks and body and not streaming, we'll prefer body over the chunks and
            // override existing chunks content with body content, so users can override previously stored response in transformResponse
            // with their own response. Working with single body is more convenient than with chunks for most users.
            this.chunks = [this.body];
            log_1.default.trace('[LambdaResponse] Found both chunks and body. Synced body => chunks');
        }
        if (!this.isStreamable() && this.chunks.length === 0 && this.body) {
            // If we have body, not streaming, and don't have chunks, we'll put body into chunks.
            this.chunks = [this.body];
            log_1.default.trace('[LambdaResponse] Found just body. Synced body => chunks');
        }
        if (!this.isStreamable() && this.chunks.length > 0 && !this.body) {
            // If we have chunks, not streaming, and don't have body, we'll sync body with chunks
            // for future processing if streaming is disabled,
            // so chunks are in sync with body in compute
            // Convert Buffer or string chunks, concat them and store them in body as a single Buffer
            this.body = Buffer.concat(this.chunks.map(chunk => Buffer.from(chunk)));
            log_1.default.trace('[LambdaResponse] Found just chunks. Synced chunks => body');
        }
        // Push 'end' event into the stream if not present,
        // so end can be called multiple times without corrupting response
        if (this.chunks.length === 0 || this.chunks[this.chunks.length - 1] !== null) {
            this.chunks.push(null);
        }
        // Call onStream directly
        // if we don't have any real content in chunks and just null (end of stream) in the chunks
        if (this.chunks.length === 1) {
            this.isStreamable() && (await this.onStream());
        }
    }
    setEncoder(value) {
        this.encoder = value;
    }
    setDecoder(value) {
        this.decoder = value;
    }
    // in cases we dont want to do anything with the body (eg. cache it), we can set this flag to stream directly
    // this will be used in production environment / executeApp in the future in order to not hog resources
    setDirectStream(value) {
        this.isDirectStream = value;
    }
    stream() {
        this.isStreaming = true;
        if (!this.isHeadersStreamed) {
            this.onHeaders();
            this.isHeadersStreamed = true;
        }
        this.chunks.forEach(chunk => this.downstream.push(chunk));
        this.chunks = [];
    }
    async waitForFlush() {
        // if we're not streaming, we dont have to wait, as there's no body
        return this.isStreaming
            ? new Promise(resolve => {
                this.onFlush = resolve;
                // if already ended, we just resolve, as the stream could have ended before callback was set
                this.isEnded && resolve();
            })
            : Promise.resolve(true);
    }
    initDownstream() {
        if (!this.isDownstreamInit) {
            this.isDownstreamInit = true;
            this.downstream = new stream_1.Stream.Readable({
                read: () => {
                    // this must be implemented, we dont need to use it tho
                },
            });
            let curPipe = this.downstream;
            this.decoder && (curPipe = curPipe.pipe(this.decoder));
            this.encoder && (curPipe = curPipe.pipe(this.encoder));
            curPipe.pipe(new stream_1.PassThrough()
                .on('data', chunk => {
                var _a;
                // if we dont have reponse stream we're invoking programatically,
                // eg. in runRoute, revalidation, tests, AND IN PRODUCTION AT THE MOMENT
                // we test for the 'equality' of these approaches in separate LambdaResponse tests
                (_a = this.responseStream) === null || _a === void 0 ? void 0 : _a.write(chunk);
                if (!this.isDirectStream) {
                    this.byteLength += Buffer.byteLength(chunk);
                    // if currently streamed byte length is too big, we stop appending to encoded chunks -> memory will be cleared on chain end
                    // this will consequently disable caching for large payloads
                    this.byteLength < this.maxByteLength && this.encodedChunks.push(chunk);
                }
            })
                .on('end', () => {
                if (this.responseStream) {
                    this.responseStream.end();
                }
                else {
                    // if there are no chunks, we leave body undefined as expected
                    if (this.encodedChunks.length > 0) {
                        if (this.byteLength < this.maxByteLength) {
                            this.body = Buffer.concat(this.encodedChunks);
                        }
                        else {
                            this.statusCode = 413;
                            this.statusMessage = 'Content Too Large';
                        }
                    }
                }
                // we push null to processed chunks, as it signals the end of the stream in case we stream from cache later on
                // it needs to be pushed after the body is concatted, otherwise it fails, as buffers would fail to concat null
                this.encodedChunks.push(null);
                this.isEnded = true;
                this.onFlush();
                // temporary callback for reqResMapper backwards compatibility
                // TODO: this wont be needed once we have streaming in lambda function
                this.onEnd();
            }));
        }
    }
    // allows to enable or disable streaming
    // or set callback that can enable streaming if certain conditions are met
    setIsStreamable(callback) {
        if (typeof callback === 'boolean') {
            return (this.isStreamableCallback = () => callback);
        }
        this.isStreamableCallback = callback;
    }
    // returns true if the response is streamable
    isStreamable() {
        return this.isStreamableCallback();
    }
    // callback used to react to data write, as we need to be able to make a decision whether or not to stream based on many criteria
    setOnStream(callback) {
        this.onStream = callback;
    }
    // TODO: this wont be needed once we have streaming in lambda function
    setOnEnd(callback) {
        this.onEnd = callback;
    }
    setOnHeaders(callback) {
        this.onHeaders = callback;
    }
    // TODO: this wont be needed once we have streaming in lambda function
    getData() {
        return {
            body: this.body,
            statusCode: this.statusCode,
            statusMessage: this.statusMessage,
            headers: this.headers,
        };
    }
    /**
     * Injects the metrics headers into the response
     * @private
     */
    addMetrics() {
        this.setHeader(constants_1.HTTP_HEADERS.xEdgeT, serverMetrics_1.metrics.serialize());
        this.setHeader(constants_1.HTTP_HEADERS.xEdgeStatus, `s=${this.statusCode}`);
    }
    /**
     * Sets the response header to a new value by prefixing it with the given value or,
     * if it doesn't exist, by setting it to the given value,
     *
     * @param headerName The name of the header to prefix or set
     * @param prefixValue The value of the prefix without trailing comma
     * @private
     */
    prefixResponseHeader(headerName, prefixValue) {
        const headerValue = this.headers[headerName];
        if (headerValue) {
            if (Array.isArray(headerValue)) {
                headerValue[0] = `${prefixValue},${headerValue[0]}`;
            }
            else {
                this.headers[headerName] = `${prefixValue},${headerValue}`;
            }
        }
        else {
            this.headers[headerName] = `${prefixValue}`;
        }
    }
}
exports.default = LambdaResponse;
