"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VersionGraph = void 0;
exports.getDataVersion = getDataVersion;
exports.setDataVersion = setDataVersion;
exports.overlaps = overlaps;
const exver_1 = require("../../../base/lib/exver");
const util_1 = require("../util");
const VersionInfo_1 = require("./VersionInfo");
/**
 * Read the current data version from the effects system.
 * @param effects - The effects context
 * @returns The parsed ExtendedVersion or VersionRange, or null if no version is set
 */
async function getDataVersion(effects) {
    const versionStr = await effects.getDataVersion();
    if (!versionStr)
        return null;
    try {
        return exver_1.ExtendedVersion.parse(versionStr);
    }
    catch (_) {
        return exver_1.VersionRange.parse(versionStr);
    }
}
/**
 * Persist a data version to the effects system.
 * @param effects - The effects context
 * @param version - The version to set, or null to clear it
 */
async function setDataVersion(effects, version) {
    return effects.setDataVersion({ version: version?.toString() || null });
}
function isExver(v) {
    return 'satisfies' in v;
}
function isRange(v) {
    return 'satisfiedBy' in v;
}
/**
 * Check whether two version specifiers overlap (i.e. share at least one common version).
 * Works with any combination of ExtendedVersion and VersionRange.
 *
 * @param a - First version or range
 * @param b - Second version or range
 * @returns True if the two specifiers overlap
 */
function overlaps(a, b) {
    return ((isRange(a) && isRange(b) && a.intersects(b)) ||
        (isRange(a) && isExver(b) && a.satisfiedBy(b)) ||
        (isExver(a) && isRange(b) && a.satisfies(b)) ||
        (isExver(a) && isExver(b) && a.equals(b)));
}
/**
 * A directed graph of service versions and their migration paths.
 *
 * Builds a graph from {@link VersionInfo} definitions, then uses shortest-path
 * search to find and execute migration sequences between any two versions.
 * Implements both {@link InitScript} (for install/update migrations) and
 * {@link UninitScript} (for uninstall/downgrade migrations).
 *
 * @typeParam CurrentVersion - The string literal type of the current service version
 */
class VersionGraph {
    /** Dump the version graph as a human-readable string for debugging */
    dump() {
        return this.graph().dump((metadata) => metadata?.toString());
    }
    constructor(current, versions) {
        this.current = current;
        this.initFn = this.init.bind(this);
        this.uninitFn = this.uninit.bind(this);
        this.currentVersion = (0, util_1.once)(() => exver_1.ExtendedVersion.parse(this.current.options.version));
        /**
         * Compute the version range from which the current version can be reached via migration.
         * Uses reverse breadth-first search from the current version vertex.
         */
        this.canMigrateFrom = (0, util_1.once)(() => Array.from(this.graph().reverseBreadthFirstSearch((v) => overlaps(v.metadata, this.currentVersion())))
            .reduce((acc, x) => acc.or(isRange(x.metadata)
            ? x.metadata
            : exver_1.VersionRange.anchor('=', x.metadata)), exver_1.VersionRange.none())
            .normalize());
        /**
         * Compute the version range that the current version can migrate to.
         * Uses forward breadth-first search from the current version vertex.
         */
        this.canMigrateTo = (0, util_1.once)(() => Array.from(this.graph().breadthFirstSearch((v) => overlaps(v.metadata, this.currentVersion())))
            .reduce((acc, x) => acc.or(isRange(x.metadata)
            ? x.metadata
            : exver_1.VersionRange.anchor('=', x.metadata)), exver_1.VersionRange.none())
            .normalize());
        this.graph = (0, util_1.once)(() => {
            const graph = new util_1.Graph();
            const flavorMap = {};
            for (let version of [current, ...versions]) {
                const v = exver_1.ExtendedVersion.parse(version.options.version);
                const vertex = graph.addVertex(v, [], []);
                const flavor = v.flavor || '';
                if (!flavorMap[flavor]) {
                    flavorMap[flavor] = [];
                }
                flavorMap[flavor].push([v, version, vertex]);
            }
            for (let flavor in flavorMap) {
                flavorMap[flavor].sort((a, b) => a[0].compareForSort(b[0]));
                let prev = undefined;
                for (let [v, version, vertex] of flavorMap[flavor]) {
                    if (version.options.migrations.up !== VersionInfo_1.IMPOSSIBLE) {
                        let range;
                        if (prev) {
                            graph.addEdge(version.options.migrations.up, prev[2], vertex);
                            range = exver_1.VersionRange.anchor('>=', prev[0]).and(exver_1.VersionRange.anchor('<', v));
                        }
                        else {
                            range = exver_1.VersionRange.anchor('<', v);
                        }
                        const vRange = graph.addVertex(range, [], []);
                        graph.addEdge(version.options.migrations.up, vRange, vertex);
                    }
                    if (version.options.migrations.down !== VersionInfo_1.IMPOSSIBLE) {
                        let range;
                        if (prev) {
                            graph.addEdge(version.options.migrations.down, vertex, prev[2]);
                            range = exver_1.VersionRange.anchor('>=', prev[0]).and(exver_1.VersionRange.anchor('<', v));
                        }
                        else {
                            range = exver_1.VersionRange.anchor('<', v);
                        }
                        const vRange = graph.addVertex(range, [], []);
                        graph.addEdge(version.options.migrations.down, vertex, vRange);
                    }
                    if (version.options.migrations.other) {
                        for (let rangeStr in version.options.migrations.other) {
                            const range = exver_1.VersionRange.parse(rangeStr);
                            const vRange = graph.addVertex(range, [], []);
                            const migration = version.options.migrations.other[rangeStr];
                            if (migration.up)
                                graph.addEdge(migration.up, vRange, vertex);
                            if (migration.down)
                                graph.addEdge(migration.down, vertex, vRange);
                            for (let matching of graph.findVertex((v) => isExver(v.metadata) && v.metadata.satisfies(range))) {
                                if (migration.up)
                                    graph.addEdge(migration.up, matching, vertex);
                                if (migration.down)
                                    graph.addEdge(migration.down, vertex, matching);
                            }
                        }
                    }
                    prev = [v, version, vertex];
                }
            }
            return graph;
        });
    }
    /**
     * Each exported `VersionInfo.of()` should be imported and provided as an argument to this function.
     *
     * ** The current version must be the FIRST argument. **
     */
    static of(options) {
        return new VersionGraph(options.current, options.other);
    }
    /**
     * Execute the shortest migration path between two versions.
     *
     * Finds the shortest path in the version graph from `from` to `to`,
     * executes each migration step in order, and updates the data version after each step.
     *
     * @param options.effects - The effects context
     * @param options.from - The source version or range
     * @param options.to - The target version or range
     * @returns The final data version after migration
     * @throws If no migration path exists between the two versions
     */
    async migrate({ effects, from, to, }) {
        if (overlaps(from, to))
            return from;
        const graph = this.graph();
        if (from && to) {
            const path = graph.shortestPath((v) => overlaps(v.metadata, from), (v) => overlaps(v.metadata, to));
            if (path) {
                console.log(`Migrating ${path.reduce(({ acc, prev }, x) => ({
                    acc: acc +
                        (prev && prev != x.from.metadata.toString()
                            ? ` (as ${prev})`
                            : '') +
                        ' -> ' +
                        x.to.metadata.toString(),
                    prev: x.to.metadata.toString(),
                }), { acc: from.toString(), prev: null }).acc}`);
                let dataVersion = from;
                for (let edge of path) {
                    if (edge.metadata) {
                        await edge.metadata({ effects });
                    }
                    dataVersion = edge.to.metadata;
                    await setDataVersion(effects, edge.to.metadata);
                }
                return dataVersion;
            }
        }
        throw new Error(`cannot migrate from ${from.toString()} to ${to.toString()}`);
    }
    /**
     * InitScript implementation: migrate from the stored data version to the current version.
     * If no data version exists (fresh install), sets it to the current version.
     * @param effects - The effects context
     */
    async init(effects) {
        const from = await getDataVersion(effects);
        if (from) {
            await this.migrate({
                effects,
                from,
                to: this.currentVersion(),
            });
        }
        else {
            await effects.setDataVersion({ version: this.current.options.version });
        }
    }
    /**
     * UninitScript implementation: migrate from the current data version to the target version.
     * Used during uninstall or downgrade to prepare data for the target version.
     *
     * @param effects - The effects context
     * @param target - The target version to migrate to, or null to clear the data version
     */
    async uninit(effects, target) {
        if (target) {
            if (isRange(target) && !target.satisfiable()) {
                return;
            }
            const from = await getDataVersion(effects);
            if (from) {
                target = await this.migrate({
                    effects,
                    from,
                    to: target,
                });
            }
        }
        await setDataVersion(effects, target);
    }
}
exports.VersionGraph = VersionGraph;
//# sourceMappingURL=VersionGraph.js.map