"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeTomlFileAtomic = exports.probeTomlObjectFile = exports.stringifyTomlObject = exports.parseTomlObjectText = exports.TomlFileConflictError = exports.TomlFileValidationError = void 0;
const fs_1 = require("fs");
const path = __importStar(require("path"));
const smol_toml_1 = require("smol-toml");
const toml_object_1 = require("../../shared/toml-object");
class TomlFileValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'TomlFileValidationError';
    }
}
exports.TomlFileValidationError = TomlFileValidationError;
class TomlFileConflictError extends Error {
    constructor(message, mtime) {
        super(message);
        this.code = 'CONFLICT';
        this.name = 'TomlFileConflictError';
        this.mtime = mtime;
    }
}
exports.TomlFileConflictError = TomlFileConflictError;
function isObject(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
async function statPath(filePath) {
    try {
        return await fs_1.promises.lstat(filePath);
    }
    catch (error) {
        if (error.code === 'ENOENT') {
            return null;
        }
        throw error;
    }
}
async function resolveConflictMtime(filePath) {
    const stat = await statPath(filePath);
    return stat?.mtimeMs ?? Date.now();
}
async function acquireWriteLock(lockPath, targetPath, fileLabel) {
    let handle = null;
    try {
        handle = await fs_1.promises.open(lockPath, 'wx', 0o600);
    }
    catch (error) {
        const code = error.code;
        if (code === 'EEXIST') {
            const existingLock = await statPath(lockPath);
            if (existingLock?.isSymbolicLink()) {
                throw new Error(`Refusing to write: ${fileLabel}.lock is a symlink.`);
            }
            if (existingLock && !existingLock.isFile()) {
                throw new Error(`Refusing to write: ${fileLabel}.lock is not a regular file.`);
            }
            throw new TomlFileConflictError('File is currently being written by another request. Refresh and retry.', await resolveConflictMtime(targetPath));
        }
        throw error;
    }
    return async () => {
        if (!handle)
            return;
        try {
            await handle.close();
        }
        finally {
            try {
                await fs_1.promises.unlink(lockPath);
            }
            catch (error) {
                if (error.code !== 'ENOENT') {
                    throw error;
                }
            }
        }
    };
}
function ensureWritableTarget(targetStat, fileLabel) {
    if (!targetStat)
        return null;
    if (targetStat.isSymbolicLink()) {
        throw new Error(`Refusing to write: ${fileLabel} is a symlink.`);
    }
    if (!targetStat.isFile()) {
        throw new Error(`Refusing to write: ${fileLabel} is not a regular file.`);
    }
    return targetStat;
}
function assertExpectedMtime(targetStat, expectedMtime) {
    if (!targetStat) {
        if (expectedMtime !== undefined) {
            throw new TomlFileConflictError('File modified externally.', Date.now());
        }
        return;
    }
    if (typeof expectedMtime !== 'number' || !Number.isFinite(expectedMtime)) {
        throw new TomlFileConflictError('File metadata not loaded. Refresh and retry.', targetStat.mtimeMs);
    }
    if (targetStat.mtimeMs !== expectedMtime) {
        throw new TomlFileConflictError('File modified externally.', targetStat.mtimeMs);
    }
}
async function verifyTargetUnchanged(targetPath, initialTargetStat, fileLabel) {
    const currentTargetStat = ensureWritableTarget(await statPath(targetPath), fileLabel);
    if (!initialTargetStat) {
        if (currentTargetStat) {
            throw new TomlFileConflictError('File modified externally.', currentTargetStat.mtimeMs);
        }
        return;
    }
    if (!currentTargetStat || currentTargetStat.mtimeMs !== initialTargetStat.mtimeMs) {
        throw new TomlFileConflictError('File modified externally.', currentTargetStat?.mtimeMs ?? Date.now());
    }
}
function parseTomlObjectText(rawText, fieldName = 'rawText') {
    if (typeof rawText !== 'string') {
        throw new TomlFileValidationError(`${fieldName} must be a string.`);
    }
    try {
        return (0, toml_object_1.parseTomlObject)(rawText);
    }
    catch (error) {
        const message = error.message;
        if (message === 'TOML root must be a table.') {
            throw new TomlFileValidationError(`${fieldName} TOML root must be a table.`);
        }
        throw new TomlFileValidationError(`Invalid TOML in ${fieldName}: ${message}`);
    }
}
exports.parseTomlObjectText = parseTomlObjectText;
function stringifyTomlObject(config) {
    if (!isObject(config)) {
        throw new TomlFileValidationError('config TOML root must be a table.');
    }
    const text = (0, smol_toml_1.stringify)(config).trimEnd();
    return text ? `${text}\n` : '';
}
exports.stringifyTomlObject = stringifyTomlObject;
async function probeTomlObjectFile(filePath, label, displayPath) {
    const stat = await statPath(filePath);
    if (!stat) {
        return {
            diagnostics: {
                label,
                path: displayPath,
                resolvedPath: filePath,
                exists: false,
                isSymlink: false,
                isRegularFile: false,
                sizeBytes: null,
                mtimeMs: null,
                parseError: null,
                readError: null,
            },
            config: null,
            rawText: '',
        };
    }
    const diagnostics = {
        label,
        path: displayPath,
        resolvedPath: filePath,
        exists: true,
        isSymlink: stat.isSymbolicLink(),
        isRegularFile: stat.isFile(),
        sizeBytes: stat.size,
        mtimeMs: stat.mtimeMs,
        parseError: null,
        readError: null,
    };
    if (diagnostics.isSymlink) {
        diagnostics.readError = 'Refusing symlink file for safety.';
        return { diagnostics, config: null, rawText: '' };
    }
    if (!diagnostics.isRegularFile) {
        diagnostics.readError = 'Target is not a regular file.';
        return { diagnostics, config: null, rawText: '' };
    }
    try {
        const rawText = await fs_1.promises.readFile(filePath, 'utf8');
        try {
            const config = parseTomlObjectText(rawText, displayPath);
            return { diagnostics, config, rawText };
        }
        catch (error) {
            diagnostics.parseError = error.message;
            return { diagnostics, config: null, rawText };
        }
    }
    catch (error) {
        diagnostics.readError = error.message;
        return { diagnostics, config: null, rawText: '' };
    }
}
exports.probeTomlObjectFile = probeTomlObjectFile;
async function writeTomlFileAtomic(input) {
    const fileLabel = input.fileLabel || path.basename(input.filePath);
    parseTomlObjectText(input.rawText, fileLabel);
    const targetPath = input.filePath;
    const targetDir = path.dirname(targetPath);
    const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
    const lockPath = `${targetPath}.lock`;
    const dirMode = input.dirMode ?? 0o700;
    const fileMode = input.fileMode ?? 0o600;
    await fs_1.promises.mkdir(targetDir, { recursive: true, mode: dirMode });
    const releaseLock = await acquireWriteLock(lockPath, targetPath, fileLabel);
    let wroteTemp = false;
    try {
        const targetStat = ensureWritableTarget(await statPath(targetPath), fileLabel);
        assertExpectedMtime(targetStat, input.expectedMtime);
        await fs_1.promises.writeFile(tempPath, input.rawText, { mode: fileMode, flag: 'wx' });
        wroteTemp = true;
        const tempStat = await fs_1.promises.lstat(tempPath);
        if (tempStat.isSymbolicLink()) {
            throw new Error(`Refusing to write: ${fileLabel}.tmp is a symlink.`);
        }
        if (!tempStat.isFile()) {
            throw new Error(`Refusing to write: ${fileLabel}.tmp is not a regular file.`);
        }
        await verifyTargetUnchanged(targetPath, targetStat, fileLabel);
        await fs_1.promises.rename(tempPath, targetPath);
        wroteTemp = false;
        try {
            await fs_1.promises.chmod(targetPath, fileMode);
        }
        catch {
            // Best-effort permission hardening.
        }
        const stat = await fs_1.promises.stat(targetPath);
        return { mtime: stat.mtimeMs };
    }
    finally {
        if (wroteTemp) {
            try {
                await fs_1.promises.unlink(tempPath);
            }
            catch (error) {
                if (error.code !== 'ENOENT') {
                    throw error;
                }
            }
        }
        await releaseLock();
    }
}
exports.writeTomlFileAtomic = writeTomlFileAtomic;
//# sourceMappingURL=compatible-cli-toml-file-service.js.map