ClosedSourceParameterMigrationPlan.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.sandbox.jdt.container.api;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.ArgumentTransfer;

/**
 * Immutable aggregate plan for one closed-source local-array to parameter-list
 * migration.
 *
 * <p>One local source may feed a complete source-resolved parameter atomicity group,
 * including interface declarations and all editable implementations/overrides. The
 * unchanged call expressions are tied to exact target method handles and parameter
 * indices. Every local and parameter member is re-resolved before the existing
 * coordinated cleanup lifecycle emits edits.</p>
 */
public record ClosedSourceParameterMigrationPlan(
		TargetContainerContract targetContract,
		ContainerLocalRewritePlan callerPlan,
		List<ContainerParameterRewritePlan> parameterPlans) {

	/** Compatibility constructor for the existing single-parameter vertical slice. */
	public ClosedSourceParameterMigrationPlan(
			TargetContainerContract targetContract,
			ContainerLocalRewritePlan callerPlan,
			ContainerParameterRewritePlan parameterPlan) {
		this(targetContract, callerPlan, List.of(parameterPlan));
	}

	public ClosedSourceParameterMigrationPlan {
		Objects.requireNonNull(targetContract, "targetContract"); //$NON-NLS-1$
		Objects.requireNonNull(callerPlan, "callerPlan"); //$NON-NLS-1$
		parameterPlans= List.copyOf(
				Objects.requireNonNull(parameterPlans, "parameterPlans")); //$NON-NLS-1$
		if (parameterPlans.isEmpty()) {
			throw new IllegalArgumentException(
					"A closed-source parameter migration requires parameter rewrites"); //$NON-NLS-1$
		}
		if (!targetContract.equals(callerPlan.targetContract())) {
			throw new IllegalArgumentException(
					"All closed-source migration members must share one target contract"); //$NON-NLS-1$
		}
		for (ContainerParameterRewritePlan parameterPlan : parameterPlans) {
			if (!targetContract.equals(parameterPlan.targetContract())) {
				throw new IllegalArgumentException(
						"All closed-source migration members must share one target contract"); //$NON-NLS-1$
			}
		}
		validateUniqueParameterTargets(parameterPlans);
		validateArgumentTransfers(callerPlan, parameterPlans);
	}

	/** Returns affected compilation-unit handles in deterministic execution order. */
	public List<String> affectedCompilationUnitHandles() {
		Set<String> handles= LinkedHashSet.newLinkedHashSet(parameterPlans.size() + 1);
		handles.add(callerPlan.compilationUnitHandle());
		for (ContainerParameterRewritePlan parameterPlan : parameterPlans) {
			handles.add(parameterPlan.compilationUnitHandle());
		}
		return List.copyOf(handles);
	}

	/** Planning result retaining fail-closed aggregate diagnostics. */
	public record PlanningResult(
			Optional<ClosedSourceParameterMigrationPlan> plan,
			List<PlanningDiagnostic> diagnostics) {

		public PlanningResult {
			plan= Objects.requireNonNull(plan, "plan"); //$NON-NLS-1$
			diagnostics= List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); //$NON-NLS-1$
			if (plan.isPresent() == !diagnostics.isEmpty()) {
				throw new IllegalArgumentException(
						"An aggregate result must contain either one plan or diagnostics"); //$NON-NLS-1$
			}
		}

		public static PlanningResult accepted(
				ClosedSourceParameterMigrationPlan plan) {
			return new PlanningResult(Optional.of(plan), List.of());
		}

		public static PlanningResult rejected(
				List<PlanningDiagnostic> diagnostics) {
			if (diagnostics.isEmpty()) {
				throw new IllegalArgumentException(
						"A rejected aggregate migration requires diagnostics"); //$NON-NLS-1$
			}
			return new PlanningResult(Optional.empty(), diagnostics);
		}

		public boolean ready() {
			return plan.isPresent();
		}
	}

	/** One aggregate eligibility or delegated member diagnostic. */
	public record PlanningDiagnostic(DiagnosticKind kind, String message) {

		public PlanningDiagnostic {
			Objects.requireNonNull(kind, "kind"); //$NON-NLS-1$
			message= requiredText(message, "message"); //$NON-NLS-1$
		}
	}

	public enum DiagnosticKind {
		UNSUPPORTED_FLOW_TOPOLOGY,
		PROFILE_NOT_FOUND,
		RECOMMENDATION_MISMATCH,
		SIGNATURE_PLAN_MISMATCH,
		LOCAL_REWRITE_REJECTED,
		PARAMETER_REWRITE_REJECTED
	}

	private static void validateUniqueParameterTargets(
			List<ContainerParameterRewritePlan> parameterPlans) {
		Set<ParameterTarget> targets= HashSet.newHashSet(parameterPlans.size());
		for (ContainerParameterRewritePlan parameterPlan : parameterPlans) {
			ParameterTarget target= new ParameterTarget(
					parameterPlan.methodJavaElementHandle(),
					parameterPlan.parameterIndex());
			if (!targets.add(target)) {
				throw new IllegalArgumentException(
						"Closed-source parameter targets must be unique: " + target); //$NON-NLS-1$
			}
		}
	}

	private static void validateArgumentTransfers(
			ContainerLocalRewritePlan caller,
			List<ContainerParameterRewritePlan> parameterPlans) {
		if (caller.argumentTransfers().isEmpty()) {
			throw new IllegalArgumentException(
					"A closed-source parameter migration requires an exact argument transfer"); //$NON-NLS-1$
		}
		Set<ParameterTarget> targets= HashSet.newHashSet(parameterPlans.size());
		for (ContainerParameterRewritePlan parameterPlan : parameterPlans) {
			targets.add(new ParameterTarget(
					parameterPlan.methodJavaElementHandle(),
					parameterPlan.parameterIndex()));
		}
		for (ArgumentTransfer transfer : caller.argumentTransfers()) {
			ParameterTarget target= new ParameterTarget(
					transfer.methodJavaElementHandle(),
					transfer.parameterIndex());
			if (!targets.contains(target)) {
				throw new IllegalArgumentException(
						"Caller argument target has no matching parameter rewrite: " + target); //$NON-NLS-1$
			}
		}
	}

	private static String requiredText(String value, String fieldName) {
		String text= Objects.requireNonNull(value, fieldName).strip();
		if (text.isEmpty()) {
			throw new IllegalArgumentException(fieldName + " must not be empty"); //$NON-NLS-1$
		}
		return text;
	}

	private record ParameterTarget(String methodJavaElementHandle, int parameterIndex) {
		private ParameterTarget {
			methodJavaElementHandle= requiredText(
					methodJavaElementHandle, "methodJavaElementHandle"); //$NON-NLS-1$
			if (parameterIndex < 0) {
				throw new IllegalArgumentException(
						"parameterIndex must not be negative"); //$NON-NLS-1$
			}
		}
	}
}