"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileCache = void 0;
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const node_path_1 = require("node:path");
const cachedir_1 = __importDefault(require("cachedir"));
const upload_1 = require("../upload");
const env_1 = require("../env/env");
const loggingProvider_1 = require("../providers/logging/loggingProvider");
const DEFAULT_READ_BUFFER_SIZE_BYTES = 8 * 1024 * 1024;
class File {
    constructor(directory, name) {
        this.path = (0, node_path_1.resolve)((0, node_path_1.join)(directory, name));
        this.stats = (0, fs_1.statSync)(this.path);
    }
    get createdTimeMillis() {
        return this.stats.ctimeMs;
    }
    delete() {
        (0, fs_1.rmSync)(this.path, { force: true, recursive: this.isDirectory });
    }
    get isDirectory() {
        return this.stats.isDirectory();
    }
}
class FileCache {
    constructor({ cacheName, maximumCachedFiles, maximumCachedFileAgeMinutes, readBufferSizeBytes, }) {
        this.cacheName = cacheName;
        this.cacheDirectoryPath = (0, node_path_1.resolve)((0, node_path_1.join)((0, cachedir_1.default)(env_1.CONF_FILE_PROJECT_NAME), cacheName));
        this.maximumCachedFiles = maximumCachedFiles;
        this.maximumCachedFileAgeMinutes = maximumCachedFileAgeMinutes;
        this.readBufferSizeBytes =
            readBufferSizeBytes ?? DEFAULT_READ_BUFFER_SIZE_BYTES;
    }
    async performCacheCleanup() {
        if (!(await this.directoryExists(this.cacheDirectoryPath))) {
            return;
        }
        const nowMillis = Date.now();
        try {
            const files = (await (0, promises_1.readdir)(this.cacheDirectoryPath))
                .map((name) => new File(this.cacheDirectoryPath, name))
                .filter((file) => {
                if (this.maximumCachedFileAgeMinutes &&
                    (nowMillis - file.createdTimeMillis) / 1000 / 60 >
                        this.maximumCachedFileAgeMinutes) {
                    loggingProvider_1.logger.info(`Deleting expired cache ${file.isDirectory ? 'directory' : 'file'} ${file.path}`);
                    file.delete();
                    return false;
                }
                return true;
            })
                .sort((left, right) => right.createdTimeMillis - left.createdTimeMillis);
            if (this.maximumCachedFiles && files.length > this.maximumCachedFiles) {
                loggingProvider_1.logger.info(`Deleting the oldest ${files.length - this.maximumCachedFiles} file(s) in ${this.cacheName} cache`);
                files.slice(this.maximumCachedFiles).forEach((file) => {
                    loggingProvider_1.logger.info(`Deleting cache ${file.isDirectory ? 'directory' : 'file'} ${file.path}`);
                    file.delete();
                });
            }
        }
        catch (error) {
            loggingProvider_1.logger.error(`Error performing cache cleanup on ${this.cacheDirectoryPath}`, error);
        }
    }
    async getOrCreateFile(name, getReader, metadata) {
        const path = this.getCacheFilePath(name);
        await this.performCacheCleanup();
        if (await this.cachedFileIsValid(path, metadata)) {
            return path;
        }
        const { crc32cHex: writtenCrc32cHex, sizeBytes: writtenSizeBytes } = await this.write(await getReader(), path, metadata?.sizeBytes);
        if (metadata) {
            if (writtenSizeBytes !== metadata.sizeBytes) {
                throw new Error(`Expected ${metadata.sizeBytes}b but received ${writtenSizeBytes}b`);
            }
            if (writtenCrc32cHex !== metadata.crc32cHex) {
                throw new Error(`Expected checksum ${metadata.crc32cHex} but computed ${writtenCrc32cHex}`);
            }
        }
        if (!(await this.cachedFileIsValid(path, metadata))) {
            throw new Error(`Downloaded cache file ${path} failed verification`);
        }
        return path;
    }
    async directoryExists(path) {
        try {
            return (await (0, promises_1.stat)(path)).isDirectory();
        }
        catch (error) {
            return false;
        }
    }
    async fileExists(path) {
        try {
            return (await (0, promises_1.stat)(path)).isFile();
        }
        catch (error) {
            return false;
        }
    }
    async cachedFileIsValid(path, metadata) {
        if (!(await this.fileExists(path))) {
            return false;
        }
        if (metadata) {
            loggingProvider_1.logger.info(`Verifying integrity of cache file ${path}`);
            const actualSizeBytes = (await (0, promises_1.stat)(path)).size;
            if (actualSizeBytes !== metadata.sizeBytes) {
                loggingProvider_1.logger.warn(`Expected cache file at ${path} to have size [${metadata.sizeBytes}b] but was [${actualSizeBytes}b]`);
                return false;
            }
            const actualCrc32cHex = await this.computeCrc32cHex(path);
            if (actualCrc32cHex !== metadata.crc32cHex) {
                loggingProvider_1.logger.warn(`Expected cache file at ${path} to have checksum [${metadata.crc32cHex}] but was [${actualCrc32cHex}]}`);
                return false;
            }
        }
        return true;
    }
    getCacheFilePath(relativePath) {
        return (0, node_path_1.resolve)((0, node_path_1.join)(this.cacheDirectoryPath, relativePath));
    }
    async write(reader, destination, expectedSizeBytes) {
        const destinationDirectory = (0, node_path_1.dirname)(destination);
        if (!(await this.directoryExists(destinationDirectory))) {
            await (0, promises_1.mkdir)(destinationDirectory, { recursive: true });
        }
        const fd = await (0, promises_1.open)(destination, 'w');
        const writeStream = fd.createWriteStream({ autoClose: true });
        const metadata = await new Promise((resolve, reject) => {
            const crc32c = new upload_1.Crc32cChecksum();
            let sizeBytes = 0;
            let percentageComplete = 0;
            writeStream.on('error', (error) => reject(error));
            writeStream.on('close', () => resolve({ crc32cHex: crc32c.digestHex(), sizeBytes }));
            reader.on('error', (error) => reject(error));
            reader.on('end', () => writeStream.close());
            reader.on('data', (chunk) => {
                crc32c.update(chunk);
                sizeBytes += chunk.byteLength;
                if (expectedSizeBytes) {
                    const newPercentageComplete = Math.round((sizeBytes / expectedSizeBytes) * 100);
                    if (newPercentageComplete % 10 === 0 &&
                        newPercentageComplete > percentageComplete) {
                        percentageComplete = newPercentageComplete;
                        loggingProvider_1.logger.info(`Downloaded ${sizeBytes} of ${expectedSizeBytes} bytes (${percentageComplete}% complete)`);
                    }
                }
            });
            reader.pipe(writeStream);
        });
        await this.performCacheCleanup();
        return metadata;
    }
    async computeCrc32cHex(path) {
        const fd = await (0, promises_1.open)(path, 'r');
        const crc32c = new upload_1.Crc32cChecksum();
        let totalBytesRead = 0;
        while (true) {
            const { buffer, bytesRead: chunkSizeBytes } = await fd.read({
                buffer: Buffer.alloc(this.readBufferSizeBytes),
                length: this.readBufferSizeBytes,
                position: totalBytesRead,
            });
            if (chunkSizeBytes === 0) {
                break;
            }
            totalBytesRead += chunkSizeBytes;
            const chunk = buffer.subarray(0, chunkSizeBytes);
            crc32c.update(chunk);
        }
        return crc32c.digestHex();
    }
}
exports.FileCache = FileCache;
