import { createWriteStream, createReadStream, promises as fs } from 'node:fs';
import readline from 'node:readline';
import { pipeline, Readable } from 'node:stream';
import { promisify } from 'node:util';
import { join } from 'node:path';
import debug from 'debug';
import rimraf from '@zkochan/rimraf';
import { last } from 'rambda';
import { doesFileExist, retry } from '../../utils.js';
const logFetch = debug('fetch');
const logNetwork = logFetch.extend('network-io');
const logDisk = logFetch.extend('disk-io');
const streamPipeline = promisify(pipeline);
let networkHits = 0;
let cacheHits = 0;
const cacheLocation = join(process.cwd(), 'cache');
const cachePath = (pathParts) => pathParts.length === 0
    ? [cacheLocation, `${pathParts[0]}.txt`]
    : [join(cacheLocation, ...pathParts.slice(0, -1)), `${last(pathParts)}.txt`];
const fileNameForLogs = (fileName) => fileName.replace(`${process.cwd()}/`, '');
const getFirstLine = async (pathToFile) => {
    const readable = createReadStream(pathToFile);
    const reader = readline.createInterface({ input: readable });
    const line = await new Promise(resolve => {
        reader.on('line', line => {
            reader.close();
            resolve(line);
        });
    });
    readable.close();
    return line;
};
const getFrontMatter = async (filePath) => JSON.parse(await getFirstLine(filePath));
const looksLikeDate = (value) => /\d{4}-[01]\d-[0-3]\dT[0-2](?:\d:[0-5]){2}\d(.*Z)/.test(value);
const parseDate = (_, value) => {
    if (typeof value !== 'string')
        return value;
    if (!looksLikeDate(value))
        return value;
    return new Date(value);
};
const streamToDisk = async (fileLocation, fetcher) => {
    const response = await fetcher();
    const filePath = join(...fileLocation);
    if (!response.body && response.status !== 204) {
        logNetwork(`HTTP error: Stream is empty. ${response.url}`);
        throw new Error(`HTTP error: Stream is empty. ${response.url}`);
    }
    logNetwork(`Status: ${response.status}. Streaming from ${response.url} to ${fileNameForLogs(filePath)}`);
    await fs.mkdir(fileLocation[0], { recursive: true });
    const fileStream = createWriteStream(filePath);
    fileStream.write(JSON.stringify({
        date: Date.now(),
        status: response.status,
        headers: Object.fromEntries(response.headers.entries()),
    }));
    fileStream.write('\n');
    const readStream = response.body || Readable.from([]);
    await streamPipeline(readStream, fileStream);
    logDisk(`Wrote ${fileNameForLogs(filePath)}`);
};
export const fetchCounters = () => ({ networkHits, cacheHits });
export default (diskCacheTimeMs) => ({
    usingDiskCache: async (pathParts, fetcher) => retry(async () => {
        const fileLocation = cachePath(pathParts);
        const filePath = join(...fileLocation);
        let fromCache = true;
        if (!(await doesFileExist(filePath))) {
            fromCache = false;
            await streamToDisk(fileLocation, fetcher);
        }
        else if (Date.now() - (await getFrontMatter(filePath)).date > diskCacheTimeMs) {
            fromCache = false;
            await streamToDisk(fileLocation, fetcher);
        }
        if (fromCache) {
            cacheHits += 1;
        }
        else {
            networkHits += 1;
        }
        logDisk(`Reading ${fileNameForLogs(filePath)}`);
        const fileContents = await fs.readFile(filePath, 'utf8');
        const [frontMatterString, dataString] = fileContents.split('\n');
        try {
            const frontMatter = JSON.parse(frontMatterString);
            return {
                ...frontMatter,
                fromCache,
                data: frontMatter.status === 204 ? null : JSON.parse(dataString, parseDate),
            };
        }
        catch (error) {
            await fs.unlink(filePath);
            error.message += ` reading ${filePath}`;
            throw error;
        }
    }),
    clearDiskCache: async (pathParts) => {
        const possibleDirectory = join(cacheLocation, ...pathParts);
        if ((await doesFileExist(possibleDirectory)) &&
            (await fs.stat(possibleDirectory)).isDirectory()) {
            logDisk(`Deleting directory ${fileNameForLogs(possibleDirectory)}`);
            await rimraf(possibleDirectory);
        }
        else if (await doesFileExist(join(...cachePath(pathParts)))) {
            logDisk(`Deleting ${fileNameForLogs(join(...cachePath(pathParts)))}`);
            await fs.unlink(join(...cachePath(pathParts)));
        }
    },
});
