/**
 * Vertical pack install / uninstall lifecycle.
 *
 * Reading a schema manifest from `src/services/verticals/schemas/<slug>.json`,
 * registering each schema via `SchemaRegistryService.register` (or upgrading
 * an existing schema via `updateSchemaIncremental`), and recording installed
 * pack state in `${NEOTOMA_DATA_DIR}/installed_vertical_packs.json` so that
 * subsequent boots and uninstalls can find what was installed.
 *
 * Idempotent: installing the same pack twice with no schema changes is a
 * no-op. Re-installing a pack with an upgraded manifest replays the diff via
 * `updateSchemaIncremental`. Uninstall removes the pack from the install
 * registry but does NOT drop schemas, since data may already exist for them.
 */
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname, isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
import { config } from "../../config.js";
import { schemaRegistry, } from "../schema_registry.js";
import { findVerticalPack, resolveVerticalPack, VERTICAL_PACKS, } from "./registry.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
function projectRoot() {
    return config.projectRoot;
}
function installRegistryPath() {
    return join(config.dataDir, "installed_vertical_packs.json");
}
function ensureDataDir() {
    const dir = dirname(installRegistryPath());
    if (!existsSync(dir)) {
        mkdirSync(dir, { recursive: true });
    }
}
function readInstallRegistry() {
    const path = installRegistryPath();
    if (!existsSync(path)) {
        return { version: 1, packs: {} };
    }
    try {
        const raw = readFileSync(path, "utf-8");
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== "object" || !parsed.packs) {
            return { version: 1, packs: {} };
        }
        return parsed;
    }
    catch (err) {
        process.stderr.write(`[verticals] Failed to read install registry at ${path}: ${err.message}\n`);
        return { version: 1, packs: {} };
    }
}
function writeInstallRegistry(file) {
    ensureDataDir();
    writeFileSync(installRegistryPath(), JSON.stringify(file, null, 2) + "\n", "utf-8");
}
function resolveManifestPath(packOrPath) {
    if (typeof packOrPath === "string") {
        return isAbsolute(packOrPath)
            ? packOrPath
            : join(projectRoot(), packOrPath);
    }
    if (!packOrPath.schema_manifest_path) {
        throw new Error(`[verticals] pack '${packOrPath.slug}' has no schema_manifest_path`);
    }
    return isAbsolute(packOrPath.schema_manifest_path)
        ? packOrPath.schema_manifest_path
        : join(projectRoot(), packOrPath.schema_manifest_path);
}
export function readManifest(packOrPath) {
    const path = resolveManifestPath(packOrPath);
    if (!existsSync(path)) {
        throw new Error(`[verticals] schema manifest not found at ${path}`);
    }
    const raw = readFileSync(path, "utf-8");
    const manifest = JSON.parse(raw);
    validateManifest(manifest, path);
    return manifest;
}
function validateManifest(manifest, path) {
    if (!manifest || typeof manifest !== "object") {
        throw new Error(`[verticals] manifest at ${path} is not a JSON object`);
    }
    if (!manifest.vertical || typeof manifest.vertical !== "string") {
        throw new Error(`[verticals] manifest at ${path} missing 'vertical'`);
    }
    if (!Array.isArray(manifest.schemas) || manifest.schemas.length === 0) {
        throw new Error(`[verticals] manifest at ${path} must declare at least one schema`);
    }
    for (const entry of manifest.schemas) {
        if (!entry.entity_type || typeof entry.entity_type !== "string") {
            throw new Error(`[verticals] manifest ${path} has a schema entry missing entity_type`);
        }
        if (!entry.schema_version || typeof entry.schema_version !== "string") {
            throw new Error(`[verticals] manifest ${path} entry '${entry.entity_type}' missing schema_version`);
        }
        if (!entry.schema_definition || !entry.schema_definition.fields) {
            throw new Error(`[verticals] manifest ${path} entry '${entry.entity_type}' missing schema_definition.fields`);
        }
        if (!entry.reducer_config) {
            throw new Error(`[verticals] manifest ${path} entry '${entry.entity_type}' missing reducer_config`);
        }
    }
}
function fieldsAreCompatible(existing, incoming) {
    const missingFields = [];
    for (const [name, def] of Object.entries(incoming.fields)) {
        const prior = existing.fields[name];
        if (!prior) {
            missingFields.push(name);
            continue;
        }
        if (prior.type !== def.type) {
            return {
                compatible: false,
                reason: `field '${name}' type changed (${prior.type} -> ${def.type})`,
                missingFields,
            };
        }
    }
    return { compatible: true, missingFields };
}
/** Install a vertical pack by slug. Idempotent. */
export async function installVerticalPack(slug, options = {}) {
    const pack = resolveVerticalPack(slug);
    if (!pack.schema_manifest_path) {
        return {
            slug: pack.slug,
            installed: [],
            upgraded: [],
            skipped: [],
            manifest_path: "",
        };
    }
    const manifestPath = resolveManifestPath(pack);
    const manifest = readManifest(pack);
    const installed = [];
    const upgraded = [];
    const skipped = [];
    for (const entry of manifest.schemas) {
        const current = await schemaRegistry.loadActiveSchema(entry.entity_type, options.user_id);
        if (!current) {
            await schemaRegistry.register({
                entity_type: entry.entity_type,
                schema_version: entry.schema_version,
                schema_definition: entry.schema_definition,
                reducer_config: entry.reducer_config,
                metadata: entry.metadata,
                user_id: options.user_id,
                user_specific: options.user_specific,
                activate: true,
            });
            installed.push(entry);
            continue;
        }
        if (current.schema_version === entry.schema_version) {
            skipped.push({
                entity_type: entry.entity_type,
                reason: `already at version ${current.schema_version}`,
            });
            continue;
        }
        const compat = fieldsAreCompatible(current.schema_definition, entry.schema_definition);
        if (!compat.compatible) {
            skipped.push({
                entity_type: entry.entity_type,
                reason: `incompatible upgrade: ${compat.reason}`,
            });
            continue;
        }
        if (compat.missingFields.length === 0) {
            skipped.push({
                entity_type: entry.entity_type,
                reason: `no field diff vs ${current.schema_version}`,
            });
            continue;
        }
        const fieldsToAdd = compat.missingFields.map((name) => {
            const def = entry.schema_definition.fields[name];
            const reducerStrategy = entry.reducer_config.merge_policies?.[name]?.strategy;
            return {
                field_name: name,
                field_type: def.type,
                required: def.required ?? false,
                reducer_strategy: reducerStrategy,
            };
        });
        await schemaRegistry.updateSchemaIncremental({
            entity_type: entry.entity_type,
            fields_to_add: fieldsToAdd,
            schema_version: entry.schema_version,
            user_id: options.user_id,
            user_specific: options.user_specific,
            activate: true,
        });
        upgraded.push(entry);
    }
    const file = readInstallRegistry();
    file.packs[pack.slug] = {
        slug: pack.slug,
        installed_at: new Date().toISOString(),
        manifest_path: pack.schema_manifest_path,
        schemas: manifest.schemas.map((s) => ({
            entity_type: s.entity_type,
            schema_version: s.schema_version,
        })),
    };
    writeInstallRegistry(file);
    return {
        slug: pack.slug,
        installed,
        upgraded,
        skipped,
        manifest_path: manifestPath,
    };
}
/**
 * Remove a vertical pack from the install registry. Schema definitions are
 * retained because data may already exist for them; we only forget that the
 * pack was the source.
 */
export async function uninstallVerticalPack(slug) {
    const pack = findVerticalPack(slug);
    const resolvedSlug = pack?.slug ?? slug;
    const file = readInstallRegistry();
    const record = file.packs[resolvedSlug];
    if (!record) {
        return {
            slug: resolvedSlug,
            removed_record: false,
            retained_schemas: [],
        };
    }
    const retained = record.schemas.map((s) => s.entity_type);
    delete file.packs[resolvedSlug];
    writeInstallRegistry(file);
    return {
        slug: resolvedSlug,
        removed_record: true,
        retained_schemas: retained,
    };
}
/** Return the install records currently on disk. */
export function getInstalledPacks() {
    const file = readInstallRegistry();
    return Object.values(file.packs).sort((a, b) => a.slug.localeCompare(b.slug));
}
/** Return true when the pack slug appears in the install registry. */
export function isVerticalPackInstalled(slug) {
    const file = readInstallRegistry();
    return Boolean(file.packs[slug]);
}
/** All packs that ship a schema manifest, paired with their install state. */
export function listPacksWithStatus() {
    const file = readInstallRegistry();
    return VERTICAL_PACKS.map((pack) => ({
        pack,
        installed: Boolean(file.packs[pack.slug]),
        installed_at: file.packs[pack.slug]?.installed_at,
    }));
}
//# sourceMappingURL=install.js.map