"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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileHelper = void 0;
const TOML = __importStar(require("@iarna/toml"));
const fast_xml_parser_1 = require("fast-xml-parser");
const INI = __importStar(require("ini"));
const fs = __importStar(require("node:fs/promises"));
const YAML = __importStar(require("yaml"));
const zod_1 = require("zod");
const util_1 = require("../../../base/lib/util");
const Watchable_1 = require("../../../base/lib/util/Watchable");
const previousPath = /(.+?)\/([^/]*)$/;
const exists = (path) => fs.access(path).then(() => true, () => false);
async function onCreated(path) {
    if (path === '/')
        return;
    if (!path.startsWith('/'))
        path = `${process.cwd()}/${path}`;
    if (await exists(path)) {
        return;
    }
    const split = path.split('/');
    const filename = split.pop();
    const parent = split.join('/');
    await onCreated(parent);
    const ctrl = new AbortController();
    const watch = fs.watch(parent, { persistent: false, signal: ctrl.signal });
    if (await exists(path)) {
        ctrl.abort();
        return;
    }
    if (await fs.access(path).then(() => true, () => false)) {
        ctrl.abort();
        return;
    }
    for await (let event of watch) {
        if (event.filename === filename) {
            ctrl.abort('finished');
            return;
        }
    }
}
function fileMerge(...args) {
    let res = args.shift();
    for (const arg of args) {
        if (res === arg)
            continue;
        else if (res &&
            arg &&
            typeof res === 'object' &&
            typeof arg === 'object' &&
            !Array.isArray(res) &&
            !Array.isArray(arg)) {
            for (const key of Object.keys(arg)) {
                res[key] = fileMerge(res[key], arg[key]);
            }
        }
        else
            res = arg;
    }
    return res;
}
function filterUndefined(a) {
    if (a && typeof a === 'object') {
        if (Array.isArray(a)) {
            return a.map(filterUndefined);
        }
        return Object.entries(a).reduce((acc, [k, v]) => {
            if (v !== undefined) {
                acc[k] = filterUndefined(v);
            }
            return acc;
        }, {});
    }
    return a;
}
function toPath(path) {
    if (typeof path === 'string') {
        return path;
    }
    return path.base.subpath(path.subpath);
}
/**
 * @description Use this class to read/write an underlying configuration file belonging to the upstream service.
 *
 *   These type definitions should reflect the underlying file as closely as possible. For example, if the service does not require a particular value, it should be marked as optional(), even if your package requires it.
 *
 *   It is recommended to use onMismatch() whenever possible. This provides an escape hatch in case the user edits the file manually and accidentally sets a value to an unsupported type.
 *
 *   Officially supported file types are json, yaml, and toml. Other files types can use "raw"
 *
 *   Choose between officially supported file formats (), or a custom format (raw).
 *
 * @example
 * Below are a few examples
 *
 * ```
 * import { matches, FileHelper } from '@start9labs/start-sdk'
 * const { arrayOf, boolean, literal, literals, object, natural, string } = matches
 *
 * export const jsonFile = FileHelper.json('./inputSpec.json', object({
 *   passwords: arrayOf(string).onMismatch([])
 *   type: literals('private', 'public').optional().onMismatch(undefined)
 * }))
 *
 * export const tomlFile = FileHelper.toml('./inputSpec.toml', object({
 *   url: literal('https://start9.com').onMismatch('https://start9.com')
 *   public: boolean.onMismatch(true)
 * }))
 *
 * export const yamlFile = FileHelper.yaml('./inputSpec.yml', object({
 *   name: string.optional().onMismatch(undefined)
 *   age: natural.optional().onMismatch(undefined)
 * }))
 *
 * export const bitcoinConfFile = FileHelper.raw(
 *   './service.conf',
 *   (obj: CustomType) => customConvertObjToFormattedString(obj),
 *   (str) => customParseStringToTypedObj(str),
 * )
 * ```
 */
class FileHelper {
    constructor(path, writeData, readData, validate) {
        this.path = path;
        this.writeData = writeData;
        this.readData = readData;
        this.validate = validate;
        this.consts = [];
    }
    async writeFileRaw(data) {
        const parent = previousPath.exec(this.path);
        if (parent) {
            await fs.mkdir(parent[1], { recursive: true });
        }
        await fs.writeFile(this.path, data);
        return null;
    }
    /**
     * Accepts structured data and overwrites the existing file on disk.
     */
    async writeFile(data) {
        return await this.writeFileRaw(this.writeData(data));
    }
    async readFileRaw() {
        if (!(await exists(this.path))) {
            return null;
        }
        return await fs.readFile(this.path).then((data) => data.toString('utf-8'));
    }
    async readFile() {
        const raw = await this.readFileRaw();
        if (raw === null) {
            return raw;
        }
        return this.readData(raw);
    }
    /**
     * Reads the file from disk and converts it to structured data.
     */
    async readOnce(map) {
        const data = await this.readFile();
        if (!data)
            return null;
        return map(this.validate(data));
    }
    createFileWatchable(effects, map, eq) {
        const doRead = async () => {
            const data = await this.readFile();
            if (!data)
                return null;
            return this.validate(data);
        };
        const filePath = this.path;
        const fileHelper = this;
        const wrappedMap = (raw) => {
            if (raw === null)
                return null;
            return map(raw);
        };
        return new (class extends Watchable_1.Watchable {
            constructor() {
                super(...arguments);
                this.label = 'FileHelper';
            }
            async fetch() {
                return doRead();
            }
            async *produce(abort) {
                while (this.effects.isInContext && !abort.aborted) {
                    if (await exists(filePath)) {
                        const ctrl = new AbortController();
                        abort.addEventListener('abort', () => ctrl.abort());
                        const watch = fs.watch(filePath, {
                            persistent: false,
                            signal: ctrl.signal,
                        });
                        yield await doRead();
                        await Promise.resolve()
                            .then(async () => {
                            for await (const _ of watch) {
                                ctrl.abort();
                                return null;
                            }
                        })
                            .catch((e) => console.error((0, util_1.asError)(e)));
                    }
                    else {
                        yield null;
                        await onCreated(filePath).catch((e) => console.error((0, util_1.asError)(e)));
                    }
                }
            }
            onConstRegistered(value) {
                if (!this.effects.constRetry)
                    return;
                const record = [
                    this.effects.constRetry,
                    value,
                    wrappedMap,
                    eq,
                ];
                fileHelper.consts.push(record);
                return () => {
                    fileHelper.consts = fileHelper.consts.filter((r) => r !== record);
                };
            }
        })(effects, { map: wrappedMap, eq });
    }
    read(map, eq) {
        map = map ?? ((a) => a);
        eq = eq ?? util_1.deepEqual;
        return {
            once: () => this.readOnce(map),
            const: (effects) => this.createFileWatchable(effects, map, eq).const(),
            watch: (effects, abort) => this.createFileWatchable(effects, map, eq).watch(abort),
            onChange: (effects, callback) => this.createFileWatchable(effects, map, eq).onChange(callback),
            waitFor: (effects, pred) => this.createFileWatchable(effects, map, eq).waitFor(pred),
        };
    }
    /**
     * Accepts full structured data and overwrites the existing file on disk if it exists.
     */
    async write(effects, data, options = {}) {
        const newData = this.validate(data);
        await this.writeFile(newData);
        if (!options.allowWriteAfterConst && effects.constRetry) {
            const records = this.consts.filter(([c]) => c === effects.constRetry);
            for (const record of records) {
                const [_, prev, map, eq] = record;
                if (!eq(prev, map(newData))) {
                    throw new Error(`Canceled: write after const: ${this.path}`);
                }
            }
        }
        return null;
    }
    /**
     * Accepts partial structured data and performs a merge with the existing file on disk.
     */
    async merge(effects, data, options = {}) {
        const fileDataRaw = await this.readFileRaw();
        let fileData = fileDataRaw === null ? null : this.readData(fileDataRaw);
        try {
            fileData = this.validate(fileData);
        }
        catch (_) { }
        const mergeData = this.validate(fileMerge({}, fileData, data));
        const toWrite = this.writeData(mergeData);
        if (toWrite !== fileDataRaw) {
            await this.writeFile(mergeData);
            if (!options.allowWriteAfterConst && effects.constRetry) {
                const records = this.consts.filter(([c]) => c === effects.constRetry);
                for (const record of records) {
                    const [_, prev, map, eq] = record;
                    if (!eq(prev, map(mergeData))) {
                        throw new Error(`Canceled: write after const: ${this.path}`);
                    }
                }
            }
        }
        return null;
    }
    /**
     * We wanted to be able to have a fileHelper, and just modify the path later in time.
     * Like one behavior of another dependency or something similar.
     */
    withPath(path) {
        return new FileHelper(toPath(path), this.writeData, this.readData, this.validate);
    }
    /**
     * Create a File Helper for an arbitrary file type.
     *
     * Provide custom functions for translating data to/from the file format.
     */
    static raw(path, toFile, fromFile, validate) {
        return new FileHelper(toPath(path), toFile, fromFile, validate);
    }
    static rawTransformed(path, toFile, fromFile, validate, transformers) {
        return FileHelper.raw(path, (inData) => {
            if (transformers) {
                return toFile(transformers.onWrite(inData));
            }
            return toFile(inData);
        }, (fileData) => {
            if (transformers) {
                return transformers.onRead(fromFile(fileData));
            }
            return fromFile(fileData);
        }, validate);
    }
    static string(path, shape, transformers) {
        return FileHelper.rawTransformed(path, (inData) => inData, (inString) => inString, (data) => (shape || zod_1.z.string()).parse(data), transformers);
    }
    static json(path, shape, transformers) {
        return FileHelper.rawTransformed(path, (inData) => JSON.stringify(inData, null, 2), (inString) => JSON.parse(inString), (data) => shape.parse(data), transformers);
    }
    static yaml(path, shape, transformers) {
        return FileHelper.rawTransformed(path, (inData) => YAML.stringify(inData, null, 2), (inString) => YAML.parse(inString), (data) => shape.parse(data), transformers);
    }
    static toml(path, shape, transformers) {
        return FileHelper.rawTransformed(path, (inData) => TOML.stringify(inData), (inString) => TOML.parse(inString), (data) => shape.parse(data), transformers);
    }
    static ini(path, shape, options, transformers) {
        return FileHelper.rawTransformed(path, (inData) => INI.stringify(filterUndefined(inData), options), (inString) => INI.parse(inString, options), (data) => shape.parse(data), transformers);
    }
    static env(path, shape, transformers) {
        return FileHelper.rawTransformed(path, (inData) => Object.entries(inData)
            .map(([k, v]) => `${k}=${v}`)
            .join('\n'), (inString) => Object.fromEntries(inString
            .split('\n')
            .map((line) => line.trim())
            .filter((line) => !line.startsWith('#') && line.includes('='))
            .map((line) => {
            const pos = line.indexOf('=');
            return [line.slice(0, pos), line.slice(pos + 1)];
        })), (data) => shape.parse(data), transformers);
    }
    static xml(path, shape, options, transformers) {
        const parser = new fast_xml_parser_1.XMLParser(options?.parser);
        const builder = new fast_xml_parser_1.XMLBuilder(options?.builder);
        return FileHelper.rawTransformed(path, (inData) => builder.build(inData), (inString) => parser.parse(inString), (data) => shape.parse(data), transformers);
    }
}
exports.FileHelper = FileHelper;
exports.default = FileHelper;
//# sourceMappingURL=fileHelper.js.map