"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
    var m = o[Symbol.asyncIterator], i;
    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMaxAge = exports.imageOptimizer = void 0;
const path_1 = require("path");
const accept_1 = require("@hapi/accept");
const fs_1 = require("fs");
const crypto_1 = require("crypto");
const serveStatic_1 = require("./serveStatic");
const is_animated_1 = __importDefault(require("is-animated"));
const sendEtagResponse_1 = require("../lib/sendEtagResponse");
const imageConfig_1 = require("./imageConfig");
const s3RetryStrategy_1 = require("../s3/s3RetryStrategy");
const fs = __importStar(require("fs"));
const node_fetch_1 = __importDefault(require("node-fetch"));
let sharp;
const WEBP = "image/webp";
const PNG = "image/png";
const JPEG = "image/jpeg";
const GIF = "image/gif";
const SVG = "image/svg+xml";
const CACHE_VERSION = 2;
const MODERN_TYPES = [WEBP];
const ANIMATABLE_TYPES = [WEBP, PNG, GIF];
const VECTOR_TYPES = [SVG];
const DEFAULT_CACHE_CONTROL = "public, max-age=31536000, immutable";
function imageOptimizer({ basePath, bucketName, region }, imagesManifest, req, res, parsedUrl) {
    var e_1, _a;
    var _b;
    return __awaiter(this, void 0, void 0, function* () {
        const imageConfig = (_b = imagesManifest === null || imagesManifest === void 0 ? void 0 : imagesManifest.images) !== null && _b !== void 0 ? _b : imageConfig_1.imageConfigDefault;
        const { deviceSizes = [], imageSizes = [], domains = [], loader } = imageConfig;
        const sizes = [...deviceSizes, ...imageSizes];
        if (loader !== "default") {
            res.statusCode = 404;
            res.end("default loader not found");
            return { finished: true };
        }
        const { headers } = req;
        const { url, w, q } = parsedUrl.query;
        const mimeType = getSupportedMimeType(MODERN_TYPES, headers.accept);
        let href;
        if (!url) {
            res.statusCode = 400;
            res.end('"url" parameter is required');
            return { finished: true };
        }
        else if (Array.isArray(url)) {
            res.statusCode = 400;
            res.end('"url" parameter cannot be an array');
            return { finished: true };
        }
        let isAbsolute;
        if (url.startsWith("/")) {
            href = url;
            isAbsolute = false;
        }
        else {
            let hrefParsed;
            try {
                hrefParsed = new URL(url);
                href = hrefParsed.toString();
                isAbsolute = true;
            }
            catch (_error) {
                res.statusCode = 400;
                res.end('"url" parameter is invalid');
                return { finished: true };
            }
            if (!["http:", "https:"].includes(hrefParsed.protocol)) {
                res.statusCode = 400;
                res.end('"url" parameter is invalid');
                return { finished: true };
            }
            if (!domains.includes(hrefParsed.hostname)) {
                res.statusCode = 400;
                res.end('"url" parameter is not allowed');
                return { finished: true };
            }
        }
        if (!w) {
            res.statusCode = 400;
            res.end('"w" parameter (width) is required');
            return { finished: true };
        }
        else if (Array.isArray(w)) {
            res.statusCode = 400;
            res.end('"w" parameter (width) cannot be an array');
            return { finished: true };
        }
        if (!q) {
            res.statusCode = 400;
            res.end('"q" parameter (quality) is required');
            return { finished: true };
        }
        else if (Array.isArray(q)) {
            res.statusCode = 400;
            res.end('"q" parameter (quality) cannot be an array');
            return { finished: true };
        }
        const width = parseInt(w, 10);
        if (!width || isNaN(width)) {
            res.statusCode = 400;
            res.end('"w" parameter (width) must be a number greater than 0');
            return { finished: true };
        }
        if (!sizes.includes(width)) {
            res.statusCode = 400;
            res.end(`"w" parameter (width) of ${width} is not allowed`);
            return { finished: true };
        }
        const quality = parseInt(q);
        if (isNaN(quality) || quality < 1 || quality > 100) {
            res.statusCode = 400;
            res.end('"q" parameter (quality) must be a number between 1 and 100');
            return { finished: true };
        }
        const hash = getHash([CACHE_VERSION, href, width, quality, mimeType]);
        const imagesDir = path_1.join("/tmp", "cache", "images");
        const hashDir = path_1.join(imagesDir, hash);
        const now = Date.now();
        if (fs.existsSync(hashDir)) {
            const files = yield fs_1.promises.readdir(hashDir);
            for (const file of files) {
                const [prefix, etag, extension] = file.split(".");
                const expireAt = Number(prefix);
                const contentType = serveStatic_1.getContentType(extension);
                const fsPath = path_1.join(hashDir, file);
                if (now < expireAt) {
                    res.setHeader("Cache-Control", DEFAULT_CACHE_CONTROL);
                    if (sendEtagResponse_1.sendEtagResponse(req, res, etag)) {
                        return { finished: true };
                    }
                    if (contentType) {
                        res.setHeader("Content-Type", contentType);
                    }
                    fs_1.createReadStream(fsPath).pipe(res);
                    return { finished: true };
                }
                else {
                    yield fs_1.promises.unlink(fsPath);
                }
            }
        }
        let upstreamBuffer;
        let upstreamType;
        let maxAge;
        if (isAbsolute) {
            const upstreamRes = yield node_fetch_1.default(href);
            if (!upstreamRes.ok) {
                res.statusCode = upstreamRes.status;
                res.end('"url" parameter is valid but upstream response is invalid');
                return { finished: true };
            }
            res.statusCode = upstreamRes.status;
            upstreamBuffer = Buffer.from(yield upstreamRes.arrayBuffer());
            upstreamType = upstreamRes.headers.get("Content-Type");
            maxAge = getMaxAge(DEFAULT_CACHE_CONTROL);
        }
        else {
            try {
                let s3Key;
                if (href.startsWith(`${basePath}/_next/static`)) {
                    s3Key = href;
                }
                else {
                    s3Key = `${basePath}/public` + href;
                }
                if (s3Key.startsWith("/")) {
                    s3Key = s3Key.slice(1);
                }
                const s3Params = {
                    Bucket: bucketName,
                    Key: s3Key
                };
                const { S3Client } = yield Promise.resolve().then(() => __importStar(require("@aws-sdk/client-s3/S3Client")));
                const s3 = new S3Client({
                    region: region,
                    maxAttempts: 3,
                    retryStrategy: yield s3RetryStrategy_1.buildS3RetryStrategy()
                });
                const { GetObjectCommand } = yield Promise.resolve().then(() => __importStar(require("@aws-sdk/client-s3/commands/GetObjectCommand")));
                const { Body, CacheControl, ContentType } = yield s3.send(new GetObjectCommand(s3Params));
                const s3chunks = [];
                try {
                    for (var _c = __asyncValues(Body), _d; _d = yield _c.next(), !_d.done;) {
                        const s3Chunk = _d.value;
                        s3chunks.push(s3Chunk);
                    }
                }
                catch (e_1_1) { e_1 = { error: e_1_1 }; }
                finally {
                    try {
                        if (_d && !_d.done && (_a = _c.return)) yield _a.call(_c);
                    }
                    finally { if (e_1) throw e_1.error; }
                }
                res.statusCode = 200;
                upstreamBuffer = Buffer.concat(s3chunks);
                upstreamType = ContentType !== null && ContentType !== void 0 ? ContentType : null;
                maxAge = getMaxAge(CacheControl !== null && CacheControl !== void 0 ? CacheControl : null);
                res.setHeader("Cache-Control", DEFAULT_CACHE_CONTROL);
            }
            catch (err) {
                res.statusCode = 500;
                res.end('"url" parameter is valid but upstream response is invalid');
                console.error("Error processing upstream response (S3): " + err);
                return { finished: true };
            }
        }
        if (upstreamType) {
            const vector = VECTOR_TYPES.includes(upstreamType);
            const animate = ANIMATABLE_TYPES.includes(upstreamType) && is_animated_1.default(upstreamBuffer);
            if (vector || animate) {
                sendResponse(req, res, upstreamType, upstreamBuffer);
                return { finished: true };
            }
        }
        const expireAt = maxAge * 1000 + now;
        let contentType;
        if (mimeType) {
            contentType = mimeType;
        }
        else if ((upstreamType === null || upstreamType === void 0 ? void 0 : upstreamType.startsWith("image/")) && serveStatic_1.getExtension(upstreamType)) {
            contentType = upstreamType;
        }
        else {
            contentType = JPEG;
        }
        if (!sharp) {
            try {
                sharp = require("sharp");
            }
            catch (error) {
                if (error.code === "MODULE_NOT_FOUND") {
                    error.message += "\n\nLearn more: https://err.sh/next.js/install-sharp";
                    console.error(error);
                    sendResponse(req, res, upstreamType, upstreamBuffer);
                    return { finished: true };
                }
                throw error;
            }
        }
        try {
            const transformer = sharp(upstreamBuffer);
            transformer.rotate();
            const { width: metaWidth } = yield transformer.metadata();
            if (metaWidth && metaWidth > width) {
                transformer.resize(width);
            }
            if (contentType === WEBP) {
                transformer.webp({ quality });
            }
            else if (contentType === PNG) {
                transformer.png({ quality });
            }
            else if (contentType === JPEG) {
                transformer.jpeg({ quality });
            }
            const optimizedBuffer = yield transformer.toBuffer();
            yield fs_1.promises.mkdir(hashDir, { recursive: true });
            const extension = serveStatic_1.getExtension(contentType);
            const etag = getHash([optimizedBuffer]);
            const filename = path_1.join(hashDir, `${expireAt}.${etag}.${extension}`);
            yield fs_1.promises.writeFile(filename, optimizedBuffer);
            sendResponse(req, res, contentType, optimizedBuffer);
        }
        catch (error) {
            console.error("Error processing image with sharp, returning upstream image as fallback instead: " +
                error.stack);
            sendResponse(req, res, upstreamType, upstreamBuffer);
        }
        return { finished: true };
    });
}
exports.imageOptimizer = imageOptimizer;
function sendResponse(req, res, contentType, buffer) {
    const etag = getHash([buffer]);
    res.setHeader("Cache-Control", DEFAULT_CACHE_CONTROL);
    if (sendEtagResponse_1.sendEtagResponse(req, res, etag)) {
        return;
    }
    if (contentType) {
        res.setHeader("Content-Type", contentType);
    }
    res.end(buffer);
}
function getSupportedMimeType(options, accept = "") {
    const mimeType = accept_1.mediaType(accept, options);
    return accept.includes(mimeType) ? mimeType : "";
}
function getHash(items) {
    const hash = crypto_1.createHash("sha256");
    for (let item of items) {
        if (typeof item === "number")
            hash.update(String(item));
        else {
            hash.update(item);
        }
    }
    return hash.digest("base64").replace(/\//g, "-");
}
function parseCacheControl(str) {
    const map = new Map();
    if (!str) {
        return map;
    }
    for (let directive of str.split(",")) {
        let [key, value] = directive.trim().split("=");
        key = key.toLowerCase();
        if (value) {
            value = value.toLowerCase();
        }
        map.set(key, value);
    }
    return map;
}
function getMaxAge(str) {
    const minimum = 2678400;
    const map = parseCacheControl(str);
    if (map) {
        let age = map.get("s-maxage") || map.get("max-age") || "";
        if (age.startsWith('"') && age.endsWith('"')) {
            age = age.slice(1, -1);
        }
        const n = parseInt(age, 10);
        if (!isNaN(n)) {
            return Math.max(n, minimum);
        }
    }
    return minimum;
}
exports.getMaxAge = getMaxAge;
