"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveAppsGraphState = resolveAppsGraphState;
exports.materializeAppsResolution = materializeAppsResolution;
exports.loadEnabledAppsSnapshot = loadEnabledAppsSnapshot;
const node_path_1 = __importDefault(require("node:path"));
const app_lock_file_1 = require("./app-lock-file");
const assigned_port_1 = require("./assigned-port");
const configuration_file_1 = require("./configuration-file");
const local_package_root_1 = require("./local-package-root");
const manifest_1 = require("./manifest");
const package_info_1 = require("./package-info");
const resolution_file_1 = require("./resolution-file");
const run_app_json_1 = require("./run-app-json");
function getRuntimePort(entry) {
    if (entry.assignedPort !== null)
        return entry.assignedPort;
    const defaultPort = entry.installJson.frontend?.defaultPort;
    return typeof defaultPort === 'number' && defaultPort > 0 ? defaultPort : null;
}
function getResolutionHint(params) {
    const filePathAbs = node_path_1.default.resolve(params.rtwsRootAbs, configuration_file_1.APPS_CONFIGURATION_REL_PATH);
    const action = params.hasConfigurationFile ? 'Edit' : 'Create';
    return (`${action} ${filePathAbs} to configure 'resolutionStrategy'. ` +
        `Default local root is 'dominds-apps' (rtws-relative) and resolves app ids under '<root>/<appId path segments>/'.`);
}
async function loadManifestDepsFromResolvedApp(params) {
    const pkgInfo = await (0, package_info_1.readPackageInfo)({ packageRootAbs: params.packageRootAbs });
    const loaded = await (0, manifest_1.loadDomindsAppManifest)({
        packageRootAbs: params.packageRootAbs,
        manifestRelPath: pkgInfo.manifestRelPath,
    });
    if (loaded.kind === 'error') {
        throw new Error(`Failed to load app manifest for '${params.appId}': ${loaded.errorText} (${loaded.filePathAbs})`);
    }
    return loaded.manifest.dependencies ?? [];
}
function makeLockedAppHint(entry) {
    return {
        packageName: entry.package.name,
        version: entry.package.version,
    };
}
function buildNpxSpec(params) {
    const packageName = params.lockedHint?.packageName ??
        params.previousResolutionEntry?.installJson.package.name ??
        params.appId;
    const version = params.lockedHint?.version ?? null;
    return version && version.trim() !== '' ? `${packageName}@${version}` : packageName;
}
async function probeAppByStrategy(params) {
    for (const item of params.strategy.order) {
        if (item === 'local') {
            const packageRootAbs = await (0, local_package_root_1.resolveLocalAppPackageRootAbs)({
                rtwsRootAbs: params.rtwsRootAbs,
                appId: params.appId,
                localRoots: params.strategy.localRoots,
                previousResolutionEntry: params.previousResolutionEntry,
            });
            if (!packageRootAbs)
                continue;
            const installJson = await (0, run_app_json_1.runDomindsAppJsonViaLocalPackage)({ packageRootAbs });
            if (installJson.appId !== params.appId) {
                throw new Error(`App id mismatch for local package '${packageRootAbs}': expected '${params.appId}', got '${installJson.appId}'`);
            }
            return { source: { kind: 'local', pathAbs: packageRootAbs }, installJson };
        }
        if (item === 'npx') {
            const spec = buildNpxSpec({
                appId: params.appId,
                lockedHint: params.lockedHint,
                previousResolutionEntry: params.previousResolutionEntry,
            });
            const installJson = await (0, run_app_json_1.runDomindsAppJsonViaNpx)({
                spec,
                cwdAbs: params.rtwsRootAbs,
            });
            if (installJson.appId !== params.appId) {
                throw new Error(`App id mismatch for npx '${spec}': expected '${params.appId}', got '${installJson.appId}'`);
            }
            return { source: { kind: 'npx', spec }, installJson };
        }
        const exhaustive = item;
        throw new Error(`Unreachable resolution strategy item: ${String(exhaustive)}`);
    }
    return null;
}
function makeResolutionEntry(params) {
    return {
        id: params.appId,
        enabled: true,
        source: params.probe.source,
        assignedPort: params.cachedAssignedPort,
        installJson: params.probe.installJson,
    };
}
async function resolveGraph(params) {
    const issues = [];
    const resolvedById = new Map();
    const depsByAppId = new Map();
    const fatalApps = new Set();
    const requiredById = new Map();
    const requiredByParents = new Map();
    const queue = [];
    const pushIssue = (issue) => {
        issues.push(issue);
    };
    const addRequiredByParent = (depId, parentId) => {
        const existing = requiredByParents.get(depId);
        if (existing) {
            existing.add(parentId);
            return;
        }
        requiredByParents.set(depId, new Set([parentId]));
    };
    const mergeRequiredFlag = (id, required) => {
        const prev = requiredById.get(id);
        if (prev === undefined) {
            requiredById.set(id, required);
            return true;
        }
        if (prev === true || required === false)
            return false;
        requiredById.set(id, true);
        return true;
    };
    const enqueue = (id, required, parentId) => {
        const changed = mergeRequiredFlag(id, required);
        if (required && parentId)
            addRequiredByParent(id, parentId);
        if (changed)
            queue.push(id);
    };
    try {
        const rootDeps = await (0, manifest_1.loadRtwsDeclaredAppDependencies)({ rtwsRootAbs: params.rtwsRootAbs });
        for (const dep of rootDeps) {
            enqueue(dep.id, dep.optional !== true, 'rtws');
        }
    }
    catch (err) {
        pushIssue({
            kind: 'app_manifest_load_failed',
            severity: 'error',
            message: 'Failed to load rtws app manifest (.minds/app.yaml); app dependencies will not be resolved.',
            detail: {
                appId: 'rtws',
                manifestRelPath: '.minds/app.yaml',
                errorText: err instanceof Error ? err.message : String(err),
            },
        });
    }
    while (queue.length > 0) {
        const appId = queue.shift();
        if (!appId)
            break;
        if (resolvedById.has(appId) || fatalApps.has(appId))
            continue;
        const required = requiredById.get(appId) ?? true;
        const explicitlyDisabled = params.configurationDisabledApps.has(appId);
        const previousResolutionEntry = params.previousResolutionById.get(appId) ?? null;
        let probe = null;
        try {
            probe = await probeAppByStrategy({
                rtwsRootAbs: params.rtwsRootAbs,
                appId,
                strategy: params.strategy,
                lockedHint: params.lockedHintsById.get(appId) ?? null,
                previousResolutionEntry,
            });
        }
        catch (err) {
            if (required) {
                const roots = params.strategy.localRoots
                    .map((root) => (node_path_1.default.isAbsolute(root) ? root : node_path_1.default.resolve(params.rtwsRootAbs, root)))
                    .join(', ');
                pushIssue({
                    kind: explicitlyDisabled ? 'required_dependency_disabled' : 'required_dependency_missing',
                    severity: 'error',
                    message: explicitlyDisabled
                        ? `Required app dependency '${appId}' is disabled.`
                        : `Required app dependency '${appId}' failed to resolve.`,
                    detail: {
                        appId,
                        searchedLocalRootsAbs: roots,
                        hint: getResolutionHint({
                            rtwsRootAbs: params.rtwsRootAbs,
                            hasConfigurationFile: params.hasConfigurationFile,
                        }),
                        errorText: err instanceof Error ? err.message : String(err),
                        requiredBy: [...(requiredByParents.get(appId) ?? new Set())].sort(),
                    },
                });
            }
            continue;
        }
        if (!probe) {
            if (required) {
                const roots = params.strategy.localRoots
                    .map((root) => (node_path_1.default.isAbsolute(root) ? root : node_path_1.default.resolve(params.rtwsRootAbs, root)))
                    .join(', ');
                pushIssue({
                    kind: explicitlyDisabled ? 'required_dependency_disabled' : 'required_dependency_missing',
                    severity: 'error',
                    message: explicitlyDisabled
                        ? `Required app dependency '${appId}' is disabled.`
                        : `Required app dependency '${appId}' is missing.`,
                    detail: {
                        appId,
                        searchedLocalRootsAbs: roots,
                        hint: getResolutionHint({
                            rtwsRootAbs: params.rtwsRootAbs,
                            hasConfigurationFile: params.hasConfigurationFile,
                        }),
                        requiredBy: [...(requiredByParents.get(appId) ?? new Set())].sort(),
                    },
                });
            }
            continue;
        }
        const entry = makeResolutionEntry({
            appId,
            probe,
            cachedAssignedPort: previousResolutionEntry?.assignedPort ?? null,
        });
        resolvedById.set(appId, entry);
        if (explicitlyDisabled) {
            if (required) {
                pushIssue({
                    kind: 'required_dependency_disabled',
                    severity: 'error',
                    message: `Required app dependency '${appId}' is disabled.`,
                    detail: {
                        appId,
                        configurationFileRelPath: configuration_file_1.APPS_CONFIGURATION_REL_PATH,
                        requiredBy: [...(requiredByParents.get(appId) ?? new Set())].sort(),
                    },
                });
            }
            continue;
        }
        let manifestDeps;
        try {
            manifestDeps = await loadManifestDepsFromResolvedApp({
                appId,
                packageRootAbs: entry.installJson.package.rootAbs,
            });
        }
        catch (err) {
            pushIssue({
                kind: 'app_manifest_load_failed',
                severity: 'error',
                message: `Failed to load app manifest for '${appId}'.`,
                detail: {
                    appId,
                    packageRootAbs: entry.installJson.package.rootAbs,
                    errorText: err instanceof Error ? err.message : String(err),
                },
            });
            fatalApps.add(appId);
            continue;
        }
        const edges = manifestDeps.map((dep) => ({
            depId: dep.id,
            required: dep.optional !== true,
        }));
        depsByAppId.set(appId, edges);
        for (const edge of edges) {
            enqueue(edge.depId, edge.required, appId);
        }
    }
    const effectiveEnabledById = new Map();
    for (const [appId] of resolvedById.entries()) {
        effectiveEnabledById.set(appId, !params.configurationDisabledApps.has(appId) && !fatalApps.has(appId));
    }
    let changed = true;
    while (changed) {
        changed = false;
        for (const [appId] of resolvedById.entries()) {
            if (!(effectiveEnabledById.get(appId) ?? false))
                continue;
            const edges = depsByAppId.get(appId) ?? [];
            for (const edge of edges) {
                if (!edge.required)
                    continue;
                const depResolved = resolvedById.get(edge.depId) ?? null;
                const depExplicitlyDisabled = params.configurationDisabledApps.has(edge.depId);
                const depEffectiveEnabled = depResolved
                    ? (effectiveEnabledById.get(edge.depId) ?? false)
                    : false;
                if (depExplicitlyDisabled || !depResolved || !depEffectiveEnabled) {
                    effectiveEnabledById.set(appId, false);
                    pushIssue({
                        kind: 'app_effectively_disabled_due_to_required_dependency',
                        severity: 'error',
                        message: `App '${appId}' is effectively disabled due to missing/disabled required dependency '${edge.depId}'.`,
                        detail: {
                            appId,
                            dependencyId: edge.depId,
                            dependencyState: depExplicitlyDisabled
                                ? 'disabled'
                                : depResolved
                                    ? 'effectively_disabled'
                                    : 'missing',
                        },
                    });
                    changed = true;
                    break;
                }
            }
        }
    }
    const resolvedEntriesForPorts = [...resolvedById.values()];
    for (const [appId, entry] of resolvedById.entries()) {
        if (!(effectiveEnabledById.get(appId) ?? false))
            continue;
        const assigned = await (0, assigned_port_1.resolveStableAssignedPortWithReason)({
            appId,
            installJson: entry.installJson,
            existingApps: resolvedEntriesForPorts,
            existingAssignedPort: entry.assignedPort,
        });
        if (assigned.assignedPort !== entry.assignedPort) {
            const nextEntry = { ...entry, assignedPort: assigned.assignedPort };
            resolvedById.set(appId, nextEntry);
            const idx = resolvedEntriesForPorts.findIndex((item) => item.id === appId);
            if (idx >= 0)
                resolvedEntriesForPorts[idx] = nextEntry;
        }
        if (assigned.reason === 'reassigned_from_existing_conflict' ||
            assigned.reason === 'reassigned_from_existing_unbindable') {
            pushIssue({
                kind: 'assigned_port_reassigned',
                severity: 'warning',
                message: `App '${appId}' assignedPort was reassigned due to runtime port conflict.`,
                detail: {
                    appId,
                    previousAssignedPort: entry.assignedPort,
                    reassignedToPort: assigned.assignedPort,
                    reason: assigned.reason,
                    resolutionFileRelPath: resolution_file_1.APPS_RESOLUTION_REL_PATH,
                },
            });
        }
    }
    const apps = [];
    for (const [appId, entry] of resolvedById.entries()) {
        apps.push({ ...entry, enabled: effectiveEnabledById.get(appId) ?? false });
    }
    return {
        resolutionFile: { schemaVersion: 1, apps },
        issues,
    };
}
async function resolveAppsGraphState(params) {
    const loadedConfig = await (0, configuration_file_1.loadAppsConfigurationFile)({ rtwsRootAbs: params.rtwsRootAbs });
    if (loadedConfig.kind === 'error') {
        throw new Error(`Failed to load apps configuration: ${loadedConfig.errorText} (${loadedConfig.filePathAbs})`);
    }
    const loadedResolution = await (0, resolution_file_1.loadAppsResolutionFile)({ rtwsRootAbs: params.rtwsRootAbs });
    if (loadedResolution.kind === 'error') {
        throw new Error(`Failed to load apps resolution snapshot: ${loadedResolution.errorText} (${loadedResolution.filePathAbs})`);
    }
    const loadedLock = await (0, app_lock_file_1.loadAppLockFile)({ rtwsRootAbs: params.rtwsRootAbs });
    if (loadedLock.kind === 'error') {
        throw new Error(`Failed to load app lock: ${loadedLock.errorText} (${loadedLock.filePathAbs})`);
    }
    const previousResolutionById = new Map();
    for (const entry of loadedResolution.file.apps) {
        previousResolutionById.set(entry.id, entry);
    }
    const lockedHintsById = new Map();
    for (const entry of loadedLock.file.apps) {
        lockedHintsById.set(entry.id, makeLockedAppHint(entry));
    }
    const resolved = await resolveGraph({
        rtwsRootAbs: params.rtwsRootAbs,
        strategy: (0, configuration_file_1.normalizeAppsResolutionStrategy)(loadedConfig.file.resolutionStrategy),
        configurationDisabledApps: new Set(loadedConfig.file.disabledApps ?? []),
        previousResolutionById,
        lockedHintsById,
        hasConfigurationFile: loadedConfig.exists,
    });
    return resolved;
}
async function materializeAppsResolution(params) {
    const loadedResolution = await (0, resolution_file_1.loadAppsResolutionFile)({ rtwsRootAbs: params.rtwsRootAbs });
    if (loadedResolution.kind === 'error') {
        throw new Error(`Failed to load apps resolution snapshot: ${loadedResolution.errorText} (${loadedResolution.filePathAbs})`);
    }
    const resolved = await resolveAppsGraphState({ rtwsRootAbs: params.rtwsRootAbs });
    if (JSON.stringify(loadedResolution.file) !== JSON.stringify(resolved.resolutionFile)) {
        await (0, resolution_file_1.writeAppsResolutionFileIfChanged)({
            rtwsRootAbs: params.rtwsRootAbs,
            file: resolved.resolutionFile,
        });
        return { resolved, wroteResolutionFile: true };
    }
    return { resolved, wroteResolutionFile: false };
}
async function loadEnabledAppsSnapshot(params) {
    const { resolved } = await materializeAppsResolution({ rtwsRootAbs: params.rtwsRootAbs });
    return {
        enabledApps: resolved.resolutionFile.apps
            .filter((app) => app.enabled)
            .map((app) => ({
            id: app.id,
            runtimePort: getRuntimePort(app),
            installJson: app.installJson,
            source: app.source,
        })),
        issues: resolved.issues,
    };
}
