"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkflowPlanPatchError = void 0;
exports.applyWorkflowPlanPatch = applyWorkflowPlanPatch;
const catalog_1 = require("../../i18n/catalog");
const types_1 = require("../types");
const plan_support_1 = require("./plan-support");
class WorkflowPlanPatchError extends Error {
    constructor(message) {
        super(message);
        this.name = "WorkflowPlanPatchError";
    }
}
exports.WorkflowPlanPatchError = WorkflowPlanPatchError;
function applyWorkflowPlanPatch(input) {
    const { workflowState, patch } = input;
    const plan = workflowState.plan ?? { steps: [] };
    if (patch.basePlanRevision !== workflowState.planRevision) {
        throw new WorkflowPlanPatchError(`Workflow plan patch base revision ${patch.basePlanRevision} does not match current revision ${workflowState.planRevision}.`);
    }
    let nextPlan = plan;
    const childIssueDecisionValidation = validateChildIssueDecisions({
        plan,
        stepChanges: patch.stepChanges,
        childIssueDecisions: patch.childIssueDecisions,
    });
    const childDecisionByKey = new Map(childIssueDecisionValidation.decisions.map((decision) => [decision.childKey, decision]));
    for (const change of patch.stepChanges) {
        nextPlan = applyWorkflowStepChange(nextPlan, change, childDecisionByKey);
    }
    const { pendingChildIssueDecisions: _pendingChildIssueDecisions, ...baseState } = workflowState;
    let nextWorkflowState = {
        ...baseState,
        plan: nextPlan,
        ...(childIssueDecisionValidation.decisions.length > 0
            ? {
                pendingChildIssueDecisions: childIssueDecisionValidation.decisions.map((decision) => ({
                    ...decision,
                })),
            }
            : {}),
    };
    for (const change of patch.workflowRefChanges) {
        nextWorkflowState = applyWorkflowRefChange({
            workflowState: nextWorkflowState,
            change,
            locale: input.locale ?? catalog_1.DEFAULT_APP_LOCALE,
        });
    }
    const { pendingPlanningIntent: _pendingPlanningIntent, pendingPlanningReason: _pendingPlanningReason, pendingPlanningSource: _pendingPlanningSource, ...clearedPlanningState } = nextWorkflowState;
    return {
        ...clearedPlanningState,
        planRevision: workflowState.planRevision + 1,
        finalResult: null,
    };
}
function applyWorkflowStepChange(plan, change, childDecisionByKey) {
    switch (change.action) {
        case "add_step":
            return addWorkflowStep(plan, change);
        case "update_step":
            return updateWorkflowStep(plan, change);
        case "remove_step":
            return removeWorkflowStep(plan, {
                stepId: change.stepId,
                childDecisionByKey,
            });
        case "move_step":
            return moveWorkflowStep(plan, change);
    }
}
function addWorkflowStep(plan, change) {
    assertStepIsIncomplete(change.step, ["stepChanges", "step"]);
    assertNoWorkerAuthoredWorkflowRefOriginMetadata(change.step);
    const existingStepIds = new Set(plan.steps.map((step) => step.id));
    if (existingStepIds.has(change.step.id)) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot add duplicate step ${change.step.id}.`);
    }
    validateReplacementStepIds(plan, change.replacementForStepIds ?? []);
    const normalizedStep = (0, plan_support_1.normalizeWorkflowStep)({
        ...change.step,
        replacementForStepIds: [...(change.replacementForStepIds ?? [])],
        result: null,
    });
    const afterStepId = change.afterStepId ?? null;
    if (afterStepId === null) {
        return validateWorkflowPlanOrThrow({
            steps: [normalizedStep, ...plan.steps],
        });
    }
    const afterIndex = plan.steps.findIndex((step) => step.id === afterStepId);
    if (afterIndex < 0) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot insert after missing step ${afterStepId}.`);
    }
    return validateWorkflowPlanOrThrow({
        steps: [
            ...plan.steps.slice(0, afterIndex + 1),
            normalizedStep,
            ...plan.steps.slice(afterIndex + 1),
        ],
    });
}
function updateWorkflowStep(plan, change) {
    const index = findStepIndexOrThrow(plan, change.stepId);
    const previousStep = plan.steps[index];
    if (previousStep === undefined) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot update missing step ${change.stepId}.`);
    }
    if (previousStep.result !== null) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot update completed step ${change.stepId}; add replacement work instead.`);
    }
    if (change.step.id !== change.stepId) {
        throw new WorkflowPlanPatchError(`Workflow plan patch update_step must keep step id ${change.stepId}.`);
    }
    if (previousStep.kind !== change.step.kind) {
        throw new WorkflowPlanPatchError(`Workflow plan patch update_step cannot change step kind for ${change.stepId}.`);
    }
    if (previousStep.kind === "workflow_ref") {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot update workflow_ref step ${change.stepId}; use workflowRefChanges instead.`);
    }
    assertStepIsIncomplete(change.step, ["stepChanges", change.stepId]);
    assertNoWorkerAuthoredWorkflowRefOriginMetadata(change.step);
    assertControlPlaneOwnedFieldsArePreserved(previousStep, change.step);
    const nextSteps = [...plan.steps];
    nextSteps[index] = (0, plan_support_1.normalizeWorkflowStep)({
        ...change.step,
        result: null,
    });
    return validateWorkflowPlanOrThrow({ steps: nextSteps });
}
function assertNoWorkerAuthoredWorkflowRefOriginMetadata(step) {
    if (step.originSubtreeKey !== undefined ||
        step.originWorkflowRef !== undefined) {
        throw new WorkflowPlanPatchError(`Workflow plan patch step ${step.id} cannot include workflow_ref origin metadata; use workflowRefChanges instead.`);
    }
}
function removeWorkflowStep(plan, input) {
    const { stepId } = input;
    const index = findStepIndexOrThrow(plan, stepId);
    const step = plan.steps[index];
    if (step === undefined) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot remove missing step ${stepId}.`);
    }
    assertStepHasNoDurableSideEffects(step, input.childDecisionByKey);
    const replacementDependencies = step.dependsOnStepIds;
    const nextSteps = plan.steps
        .filter((candidate) => candidate.id !== stepId)
        .map((candidate) => (0, plan_support_1.normalizeWorkflowStep)({
        ...candidate,
        dependsOnStepIds: replaceDependencyId(candidate.dependsOnStepIds, stepId, replacementDependencies),
    }));
    return validateWorkflowPlanOrThrow({ steps: nextSteps });
}
function moveWorkflowStep(plan, change) {
    const index = findStepIndexOrThrow(plan, change.stepId);
    const step = plan.steps[index];
    if (step === undefined) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot move missing step ${change.stepId}.`);
    }
    assertStepIsMovable(step);
    const afterStepId = change.afterStepId ?? null;
    if (afterStepId === change.stepId) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot move step ${change.stepId} after itself.`);
    }
    const remainingSteps = plan.steps.filter((candidate) => candidate.id !== change.stepId);
    if (afterStepId === null) {
        return validateWorkflowPlanOrThrow({
            steps: [step, ...remainingSteps],
        });
    }
    const afterIndex = remainingSteps.findIndex((candidate) => candidate.id === afterStepId);
    if (afterIndex < 0) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot move after missing step ${afterStepId}.`);
    }
    return validateWorkflowPlanOrThrow({
        steps: [
            ...remainingSteps.slice(0, afterIndex + 1),
            step,
            ...remainingSteps.slice(afterIndex + 1),
        ],
    });
}
function applyWorkflowRefChange(input) {
    switch (input.change.action) {
        case "refresh_workflow_ref":
            return refreshWorkflowRef(input);
        case "remove_workflow_ref":
            return {
                ...input.workflowState,
                plan: removeWorkflowRef(input.workflowState.plan, input.change),
            };
    }
}
function refreshWorkflowRef(input) {
    const plan = requirePlan(input.workflowState.plan);
    const directStep = plan.steps.find((step) => step.kind === "workflow_ref" &&
        step.callSiteId === input.change.callSiteId &&
        step.expansionGroupId === input.change.expansionGroupId);
    if (directStep !== undefined) {
        return input.workflowState;
    }
    const originSubtreeKey = input.change.originSubtreeKey;
    if (originSubtreeKey === undefined) {
        throw new WorkflowPlanPatchError("refresh_workflow_ref for an expanded subtree requires originSubtreeKey.");
    }
    const originWorkflowRef = resolveOriginWorkflowRef(plan, input.change);
    assertWorkflowRefSubtreeHasNoDurableEvidence(plan, originSubtreeKey);
    return (0, plan_support_1.replaceWorkflowExpansionSubtreeWithWorkflowRef)(input.workflowState, {
        originSubtreeKey,
        originWorkflowRef,
        locale: input.locale,
    });
}
function removeWorkflowRef(plan, change) {
    const currentPlan = requirePlan(plan);
    const directStep = currentPlan.steps.find((step) => step.kind === "workflow_ref" &&
        step.callSiteId === change.callSiteId &&
        step.expansionGroupId === change.expansionGroupId);
    if (directStep !== undefined) {
        return removeWorkflowStep(currentPlan, {
            stepId: directStep.id,
        });
    }
    const originSubtreeKey = change.originSubtreeKey;
    if (originSubtreeKey === undefined) {
        throw new WorkflowPlanPatchError("remove_workflow_ref for an expanded subtree requires originSubtreeKey.");
    }
    resolveOriginWorkflowRef(currentPlan, change);
    assertWorkflowRefSubtreeHasNoDurableEvidence(currentPlan, originSubtreeKey);
    const subtreeStepIds = new Set(currentPlan.steps
        .filter((step) => isStepInWorkflowOriginSubtree(step, originSubtreeKey))
        .map((step) => step.id));
    if (subtreeStepIds.size === 0) {
        throw new WorkflowPlanPatchError(`Workflow_ref subtree ${originSubtreeKey} was not found.`);
    }
    const nextSteps = currentPlan.steps
        .filter((step) => !subtreeStepIds.has(step.id))
        .map((step) => (0, plan_support_1.normalizeWorkflowStep)({
        ...step,
        dependsOnStepIds: step.dependsOnStepIds.filter((stepId) => !subtreeStepIds.has(stepId)),
    }));
    return validateWorkflowPlanOrThrow({ steps: nextSteps });
}
function assertStepIsIncomplete(step, path) {
    if (step.result !== undefined && step.result !== null) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot set completed result inside ${path.join(".")}.`);
    }
}
function assertStepHasNoDurableSideEffects(step, childDecisionByKey) {
    if (step.result !== null) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot remove completed step ${step.id}.`);
    }
    if ((0, types_1.isWorkflowChildIssueStep)(step) && step.childIssueRef !== null) {
        const decision = childDecisionByKey?.get(step.childKey);
        if (decision?.action === "detach_child_issue" ||
            decision?.action === "cancel_child_issue") {
            return;
        }
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot remove materialized child issue step ${step.id} without detach_child_issue or cancel_child_issue.`);
    }
}
function assertStepIsMovable(step) {
    if (step.result !== null) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot move completed step ${step.id}.`);
    }
    if (step.kind === "workflow_ref") {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot move workflow_ref step ${step.id}; use workflowRefChanges when reusable workflow placement changes.`);
    }
    if (step.originSubtreeKey !== undefined ||
        step.originWorkflowRef !== undefined) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot move expanded workflow_ref subtree step ${step.id}; use workflowRefChanges instead.`);
    }
    if ((0, types_1.isWorkflowChildIssueStep)(step) && step.childIssueRef !== null) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot move materialized child issue step ${step.id}.`);
    }
}
function assertControlPlaneOwnedFieldsArePreserved(previousStep, nextStep) {
    if (previousStep.kind === "child_issue" &&
        nextStep.kind === "child_issue" &&
        previousStep.childKey !== nextStep.childKey) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot change childKey for ${previousStep.id}.`);
    }
    if (previousStep.kind === "child_issue" &&
        nextStep.kind === "child_issue" &&
        previousStep.childIssueRef !== (nextStep.childIssueRef ?? null)) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot change childIssueRef for ${previousStep.id}.`);
    }
    if (previousStep.kind === "workflow_ref" &&
        nextStep.kind === "workflow_ref" &&
        (previousStep.callSiteId !== nextStep.callSiteId ||
            previousStep.expansionGroupId !== nextStep.expansionGroupId ||
            previousStep.targetTemplateName !== nextStep.targetTemplateName ||
            previousStep.sourceTemplateName !== nextStep.sourceTemplateName ||
            previousStep.sourceTemplateDigest !== nextStep.sourceTemplateDigest)) {
        throw new WorkflowPlanPatchError(`Workflow plan patch cannot edit workflow_ref control-plane metadata for ${previousStep.id}.`);
    }
}
function validateReplacementStepIds(plan, replacementForStepIds) {
    const stepIds = new Set(plan.steps.map((step) => step.id));
    for (const stepId of replacementForStepIds) {
        if (!stepIds.has(stepId)) {
            throw new WorkflowPlanPatchError(`replacementForStepIds references missing step ${stepId}.`);
        }
    }
}
function resolveOriginWorkflowRef(plan, change) {
    const matchingStep = plan.steps.find((step) => step.originSubtreeKey === change.originSubtreeKey &&
        step.originWorkflowRef?.callSiteId === change.callSiteId &&
        step.originWorkflowRef?.expansionGroupId === change.expansionGroupId);
    if (matchingStep?.originWorkflowRef === undefined) {
        throw new WorkflowPlanPatchError(`Workflow_ref metadata for ${change.expansionGroupId} was not found.`);
    }
    return matchingStep.originWorkflowRef;
}
function assertWorkflowRefSubtreeHasNoDurableEvidence(plan, originSubtreeKey) {
    const matchingSteps = plan.steps.filter((step) => isStepInWorkflowOriginSubtree(step, originSubtreeKey));
    if (matchingSteps.length === 0) {
        throw new WorkflowPlanPatchError(`Workflow_ref subtree ${originSubtreeKey} was not found.`);
    }
    if (matchingSteps.some((step) => step.result !== null)) {
        throw new WorkflowPlanPatchError(`Workflow_ref subtree ${originSubtreeKey} has completed evidence.`);
    }
    const materializedChild = matchingSteps.find((step) => (0, types_1.isWorkflowChildIssueStep)(step) && step.childIssueRef !== null);
    if (materializedChild !== undefined) {
        throw new WorkflowPlanPatchError(`Workflow_ref subtree ${originSubtreeKey} contains materialized child issue step ${materializedChild.id}.`);
    }
}
function validateChildIssueDecisions(input) {
    const childStepByKey = new Map(input.plan.steps.flatMap((step) => step.kind === "child_issue" ? [[step.childKey, step]] : []));
    const removedStepIds = new Set(input.stepChanges.flatMap((change) => change.action === "remove_step" ? [change.stepId] : []));
    const decisionByKey = new Map();
    for (const decision of input.childIssueDecisions) {
        if (decisionByKey.has(decision.childKey)) {
            throw new WorkflowPlanPatchError(`Workflow plan patch has duplicate child issue decision for ${decision.childKey}.`);
        }
        const childStep = childStepByKey.get(decision.childKey);
        if (childStep === undefined || childStep.childIssueRef == null) {
            throw new WorkflowPlanPatchError(`Workflow plan patch child issue decision references missing materialized child ${decision.childKey}.`);
        }
        const childIssueRef = childStep.childIssueRef;
        if (decision.childIssueRef !== undefined &&
            decision.childIssueRef !== null &&
            decision.childIssueRef !== childStep.childIssueRef) {
            throw new WorkflowPlanPatchError(`Workflow plan patch child issue decision for ${decision.childKey} does not match childIssueRef ${childStep.childIssueRef}.`);
        }
        if ((decision.action === "detach_child_issue" ||
            decision.action === "cancel_child_issue") &&
            !removedStepIds.has(childStep.id)) {
            throw new WorkflowPlanPatchError(`${decision.action} for ${decision.childKey} requires remove_step for ${childStep.id}.`);
        }
        if (decision.action === "queue_child_issue_replan" &&
            !hasUpdatedChildStep(input.stepChanges, childStep.id)) {
            throw new WorkflowPlanPatchError(`queue_child_issue_replan for ${decision.childKey} requires update_step for ${childStep.id}.`);
        }
        decisionByKey.set(decision.childKey, {
            ...decision,
            childIssueRef,
        });
    }
    return { decisions: [...decisionByKey.values()] };
}
function hasUpdatedChildStep(stepChanges, stepId) {
    return stepChanges.some((change) => change.action === "update_step" && change.stepId === stepId);
}
function isStepInWorkflowOriginSubtree(step, originSubtreeKey) {
    return (step.originSubtreeKey === originSubtreeKey ||
        step.originSubtreeKey?.startsWith(`${originSubtreeKey}>`) === true);
}
function findStepIndexOrThrow(plan, stepId) {
    const index = plan.steps.findIndex((step) => step.id === stepId);
    if (index < 0) {
        throw new WorkflowPlanPatchError(`Workflow plan patch references missing step ${stepId}.`);
    }
    return index;
}
function requirePlan(plan) {
    if (plan === null) {
        throw new WorkflowPlanPatchError("Workflow plan patch requires a plan.");
    }
    return plan;
}
function validateWorkflowPlanOrThrow(plan) {
    const stepIds = new Set();
    for (const step of plan.steps) {
        if (stepIds.has(step.id)) {
            throw new WorkflowPlanPatchError(`Workflow plan patch produced duplicate step ${step.id}.`);
        }
        stepIds.add(step.id);
    }
    for (const step of plan.steps) {
        for (const dependencyId of step.dependsOnStepIds) {
            if (dependencyId === step.id || !stepIds.has(dependencyId)) {
                throw new WorkflowPlanPatchError(`Workflow plan patch produced invalid dependency ${dependencyId} for ${step.id}.`);
            }
        }
    }
    validateDirectWorkflowRefCallSites(plan);
    if (hasWorkflowPlanDependencyCycle(plan.steps)) {
        throw new WorkflowPlanPatchError("Workflow plan patch produced dependency cycle.");
    }
    return {
        steps: plan.steps.map((step) => (0, plan_support_1.normalizeWorkflowStep)(step)),
    };
}
function validateDirectWorkflowRefCallSites(plan) {
    const workflowRefStepIdByCallSite = new Map();
    for (const step of plan.steps) {
        if (step.kind !== "workflow_ref" || step.originSubtreeKey !== undefined) {
            continue;
        }
        const callSiteKey = [
            step.sourceTemplateName ?? "",
            step.callSiteId ?? "",
            step.expansionGroupId ?? "",
        ].join("\0");
        const previousStepId = workflowRefStepIdByCallSite.get(callSiteKey);
        if (previousStepId !== undefined) {
            throw new WorkflowPlanPatchError(`Workflow plan patch produced duplicate workflow_ref call site ${step.callSiteId ?? step.id} in steps ${previousStepId} and ${step.id}.`);
        }
        workflowRefStepIdByCallSite.set(callSiteKey, step.id);
    }
}
function hasWorkflowPlanDependencyCycle(steps) {
    const dependencyIdsByStepId = new Map(steps.map((step) => [step.id, step.dependsOnStepIds]));
    const visiting = new Set();
    const visited = new Set();
    const visit = (stepId) => {
        if (visited.has(stepId)) {
            return false;
        }
        if (visiting.has(stepId)) {
            return true;
        }
        visiting.add(stepId);
        for (const dependencyId of dependencyIdsByStepId.get(stepId) ?? []) {
            if (visit(dependencyId)) {
                return true;
            }
        }
        visiting.delete(stepId);
        visited.add(stepId);
        return false;
    };
    return steps.some((step) => visit(step.id));
}
function replaceDependencyId(dependencyIds, targetId, replacementIds) {
    return [
        ...new Set(dependencyIds.flatMap((dependencyId) => dependencyId === targetId ? [...replacementIds] : [dependencyId])),
    ];
}
