import path from "node:path";
import * as nodeFs from "node:fs/promises";
import { hasOwnErrorCode } from "../errors.js";
import { assertPathHasNoSymbolicLinks } from "../path-safety.js";
function isNotFoundError(error) {
    return hasOwnErrorCode(error, "ENOENT");
}
function getCurrentLogPath(logDir, stream) {
    return path.join(logDir, `${stream}.log`);
}
function getRotatedLogPath(logDir, stream, index) {
    return path.join(logDir, `${stream}.${index}.log`);
}
async function isFile(fs, filePath) {
    try {
        await assertPathHasNoSymbolicLinks(fs, filePath);
        return (await fs.stat(filePath)).isFile();
    }
    catch (error) {
        if (isNotFoundError(error)) {
            return false;
        }
        throw error;
    }
}
async function removeIfExists(fs, filePath) {
    if (!(await isFile(fs, filePath))) {
        return;
    }
    await fs.rm(filePath, { force: true });
}
async function moveIfExists(fs, sourcePath, destinationPath) {
    if (!(await isFile(fs, sourcePath))) {
        return;
    }
    await assertPathHasNoSymbolicLinks(fs, destinationPath);
    await fs.rename(sourcePath, destinationPath);
}
function getRotatedLogIndex(fileName, stream) {
    const prefix = `${stream}.`;
    const suffix = ".log";
    if (!fileName.startsWith(prefix) || !fileName.endsWith(suffix)) {
        return null;
    }
    const rawIndex = fileName.slice(prefix.length, -suffix.length);
    const index = Number.parseInt(rawIndex, 10);
    if (!Number.isInteger(index) || index < 1 || `${index}` !== rawIndex) {
        return null;
    }
    return index;
}
async function removeAllStreamLogs(fs, logDir, stream) {
    await removeIfExists(fs, getCurrentLogPath(logDir, stream));
    try {
        const fileNames = await fs.readdir(logDir);
        for (const fileName of fileNames) {
            if (getRotatedLogIndex(fileName, stream) === null) {
                continue;
            }
            await fs.rm(path.join(logDir, fileName), { force: true });
        }
    }
    catch (error) {
        if (isNotFoundError(error)) {
            return;
        }
        throw error;
    }
}
function getLines(content) {
    if (content.length === 0) {
        return [];
    }
    const lines = content.split("\n");
    if (content.endsWith("\n")) {
        lines.pop();
    }
    return lines;
}
export function createLogWriter(logDir, retainCount, fs = nodeFs) {
    if (!Number.isFinite(retainCount) || !Number.isInteger(retainCount) || retainCount < 0) {
        throw new Error("retainCount must be a finite non-negative integer");
    }
    const maxRetainedRuns = retainCount;
    let operationQueue = Promise.resolve();
    function enqueue(operation) {
        const result = operationQueue.then(operation, operation);
        operationQueue = result.then(() => undefined, () => undefined);
        return result;
    }
    async function writeNow(line, stream) {
        await assertPathHasNoSymbolicLinks(fs, logDir);
        await fs.mkdir(logDir, { recursive: true });
        await assertPathHasNoSymbolicLinks(fs, logDir);
        const currentPath = getCurrentLogPath(logDir, stream);
        await assertPathHasNoSymbolicLinks(fs, currentPath);
        await fs.appendFile(currentPath, `${line}\n`);
    }
    function write(line, stream) {
        return enqueue(() => writeNow(line, stream));
    }
    async function rotateStream(stream) {
        const currentPath = getCurrentLogPath(logDir, stream);
        if (maxRetainedRuns === 0) {
            await removeAllStreamLogs(fs, logDir, stream);
            return;
        }
        await removeIfExists(fs, getRotatedLogPath(logDir, stream, maxRetainedRuns));
        for (let index = maxRetainedRuns - 1; index >= 1; index -= 1) {
            await moveIfExists(fs, getRotatedLogPath(logDir, stream, index), getRotatedLogPath(logDir, stream, index + 1));
        }
        await moveIfExists(fs, currentPath, getRotatedLogPath(logDir, stream, 1));
    }
    async function rotateNow() {
        await assertPathHasNoSymbolicLinks(fs, logDir);
        const priorLogs = await captureLogs(fs, logDir);
        try {
            await rotateStream("stdout");
            await rotateStream("stderr");
        }
        catch (error) {
            await restoreLogs(fs, logDir, priorLogs);
            throw error;
        }
    }
    function rotate() {
        return enqueue(() => rotateNow());
    }
    async function tailNow(stream, lines = 50) {
        if (!Number.isFinite(lines) || !Number.isInteger(lines) || lines < 0) {
            throw new Error("lines must be a finite non-negative integer");
        }
        try {
            await assertPathHasNoSymbolicLinks(fs, logDir);
            const currentPath = getCurrentLogPath(logDir, stream);
            await assertPathHasNoSymbolicLinks(fs, currentPath);
            const content = await fs.readFile(currentPath, "utf8");
            const allLines = getLines(content);
            if (lines === 0) {
                return [];
            }
            return allLines.slice(-lines);
        }
        catch (error) {
            if (isNotFoundError(error)) {
                return [];
            }
            throw error;
        }
    }
    function tail(stream, lines = 50) {
        return enqueue(() => tailNow(stream, lines));
    }
    function close() { }
    return { write, rotate, tail, close };
}
async function captureLogs(fs, logDir) {
    const snapshot = new Map();
    let fileNames;
    try {
        fileNames = await fs.readdir(logDir);
    }
    catch (error) {
        if (isNotFoundError(error)) {
            return snapshot;
        }
        throw error;
    }
    for (const fileName of fileNames) {
        if (fileName !== "stdout.log" &&
            fileName !== "stderr.log" &&
            getRotatedLogIndex(fileName, "stdout") === null &&
            getRotatedLogIndex(fileName, "stderr") === null) {
            continue;
        }
        const filePath = path.join(logDir, fileName);
        await assertPathHasNoSymbolicLinks(fs, filePath);
        snapshot.set(filePath, await fs.readFile(filePath, "utf8"));
    }
    return snapshot;
}
async function restoreLogs(fs, logDir, snapshot) {
    let fileNames = [];
    try {
        fileNames = await fs.readdir(logDir);
    }
    catch (error) {
        if (!isNotFoundError(error)) {
            throw error;
        }
    }
    for (const fileName of fileNames) {
        const filePath = path.join(logDir, fileName);
        const managed = fileName === "stdout.log" ||
            fileName === "stderr.log" ||
            getRotatedLogIndex(fileName, "stdout") !== null ||
            getRotatedLogIndex(fileName, "stderr") !== null;
        if (managed && !snapshot.has(filePath)) {
            await assertPathHasNoSymbolicLinks(fs, filePath);
            await fs.rm(filePath, { force: true });
        }
    }
    for (const [filePath, content] of snapshot) {
        await assertPathHasNoSymbolicLinks(fs, filePath);
        await fs.writeFile(filePath, content);
    }
}
