"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.APPS_RESOLUTION_REL_PATH = void 0;
exports.loadAppsResolutionFile = loadAppsResolutionFile;
exports.writeAppsResolutionFile = writeAppsResolutionFile;
exports.writeAppsResolutionFileIfChanged = writeAppsResolutionFileIfChanged;
exports.findResolvedApp = findResolvedApp;
exports.upsertResolvedApp = upsertResolvedApp;
exports.removeResolvedApp = removeResolvedApp;
exports.setResolvedAppEnabled = setResolvedAppEnabled;
exports.setResolvedAppAssignedPort = setResolvedAppAssignedPort;
const promises_1 = __importDefault(require("node:fs/promises"));
const node_path_1 = __importDefault(require("node:path"));
const yaml_1 = __importDefault(require("yaml"));
const app_json_1 = require("@longrun-ai/kernel/app-json");
exports.APPS_RESOLUTION_REL_PATH = node_path_1.default.join('.apps', 'resolution.yaml');
function isRecord(v) {
    return typeof v === 'object' && v !== null && !Array.isArray(v);
}
function asString(v) {
    return typeof v === 'string' ? v : null;
}
function asBool(v) {
    return typeof v === 'boolean' ? v : null;
}
function asNullableNumber(v) {
    if (v === null)
        return null;
    if (typeof v === 'number' && Number.isFinite(v))
        return v;
    return null;
}
function parseSource(v, at) {
    if (!isRecord(v))
        return { ok: false, errorText: `Invalid ${at}: expected object` };
    const kind = asString(v['kind']);
    if (kind !== 'npx' && kind !== 'local') {
        return { ok: false, errorText: `Invalid ${at}.kind: expected 'npx'|'local'` };
    }
    if (kind === 'npx') {
        const spec = asString(v['spec']);
        if (!spec || spec.trim() === '') {
            return { ok: false, errorText: `Invalid ${at}.spec: required` };
        }
        return { ok: true, source: { kind, spec } };
    }
    const pathAbs = asString(v['pathAbs']);
    if (!pathAbs || pathAbs.trim() === '') {
        return { ok: false, errorText: `Invalid ${at}.pathAbs: required` };
    }
    return { ok: true, source: { kind, pathAbs } };
}
function parseEntry(v, at) {
    if (!isRecord(v))
        return { ok: false, errorText: `Invalid ${at}: expected object` };
    const id = asString(v['id']);
    if (!id || id.trim() === '')
        return { ok: false, errorText: `Invalid ${at}.id: required` };
    const enabled = asBool(v['enabled']);
    if (enabled === null)
        return { ok: false, errorText: `Invalid ${at}.enabled: boolean required` };
    const assignedPortRaw = v['assignedPort'] ?? null;
    const assignedPort = asNullableNumber(assignedPortRaw);
    if (assignedPort === null) {
        if (assignedPortRaw !== null) {
            return { ok: false, errorText: `Invalid ${at}.assignedPort: expected number|null` };
        }
    }
    else if (!Number.isInteger(assignedPort) || assignedPort <= 0) {
        return { ok: false, errorText: `Invalid ${at}.assignedPort: expected positive integer|null` };
    }
    const sourceParsed = parseSource(v['source'], `${at}.source`);
    if (!sourceParsed.ok)
        return sourceParsed;
    const installJsonParsed = (0, app_json_1.parseDomindsAppInstallJson)(v['installJson']);
    if (!installJsonParsed.ok) {
        return { ok: false, errorText: `Invalid ${at}.installJson: ${installJsonParsed.errorText}` };
    }
    return {
        ok: true,
        entry: {
            id: id.trim(),
            enabled,
            source: sourceParsed.source,
            assignedPort,
            installJson: installJsonParsed.json,
        },
    };
}
function canonicalizeResolutionFile(file) {
    const byId = new Map();
    for (const entry of file.apps) {
        if (byId.has(entry.id)) {
            throw new Error(`Invalid ${exports.APPS_RESOLUTION_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)),
    };
}
async function loadAppsResolutionFile(params) {
    const filePathAbs = node_path_1.default.resolve(params.rtwsRootAbs, exports.APPS_RESOLUTION_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)}`,
        };
    }
    if (!isRecord(parsed)) {
        return { kind: 'error', filePathAbs, errorText: 'Invalid resolution.yaml: expected object' };
    }
    const keys = Object.keys(parsed);
    for (const key of keys) {
        if (key !== 'schemaVersion' && key !== 'apps') {
            return {
                kind: 'error',
                filePathAbs,
                errorText: `Invalid resolution.yaml: unknown key '${key}'`,
            };
        }
    }
    const schemaVersion = parsed['schemaVersion'];
    if (schemaVersion !== 1) {
        return {
            kind: 'error',
            filePathAbs,
            errorText: `Unsupported schemaVersion: ${String(schemaVersion)}`,
        };
    }
    const appsRaw = parsed['apps'];
    if (!Array.isArray(appsRaw)) {
        return {
            kind: 'error',
            filePathAbs,
            errorText: 'Invalid resolution.yaml: apps must be an array',
        };
    }
    const apps = [];
    for (let i = 0; i < appsRaw.length; i += 1) {
        const entryParsed = parseEntry(appsRaw[i], `apps[${i}]`);
        if (!entryParsed.ok)
            return { kind: 'error', filePathAbs, errorText: entryParsed.errorText };
        apps.push(entryParsed.entry);
    }
    try {
        return {
            kind: 'ok',
            filePathAbs,
            exists: true,
            file: canonicalizeResolutionFile({ schemaVersion: 1, apps }),
        };
    }
    catch (err) {
        return {
            kind: 'error',
            filePathAbs,
            errorText: err instanceof Error ? err.message : String(err),
        };
    }
}
async function writeAppsResolutionFile(params) {
    const dirAbs = node_path_1.default.resolve(params.rtwsRootAbs, '.apps');
    await promises_1.default.mkdir(dirAbs, { recursive: true });
    const filePathAbs = node_path_1.default.resolve(params.rtwsRootAbs, exports.APPS_RESOLUTION_REL_PATH);
    const yamlText = yaml_1.default.stringify(canonicalizeResolutionFile(params.file));
    await promises_1.default.writeFile(filePathAbs, yamlText, 'utf-8');
}
async function writeAppsResolutionFileIfChanged(params) {
    const dirAbs = node_path_1.default.resolve(params.rtwsRootAbs, '.apps');
    await promises_1.default.mkdir(dirAbs, { recursive: true });
    const filePathAbs = node_path_1.default.resolve(params.rtwsRootAbs, exports.APPS_RESOLUTION_REL_PATH);
    const yamlText = yaml_1.default.stringify(canonicalizeResolutionFile(params.file));
    try {
        const prev = await promises_1.default.readFile(filePathAbs, 'utf-8');
        if (prev === yamlText)
            return;
    }
    catch (err) {
        const isEnoent = typeof err === 'object' &&
            err !== null &&
            'code' in err &&
            err.code === 'ENOENT';
        if (!isEnoent)
            throw err;
    }
    await promises_1.default.writeFile(filePathAbs, yamlText, 'utf-8');
}
function findResolvedApp(file, appId) {
    return file.apps.find((app) => app.id === appId) ?? null;
}
function upsertResolvedApp(params) {
    const apps = [...params.existing.apps];
    const idx = apps.findIndex((app) => app.id === params.next.id);
    if (idx >= 0)
        apps[idx] = params.next;
    else
        apps.push(params.next);
    return canonicalizeResolutionFile({ schemaVersion: 1, apps });
}
function removeResolvedApp(params) {
    if (!params.existing.apps.some((app) => app.id === params.appId))
        return params.existing;
    return canonicalizeResolutionFile({
        schemaVersion: 1,
        apps: params.existing.apps.filter((app) => app.id !== params.appId),
    });
}
function setResolvedAppEnabled(params) {
    const existingApp = findResolvedApp(params.existing, params.appId);
    if (!existingApp || existingApp.enabled === params.enabled)
        return params.existing;
    return canonicalizeResolutionFile({
        schemaVersion: 1,
        apps: params.existing.apps.map((app) => app.id === params.appId ? { ...app, enabled: params.enabled } : app),
    });
}
function setResolvedAppAssignedPort(params) {
    if (params.assignedPort !== null) {
        if (!Number.isInteger(params.assignedPort) || params.assignedPort <= 0) {
            throw new Error(`Invalid assignedPort: expected positive integer|null (got ${params.assignedPort})`);
        }
    }
    const existingApp = findResolvedApp(params.existing, params.appId);
    if (!existingApp || existingApp.assignedPort === params.assignedPort)
        return params.existing;
    return canonicalizeResolutionFile({
        schemaVersion: 1,
        apps: params.existing.apps.map((app) => app.id === params.appId ? { ...app, assignedPort: params.assignedPort } : app),
    });
}
