"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.APP_LOCK_REL_PATH = void 0;
exports.parseAppLockFile = parseAppLockFile;
exports.loadAppLockFile = loadAppLockFile;
exports.findLockedApp = findLockedApp;
exports.upsertLockedApp = upsertLockedApp;
exports.removeLockedApp = removeLockedApp;
exports.writeAppLockFileIfChanged = writeAppLockFileIfChanged;
const promises_1 = __importDefault(require("node:fs/promises"));
const node_path_1 = __importDefault(require("node:path"));
const node_util_1 = require("node:util");
const yaml_1 = __importDefault(require("yaml"));
exports.APP_LOCK_REL_PATH = node_path_1.default.join('.minds', 'app-lock.yaml');
function isRecord(v) {
    return typeof v === 'object' && v !== null && !Array.isArray(v);
}
function asString(v) {
    return typeof v === 'string' ? v : null;
}
function asNullableString(v) {
    if (v === null)
        return null;
    return typeof v === 'string' ? v : null;
}
function parseEntry(v, at) {
    if (!isRecord(v))
        return { ok: false, errorText: `Invalid ${at}: expected object` };
    const keys = Object.keys(v);
    for (const k of keys) {
        if (k !== 'id' && k !== 'package') {
            return { ok: false, errorText: `Invalid ${at}: unknown key '${k}'` };
        }
    }
    const id = asString(v['id']);
    if (!id || id.trim() === '')
        return { ok: false, errorText: `Invalid ${at}.id: required` };
    const pkg = v['package'];
    if (!isRecord(pkg)) {
        return { ok: false, errorText: `Invalid ${at}.package: expected object` };
    }
    const pkgKeys = Object.keys(pkg);
    for (const k of pkgKeys) {
        if (k !== 'name' && k !== 'version') {
            return { ok: false, errorText: `Invalid ${at}.package: unknown key '${k}'` };
        }
    }
    const name = asString(pkg['name']);
    if (!name || name.trim() === '') {
        return { ok: false, errorText: `Invalid ${at}.package.name: required` };
    }
    const versionRaw = pkg['version'] ?? null;
    const version = asNullableString(versionRaw);
    if (version === null) {
        if (versionRaw !== null) {
            return { ok: false, errorText: `Invalid ${at}.package.version: expected string|null` };
        }
    }
    else if (version.trim() === '') {
        return { ok: false, errorText: `Invalid ${at}.package.version: must not be empty` };
    }
    return {
        ok: true,
        entry: {
            id: id.trim(),
            package: { name: name.trim(), version },
        },
    };
}
function canonicalizeLockFile(file) {
    const byId = new Map();
    for (const entry of file.apps) {
        if (byId.has(entry.id)) {
            throw new Error(`Invalid ${exports.APP_LOCK_REL_PATH}: duplicate app id '${entry.id}'`);
        }
        byId.set(entry.id, entry);
    }
    return {
        schemaVersion: 1,
        apps: [...byId.values()].sort((a, b) => a.id.localeCompare(b.id)),
    };
}
function parseAppLockFile(parsed, filePathAbs) {
    if (!isRecord(parsed)) {
        return { ok: false, errorText: `Invalid app-lock.yaml: expected object (${filePathAbs})` };
    }
    const keys = Object.keys(parsed);
    for (const k of keys) {
        if (k !== 'schemaVersion' && k !== 'apps') {
            return { ok: false, errorText: `Invalid app-lock.yaml: unknown key '${k}' (${filePathAbs})` };
        }
    }
    const schemaVersion = parsed['schemaVersion'];
    if (schemaVersion !== 1) {
        return {
            ok: false,
            errorText: `Unsupported app-lock.yaml schemaVersion: ${String(schemaVersion)} (${filePathAbs})`,
        };
    }
    const appsRaw = parsed['apps'];
    if (!Array.isArray(appsRaw)) {
        return {
            ok: false,
            errorText: `Invalid app-lock.yaml: apps must be an array (${filePathAbs})`,
        };
    }
    const apps = [];
    for (let i = 0; i < appsRaw.length; i += 1) {
        const parsedEntry = parseEntry(appsRaw[i], `apps[${i}]`);
        if (!parsedEntry.ok)
            return { ok: false, errorText: `${parsedEntry.errorText} (${filePathAbs})` };
        apps.push(parsedEntry.entry);
    }
    try {
        return { ok: true, file: canonicalizeLockFile({ schemaVersion: 1, apps }) };
    }
    catch (err) {
        return {
            ok: false,
            errorText: `${err instanceof Error ? err.message : String(err)} (${filePathAbs})`,
        };
    }
}
async function loadAppLockFile(params) {
    const filePathAbs = node_path_1.default.resolve(params.rtwsRootAbs, exports.APP_LOCK_REL_PATH);
    let raw;
    try {
        raw = await promises_1.default.readFile(filePathAbs, 'utf-8');
    }
    catch (err) {
        const isEnoent = typeof err === 'object' &&
            err !== null &&
            'code' in err &&
            err.code === 'ENOENT';
        if (isEnoent) {
            return { kind: 'ok', filePathAbs, exists: false, file: { schemaVersion: 1, apps: [] } };
        }
        return {
            kind: 'error',
            filePathAbs,
            errorText: err instanceof Error ? err.message : String(err),
        };
    }
    let parsed;
    try {
        parsed = yaml_1.default.parse(raw);
    }
    catch (err) {
        return {
            kind: 'error',
            filePathAbs,
            errorText: `Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`,
        };
    }
    const lockParsed = parseAppLockFile(parsed, filePathAbs);
    if (!lockParsed.ok)
        return { kind: 'error', filePathAbs, errorText: lockParsed.errorText };
    return { kind: 'ok', filePathAbs, exists: true, file: lockParsed.file };
}
function findLockedApp(file, appId) {
    return file.apps.find((entry) => entry.id === appId) ?? null;
}
function upsertLockedApp(params) {
    const existingEntry = findLockedApp(params.existing, params.next.id);
    if (existingEntry && (0, node_util_1.isDeepStrictEqual)(existingEntry, params.next))
        return params.existing;
    const apps = [...params.existing.apps];
    const idx = apps.findIndex((entry) => entry.id === params.next.id);
    if (idx >= 0)
        apps[idx] = params.next;
    else
        apps.push(params.next);
    return canonicalizeLockFile({ schemaVersion: 1, apps });
}
function removeLockedApp(params) {
    if (!params.existing.apps.some((entry) => entry.id === params.appId))
        return params.existing;
    return canonicalizeLockFile({
        schemaVersion: 1,
        apps: params.existing.apps.filter((entry) => entry.id !== params.appId),
    });
}
async function writeAppLockFileIfChanged(params) {
    const canonical = canonicalizeLockFile(params.file);
    const loaded = await loadAppLockFile({ rtwsRootAbs: params.rtwsRootAbs });
    if (loaded.kind === 'error') {
        throw new Error(`Failed to read ${exports.APP_LOCK_REL_PATH}: ${loaded.errorText}`);
    }
    if (loaded.exists) {
        if ((0, node_util_1.isDeepStrictEqual)(canonicalizeLockFile(loaded.file), canonical))
            return;
    }
    else if (canonical.apps.length === 0) {
        return;
    }
    const mindsDirAbs = node_path_1.default.resolve(params.rtwsRootAbs, '.minds');
    await promises_1.default.mkdir(mindsDirAbs, { recursive: true });
    const filePathAbs = node_path_1.default.resolve(params.rtwsRootAbs, exports.APP_LOCK_REL_PATH);
    const yamlText = yaml_1.default.stringify(canonical);
    await promises_1.default.writeFile(filePathAbs, yamlText, 'utf-8');
}
