"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkflowStatePatchError = void 0;
exports.defineWorkflowStatePatch = defineWorkflowStatePatch;
exports.applyWorkflowStatePatches = applyWorkflowStatePatches;
exports.getWorkflowStatePatches = getWorkflowStatePatches;
const node_fs_1 = require("node:fs");
const node_module_1 = require("node:module");
const node_path_1 = __importDefault(require("node:path"));
const types_1 = require("../../types");
const persisted_state_1 = require("../persisted-state");
const requireWorkflowStatePatch = (0, node_module_1.createRequire)(__filename);
class WorkflowStatePatchError extends Error {
    constructor(message) {
        super(message);
        this.name = "WorkflowStatePatchError";
    }
}
exports.WorkflowStatePatchError = WorkflowStatePatchError;
let cachedWorkflowStatePatches = null;
function defineWorkflowStatePatch(definition) {
    assertValidPatchVersion(definition.version, "Workflow state patch version");
    return {
        version: definition.version,
        changes: definition.changes,
    };
}
function applyWorkflowStatePatches(input) {
    const targetVersion = input.targetVersion ?? types_1.WORKFLOW_STATE_VERSION;
    assertValidPatchVersion(targetVersion, "Workflow state target version");
    let current = clonePatchValue(input.state);
    const currentVersion = readWorkflowStateVersion(current);
    if (currentVersion > targetVersion) {
        throw new WorkflowStatePatchError(`Workflow state version ${currentVersion} is newer than target version ${targetVersion}.`);
    }
    const patchesByVersion = buildPatchMap(input.patches ?? getWorkflowStatePatches());
    const appliedPatchVersions = [];
    for (let nextVersion = currentVersion + 1; nextVersion <= targetVersion; nextVersion += 1) {
        const patch = patchesByVersion.get(nextVersion);
        if (patch === undefined) {
            throw new WorkflowStatePatchError(`Missing workflow state patch for v${nextVersion}.`);
        }
        current = applyWorkflowStatePatch(current, patch);
        current = setPathValue(current, ["version"], nextVersion, false);
        appliedPatchVersions.push(nextVersion);
    }
    const coerced = (0, persisted_state_1.coercePersistedWorkflowStateValue)(current);
    if (coerced === null) {
        throw new WorkflowStatePatchError(`Workflow state patch result is invalid for v${targetVersion}.`);
    }
    return { state: coerced, appliedPatchVersions };
}
function getWorkflowStatePatches() {
    cachedWorkflowStatePatches ??= loadWorkflowStatePatchDefinitions(__dirname);
    return cachedWorkflowStatePatches;
}
function applyWorkflowStatePatch(state, patch) {
    assertValidPatchVersion(patch.version, "Workflow state patch version");
    return applyPatchChanges(state, patch.changes);
}
function applyPatchChanges(state, changes) {
    let current = state;
    for (const change of changes) {
        current = applyPatchChange(current, change);
    }
    return current;
}
function applyPatchChange(state, change) {
    switch (change.op) {
        case "setDefault":
            return setPathValue(state, parsePatchPath(change.path), change.value, true);
        case "set":
            return setPathValue(state, parsePatchPath(change.path), change.value, false);
        case "remove":
            return removePathValue(state, parsePatchPath(change.path));
        case "rename":
            return renamePathValue(state, parsePatchPath(change.from), parsePatchPath(change.to));
        case "mapValue":
            return mapPathValue(state, parsePatchPath(change.path), change.values);
        case "arrayEach":
            return mapArrayPathValue(state, parsePatchPath(change.path), (item) => applyPatchChanges(item, change.changes));
        case "custom":
            return change.apply(toPatchDocument(state, change.name));
    }
}
function buildPatchMap(patches) {
    const patchesByVersion = new Map();
    for (const patch of patches) {
        assertValidPatchVersion(patch.version, "Workflow state patch version");
        const existing = patchesByVersion.get(patch.version);
        if (existing !== undefined) {
            throw new WorkflowStatePatchError(`Duplicate workflow state patch for v${patch.version}.`);
        }
        patchesByVersion.set(patch.version, patch);
    }
    return patchesByVersion;
}
function loadWorkflowStatePatchDefinitions(directoryPath) {
    const patchFileExtension = __filename.endsWith(".ts") ? ".ts" : ".js";
    const entries = (0, node_fs_1.readdirSync)(directoryPath, { withFileTypes: true })
        .filter((entry) => entry.isFile())
        .flatMap((entry) => {
        if (!entry.name.endsWith(patchFileExtension) ||
            entry.name === `index${patchFileExtension}`) {
            return [];
        }
        const match = new RegExp(`^v(\\d+)-[a-z0-9][a-z0-9-]*\\${patchFileExtension}$`, "u").exec(entry.name);
        if (match === null) {
            if (/^v\d+/u.test(entry.name)) {
                throw new WorkflowStatePatchError(`Workflow state patch file must match v<version>-<summary>${patchFileExtension}: ${entry.name}`);
            }
            return [];
        }
        return [
            {
                fileName: entry.name,
                filePath: node_path_1.default.join(directoryPath, entry.name),
                version: Number(match[1]),
            },
        ];
    })
        .sort((left, right) => left.version - right.version);
    return entries.map((entry) => {
        const patch = coerceLoadedWorkflowStatePatch(requireWorkflowStatePatch(entry.filePath), entry.fileName);
        if (patch.version !== entry.version) {
            throw new WorkflowStatePatchError(`Workflow state patch ${entry.fileName} declares v${patch.version}; expected v${entry.version}.`);
        }
        return patch;
    });
}
function coerceLoadedWorkflowStatePatch(moduleExports, fileName) {
    const candidate = asWorkflowStatePatchModule(moduleExports);
    const patch = candidate?.default ?? candidate?.patch ?? candidate?.workflowStatePatch;
    if (!isWorkflowStatePatch(patch)) {
        throw new WorkflowStatePatchError(`Workflow state patch ${fileName} must export a patch definition.`);
    }
    return {
        version: patch.version,
        changes: patch.changes,
    };
}
function asWorkflowStatePatchModule(value) {
    return isRecord(value)
        ? value
        : null;
}
function isWorkflowStatePatch(value) {
    if (!isRecord(value)) {
        return false;
    }
    const candidate = value;
    return (typeof candidate.version === "number" && Array.isArray(candidate.changes));
}
function readWorkflowStateVersion(state) {
    if (!hasVersionField(state)) {
        throw new WorkflowStatePatchError("Workflow state must be an object.");
    }
    const version = state.version;
    if (typeof version !== "number" ||
        !Number.isSafeInteger(version) ||
        version < 0) {
        throw new WorkflowStatePatchError("Workflow state version must be a non-negative integer.");
    }
    return version;
}
function assertValidPatchVersion(version, label) {
    if (!Number.isSafeInteger(version) || version < 0) {
        throw new WorkflowStatePatchError(`${label} must be a non-negative integer.`);
    }
}
function parsePatchPath(path) {
    if (path.length === 0 || !path.startsWith("/")) {
        throw new WorkflowStatePatchError(`Workflow state patch path must start with "/": ${path}`);
    }
    return path
        .slice(1)
        .split("/")
        .map((segment) => segment.replace(/~1/gu, "/").replace(/~0/gu, "~"));
}
function getPathValue(state, pathSegments) {
    let current = state;
    for (const segment of pathSegments) {
        if (Array.isArray(current)) {
            const index = parseArrayIndex(segment);
            if (index === null || index >= current.length) {
                return { exists: false, value: undefined };
            }
            current = current[index];
            continue;
        }
        if (!isRecord(current) || !Object.hasOwn(current, segment)) {
            return { exists: false, value: undefined };
        }
        current = Reflect.get(current, segment);
    }
    return { exists: true, value: current };
}
function setPathValue(state, pathSegments, value, onlyIfMissing) {
    if (pathSegments.length === 0) {
        return onlyIfMissing && state !== undefined
            ? state
            : clonePatchValue(value);
    }
    if (onlyIfMissing && getPathValue(state, pathSegments).exists) {
        return state;
    }
    const [head, ...tail] = pathSegments;
    if (head === undefined) {
        return state;
    }
    if (Array.isArray(state)) {
        const index = parseArrayIndex(head);
        if (index === null || index > state.length) {
            throw new WorkflowStatePatchError(`Invalid workflow state array path.`);
        }
        const next = [...state];
        next[index] = setPathValue(next[index], tail, value, false);
        return next;
    }
    const source = isRecord(state) ? state : {};
    return {
        ...source,
        [head]: setPathValue(Reflect.get(source, head), tail, value, false),
    };
}
function removePathValue(state, pathSegments) {
    if (pathSegments.length === 0) {
        return undefined;
    }
    const [head, ...tail] = pathSegments;
    if (head === undefined) {
        return state;
    }
    if (Array.isArray(state)) {
        const index = parseArrayIndex(head);
        if (index === null || index >= state.length) {
            return state;
        }
        const next = [...state];
        if (tail.length === 0) {
            next.splice(index, 1);
            return next;
        }
        next[index] = removePathValue(next[index], tail);
        return next;
    }
    if (!isRecord(state) || !Object.hasOwn(state, head)) {
        return state;
    }
    const next = { ...state };
    if (tail.length === 0) {
        Reflect.deleteProperty(next, head);
        return next;
    }
    Reflect.set(next, head, removePathValue(Reflect.get(next, head), tail));
    return next;
}
function renamePathValue(state, fromPathSegments, toPathSegments) {
    const source = getPathValue(state, fromPathSegments);
    if (!source.exists) {
        return state;
    }
    const withoutSource = removePathValue(state, fromPathSegments);
    return setPathValue(withoutSource, toPathSegments, source.value, false);
}
function mapPathValue(state, pathSegments, values) {
    const current = getPathValue(state, pathSegments);
    if (!current.exists) {
        return state;
    }
    const key = mapValueKey(current.value);
    if (key === null || !Object.hasOwn(values, key)) {
        return state;
    }
    return setPathValue(state, pathSegments, Reflect.get(values, key), false);
}
function mapArrayPathValue(state, pathSegments, mapItem) {
    const current = getPathValue(state, pathSegments);
    if (!current.exists) {
        return state;
    }
    if (!Array.isArray(current.value)) {
        throw new WorkflowStatePatchError("Workflow state arrayEach path must point to an array.");
    }
    return setPathValue(state, pathSegments, current.value.map(mapItem), false);
}
function mapValueKey(value) {
    if (value === null ||
        typeof value === "string" ||
        typeof value === "number" ||
        typeof value === "boolean") {
        return String(value);
    }
    return null;
}
function toPatchDocument(value, customPatchName) {
    if (!isRecord(value)) {
        throw new WorkflowStatePatchError(`Custom workflow state patch ${customPatchName} must receive an object.`);
    }
    return value;
}
function parseArrayIndex(segment) {
    if (!/^(0|[1-9]\d*)$/u.test(segment)) {
        return null;
    }
    return Number(segment);
}
function hasVersionField(value) {
    return isRecord(value) && Object.hasOwn(value, "version");
}
function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function clonePatchValue(value) {
    if (Array.isArray(value)) {
        return value.map((item) => clonePatchValue(item));
    }
    if (isRecord(value)) {
        return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clonePatchValue(item)]));
    }
    return value;
}
