/*
 * Copyright 2026 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.gradle.features.internal.binding;

import com.google.common.collect.ImmutableSet;
import org.gradle.api.Action;
import org.gradle.api.DomainObjectCollection;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.artifacts.dsl.DependencyFactory;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.internal.AbstractNamedDomainObjectContainer;
import org.gradle.api.internal.DynamicObjectAware;
import org.gradle.api.internal.initialization.ClassLoaderScope;
import org.gradle.api.internal.model.ObjectFactoryFactory;
import org.gradle.api.internal.plugins.PluginManagerInternal;
import org.gradle.api.internal.tasks.TaskDependencyFactory;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.problems.internal.GradleCoreProblemGroup;
import org.gradle.api.problems.internal.ProblemInternal;
import org.gradle.api.problems.internal.ProblemReporterInternal;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.features.binding.BuildModel;
import org.gradle.features.binding.BuildModelRegistrar;
import org.gradle.features.binding.Definition;
import org.gradle.features.binding.ProjectFeatureApplicationContext;
import org.gradle.features.binding.ProjectFeatureApplyAction;
import org.gradle.features.file.ProjectFeatureLayout;
import org.gradle.features.internal.binding.ProjectFeatureSupportInternal.ProjectFeatureDefinitionContext;
import org.gradle.features.internal.file.DefaultProjectFeatureLayout;
import org.gradle.features.internal.registration.DefaultConfigurationRegistrar;
import org.gradle.features.internal.registration.DefaultTaskRegistrar;
import org.gradle.features.registration.ConfigurationRegistrar;
import org.gradle.features.registration.TaskRegistrar;
import org.gradle.internal.Cast;
import org.gradle.internal.instantiation.InstantiatorFactory;
import org.gradle.internal.instantiation.managed.ManagedObjectRegistry;
import org.gradle.internal.logging.text.TreeFormatter;
import org.gradle.internal.reflect.annotations.PropertyAnnotationMetadata;
import org.gradle.internal.reflect.annotations.TypeAnnotationMetadataStore;
import org.gradle.internal.reflect.validation.TypeValidationProblemRenderer;
import org.gradle.internal.service.ServiceLookup;
import org.gradle.internal.service.ServiceLookupException;
import org.gradle.internal.service.UnknownServiceException;
import org.jspecify.annotations.Nullable;

import javax.inject.Inject;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Applies project features to a target object by registering the feature application as a child of the target object (unless
 * configured otherwise) and performing validations.  Application returns the public model object of the feature.  Features
 * are applied only once per target object and always return the same public model object for a given target/feature
 * combination.
 */
@SuppressWarnings("this-escape")
abstract public class DefaultProjectFeatureApplicator implements ProjectFeatureApplicator {
    private final ClassLoaderScope classLoaderScope;
    private final ObjectFactory projectObjectFactory;
    private final ProblemReporterInternal problemReporter;
    private final ServiceLookup allServices;
    private final PropertyWalker propertyWalker = new DefaultPropertyWalker(getTypeAnnotationMetadataStore());
    private final List<FeatureApplication<?, ?>> pendingFeatureApplications = new ArrayList<>();

    @Inject
    public DefaultProjectFeatureApplicator(
        ClassLoaderScope classLoaderScope,
        ObjectFactory projectObjectFactory,
        ProblemReporterInternal problemReporter,
        ServiceLookup allServices
    ) {
        this.classLoaderScope = classLoaderScope;
        this.projectObjectFactory = projectObjectFactory;
        this.problemReporter = problemReporter;
        this.allServices = allServices;
    }

    private <OwnDefinition extends Definition<OwnBuildModel>, OwnBuildModel extends BuildModel> ProjectFeatureDefinitionContext.ChildDefinitionAdditionResult<OwnDefinition, OwnBuildModel> instantiateDefinition(DynamicObjectAware parentDefinition, ProjectFeatureImplementation<OwnDefinition, OwnBuildModel> projectFeature, ProjectFeatureDefinitionContext parentDefinitionContext) {
        return parentDefinitionContext.getOrAddChildDefinition(projectFeature, () -> {
            if (parentDefinition instanceof Project) {
                checkSingleProjectTypeApplication(parentDefinitionContext, projectFeature);
            }

            getPluginManager().apply(projectFeature.getPluginClass());

            return instantiateBoundFeatureObjects(parentDefinition, projectFeature);
        });
    }

    @Override
    public <OwnDefinition extends Definition<OwnBuildModel>, OwnBuildModel extends BuildModel> FeatureApplication<OwnDefinition, OwnBuildModel> registerFeatureApplicationFor(DynamicObjectAware parentDefinition, ProjectFeatureImplementation<OwnDefinition, OwnBuildModel> projectFeature) {
        ProjectFeatureDefinitionContext parentDefinitionContext = ProjectFeatureSupportInternal.getContext(parentDefinition);

        ProjectFeatureDefinitionContext.ChildDefinitionAdditionResult<OwnDefinition, OwnBuildModel> result = instantiateDefinition(parentDefinition, projectFeature, parentDefinitionContext);

        if (result.isNew) {
            pendingFeatureApplications.add(result.featureApplication);
            Plugin<Project> plugin = getPluginManager().getPluginContainer().getPlugin(projectFeature.getPluginClass());
            getModelDefaultsApplicator().applyDefaultsTo(parentDefinition, result.featureApplication.getDefinitionInstance(), new ClassLoaderContextFromScope(classLoaderScope), plugin, projectFeature);
        }

        return result.featureApplication;
    }

    @Override
    public void applyFeatures() {
        pendingFeatureApplications.forEach(FeatureApplication::apply);
    }

    private static <OwnDefinition extends Definition<OwnBuildModel>, OwnBuildModel extends BuildModel> void checkSingleProjectTypeApplication(ProjectFeatureDefinitionContext context, ProjectFeatureImplementation<OwnDefinition, OwnBuildModel> projectFeature) {
        context.childFeatures().keySet().stream().findFirst().ifPresent(projectTypeAlreadyApplied -> {
            throw new IllegalStateException(
                "The project has already applied the '" +
                    projectTypeAlreadyApplied.getFeatureName() +
                    "' project type and is also attempting to apply the '" +
                    projectFeature.getFeatureName() +
                    "' project type.  Only one project type can be applied to a project."
            );
        });
    }

    private <OwnDefinition extends Definition<OwnBuildModel>, OwnBuildModel extends BuildModel> FeatureApplication<OwnDefinition, OwnBuildModel> instantiateBoundFeatureObjects(Object parentDefinition, ProjectFeatureImplementation<OwnDefinition, OwnBuildModel> projectFeature) {
        // Context-specific services for this feature binding
        ServicesForApplyAction featureServices = getContextSpecificServiceLookup(projectFeature);
        ObjectFactory featureObjectFactory = getObjectFactoryFactory().createObjectFactory(featureServices);
        BuildModelRegistrarInternal buildModelRegistrar = new DefaultBuildModelRegistrar(featureObjectFactory, this, getProjectFeatureDeclarations());
        if (featureServices instanceof UnsafeServicesForApplyAction) {
            ((UnsafeServicesForApplyAction) featureServices).setBuildModelRegistrar(buildModelRegistrar);
        }

        // Instantiate the definition and build model objects with the feature-specific object factory
        OwnDefinition definition = featureObjectFactory.newInstance(projectFeature.getDefinitionImplementationType());
        OwnBuildModel buildModelInstance = ProjectFeatureSupportInternal.createBuildModelInstance(featureObjectFactory, projectFeature);
        ProjectFeatureSupportInternal.attachDefinitionContext(definition, buildModelInstance, this, getProjectFeatureDeclarations(), featureObjectFactory);

        // Construct an apply action context with the feature-specific object factory
        ProjectFeatureApplicationContext applyActionContext =
            projectObjectFactory.newInstance(DefaultProjectFeatureApplicationContextInternal.class);

        // bind any nested definitions to build model instances
        bindNestedDefinitions(projectFeature.getDefinitionPublicType(), Cast.uncheckedCast(definition), buildModelRegistrar, projectFeature.getNestedBuildModelTypes());

        return new DefaultFeatureApplication<>(
            projectFeature.getDefinitionImplementationType(),
            definition,
            buildModelInstance,
            parentDefinition,
            applyActionContext,
            projectFeature.getApplyActionFactory().create(featureObjectFactory),
            propertyWalker
        );
    }

    private void bindNestedDefinitions(Class<?> publicType, DynamicObjectAware parent, BuildModelRegistrarInternal buildModelRegistrar, Map<Class<?>, Class<?>> buildModelImplementationTypes) {
        // Must use an anonymous class for config cache compatibility
        propertyWalker.walkProperties(publicType, parent, new PropertyWalker.Visitor() {
            @Override
            public void visit(PropertyAnnotationMetadata propertyMetadata, Object propertyValue) {
                if (Definition.class.isAssignableFrom(propertyMetadata.getDeclaredReturnType().getRawType())) {
                    bindNestedDefinition(propertyValue, buildModelRegistrar, buildModelImplementationTypes);
                }
                if (NamedDomainObjectContainer.class.isAssignableFrom(propertyMetadata.getDeclaredReturnType().getRawType())) {
                    NamedDomainObjectContainer<?> ndoc = Cast.uncheckedCast(propertyValue);
                    Class<?> elementType = ((AbstractNamedDomainObjectContainer<?>) ndoc).getType();
                    if (Definition.class.isAssignableFrom(elementType)) {
                        // Must use an anonymous class for config cache compatibility
                        ndoc.all(new Action<Object>() {
                            @Override
                            public void execute(Object element) {
                                bindNestedDefinition(element, buildModelRegistrar, buildModelImplementationTypes);
                            }
                        });
                    }
                }
            }
        });
    }

    private static void bindNestedDefinition(Object propertyValue, BuildModelRegistrarInternal buildModelRegistrar, Map<Class<?>, Class<?>> buildModelImplementationTypes) {
        Definition<?> nestedDefinition = Cast.uncheckedCast(propertyValue);
        buildModelRegistrar.registerBuildModel(nestedDefinition, buildModelImplementationTypes);
    }

    private <OwnDefinition extends Definition<OwnBuildModel>, OwnBuildModel extends BuildModel> ServicesForApplyAction getContextSpecificServiceLookup(ProjectFeatureImplementation<OwnDefinition, OwnBuildModel> projectFeature) {
        TaskRegistrar taskRegistrar = new DefaultTaskRegistrar(getTaskContainer());
        ProjectFeatureLayout projectFeatureLayout = new DefaultProjectFeatureLayout(getProjectLayout());
        ConfigurationRegistrar configurationRegistrar = new DefaultConfigurationRegistrar(getConfigurationContainer());

        // Construct an object factory that provides the appropriate services during apply action execution
        return projectFeature.getApplyActionSafety() == ProjectFeatureBindingDeclaration.Safety.SAFE
            ? new SafeServicesForApplyAction(allServices, taskRegistrar, projectFeatureLayout, configurationRegistrar, projectFeature.getFeatureName(), problemReporter)
            : new UnsafeServicesForApplyAction(allServices, taskRegistrar, projectFeatureLayout, configurationRegistrar, projectFeature.getFeatureName(), problemReporter);
    }

    @Inject
    abstract protected TaskContainer getTaskContainer();

    @Inject
    abstract protected ProjectLayout getProjectLayout();

    @Inject
    abstract protected ConfigurationContainer getConfigurationContainer();

    @Inject
    abstract protected PluginManagerInternal getPluginManager();

    @Inject
    abstract protected ModelDefaultsApplicator getModelDefaultsApplicator();

    @Inject
    abstract protected ObjectFactoryFactory getObjectFactoryFactory();

    @Inject
    abstract protected ProjectFeatureDeclarations getProjectFeatureDeclarations();

    @Inject
    abstract protected TypeAnnotationMetadataStore getTypeAnnotationMetadataStore();

    /**
     * Walks the public properties of a given object, visiting each property and descending into any properties that are annotated with {@link Nested}.
     *
     * Ignores any properties that are null, although properties whose value is null will be visited (e.g. ignores getFoo() == null, but will visit
     * getFoo().getOrNull() == null)
     */
    private static class DefaultPropertyWalker implements PropertyWalker {
        private final TypeAnnotationMetadataStore typeAnnotationMetadataStore;

        private DefaultPropertyWalker(TypeAnnotationMetadataStore typeAnnotationMetadataStore) {
            this.typeAnnotationMetadataStore = typeAnnotationMetadataStore;
        }

        @Override
        public void walkProperties(Class<?> publicType, Object parent, PropertyWalker.Visitor visitor) {
            typeAnnotationMetadataStore.getTypeAnnotationMetadata(publicType).getPropertiesAnnotationMetadata().forEach(propertyMetadata -> {
                Object propertyValue = propertyMetadata.getPropertyValue(parent);
                if (propertyValue != null) {
                    visitor.visit(propertyMetadata, propertyValue);

                    if (propertyMetadata.isAnnotationPresent(Nested.class)) {
                        walkProperties(propertyMetadata.getDeclaredReturnType().getRawType(), Cast.uncheckedCast(propertyValue), visitor);
                    }
                    if (DomainObjectCollection.class.isAssignableFrom(propertyMetadata.getDeclaredReturnType().getRawType())) {
                        DomainObjectCollection<?> collection = Cast.uncheckedCast(propertyValue);
                        // Must use an anonymous class for config cache compatibility
                        collection.all(new Action<Object>() {
                            @Override
                            public void execute(Object element) {
                                walkProperties(element.getClass(), element, visitor);
                            }
                        });
                    }
                }
            });
        }
    }

    private interface PropertyWalker {
        void walkProperties(Class<?> publicType, Object parent, Visitor visitor);
        interface Visitor {
            void visit(PropertyAnnotationMetadata propertyMetadata, Object propertyValue);
        }
    }

    private static class DefaultFeatureApplication<OwnDefinition extends Definition<OwnBuildModel>, OwnBuildModel extends BuildModel> implements FeatureApplication<OwnDefinition, OwnBuildModel> {
        // We don't want to store the Project object because of configuration cache compatibility, so we use a placeholder
        private static final Object PROJECT = new Object();
        private final Class<? extends OwnDefinition> definitionImplementationType;
        private final OwnDefinition definitionInstance;
        private final OwnBuildModel buildModelInstance;
        private final Object parentDefinition;
        private final ProjectFeatureApplicationContext context;
        private final ProjectFeatureApplyAction<OwnDefinition, OwnBuildModel, ?> applyAction;
        private final PropertyWalker propertyWalker;
        private final boolean isProjectType;
        private boolean applied;

        public DefaultFeatureApplication(Class<? extends OwnDefinition> definitionImplementationType, OwnDefinition definitionInstance, OwnBuildModel buildModelInstance, Object parentDefinition, ProjectFeatureApplicationContext context, ProjectFeatureApplyAction<OwnDefinition, OwnBuildModel, ?> applyAction, PropertyWalker propertyWalker) {
            this.definitionImplementationType = definitionImplementationType;
            this.definitionInstance = definitionInstance;
            this.buildModelInstance = buildModelInstance;
            this.isProjectType = parentDefinition instanceof Project;
            this.parentDefinition = isProjectType ? PROJECT : parentDefinition;
            this.context = context;
            this.applyAction = applyAction;
            this.propertyWalker = propertyWalker;
        }

        @Override
        public OwnDefinition getDefinitionInstance() {
            return definitionInstance;
        }

        @Override
        public boolean isProjectType() {
            return isProjectType;
        }

        @Override
        public void apply() {
            if (!applied) {
                finalizeProperties();
                applyAction.apply(context, definitionInstance, buildModelInstance, Cast.uncheckedCast(parentDefinition));
                applied = true;
            }
        }

        private void finalizeProperties() {
            // // Must use an anonymous class for config cache compatibility
            propertyWalker.walkProperties(definitionImplementationType, definitionInstance, new PropertyWalker.Visitor() {
                @Override
                public void visit(PropertyAnnotationMetadata propertyMetadata, Object propertyValue) {
                    if (Property.class.isAssignableFrom(propertyMetadata.getDeclaredReturnType().getRawType())) {
                        ((Property<?>) propertyValue).finalizeValue();
                    }
                    if (NamedDomainObjectContainer.class.isAssignableFrom(propertyMetadata.getDeclaredReturnType().getRawType())) {
                        NamedDomainObjectContainer<?> ndoc = Cast.uncheckedCast(propertyValue);
                        ndoc.disallowChanges();
                    }
                }
            });
        }
    }

    /**
     * The internal implementation of the context passed to project feature apply actions, exposing an object factory
     * appropriate for the configured safety of the apply action.
     */
    abstract static class DefaultProjectFeatureApplicationContextInternal implements ProjectFeatureApplicationContext {

        @Inject
        @SuppressWarnings("Unused")
        public DefaultProjectFeatureApplicationContextInternal() { }

        @Override
        public <T extends Definition<V>, V extends BuildModel> V getBuildModel(T definition) {
            return Cast.uncheckedNonnullCast(ProjectFeatureSupportInternal.getContext((DynamicObjectAware) definition).getBuildModel());
        }
    }

    private static class ClassLoaderContextFromScope implements ModelDefaultsApplicator.ClassLoaderContext {
        private final ClassLoaderScope scope;

        public ClassLoaderContextFromScope(ClassLoaderScope scope) {
            this.scope = scope;
        }

        @Override
        public ClassLoader getClassLoader() {
            return scope.getLocalClassLoader();
        }

        @Override
        public ClassLoader getParentClassLoader() {
            return scope.getParent().getLocalClassLoader();
        }
    }

    /**
     * The set of services that are considered safe for use during safe apply actions.
     */
    private static final Set<Class<?>> SAFE_APPLY_ACTION_SERVICES = ImmutableSet.of(
        TaskRegistrar.class,
        ProjectFeatureLayout.class,
        ConfigurationRegistrar.class,
        ObjectFactory.class,
        ProviderFactory.class,
        DependencyFactory.class
    );

    /**
     * A base class for limited service lookups for use during feature apply actions.  Provides context-specific services
     * unique to the feature binding as well as a small set of services used by Gradle internals.
     */
    private abstract static class ServicesForApplyAction implements ServiceLookup {

        protected final ServiceLookup allServices;
        protected final TaskRegistrar taskRegistrar;
        protected final ProjectFeatureLayout projectFeatureLayout;
        protected final ConfigurationRegistrar configurationRegistrar;

        private ServicesForApplyAction(ServiceLookup allServices, TaskRegistrar taskRegistrar, ProjectFeatureLayout projectFeatureLayout, ConfigurationRegistrar configurationRegistrar) {
            this.allServices = allServices;
            this.taskRegistrar = taskRegistrar;
            this.projectFeatureLayout = projectFeatureLayout;
            this.configurationRegistrar = configurationRegistrar;
        }

        @Override
        public @Nullable Object find(Type serviceType) throws ServiceLookupException {
            if (serviceType instanceof Class) {
                Class<?> serviceClass = Cast.uncheckedNonnullCast(serviceType);
                // Context-specific services unique to this feature binding
                if (serviceClass.isAssignableFrom(TaskRegistrar.class)) {
                    return taskRegistrar;
                }
                if (serviceClass.isAssignableFrom(ProjectFeatureLayout.class)) {
                    return projectFeatureLayout;
                }
                if (serviceClass.isAssignableFrom(ConfigurationRegistrar.class)) {
                    return configurationRegistrar;
                }
                // Services used by Gradle internals
                if (serviceClass.isAssignableFrom(ManagedObjectRegistry.class)) {
                    return allServices.find(ManagedObjectRegistry.class);
                }
                if (serviceClass.isAssignableFrom(ProjectFeatureDeclarations.class)) {
                    return allServices.find(ProjectFeatureDeclarations.class);
                }
                if (serviceClass.isAssignableFrom(ProjectFeatureApplicator.class)) {
                    return allServices.find(ProjectFeatureApplicator.class);
                }
                if (serviceClass.isAssignableFrom(TaskDependencyFactory.class)) {
                    return allServices.find(TaskDependencyFactory.class);
                }
                if (serviceClass.isAssignableFrom(InstantiatorFactory.class)) {
                    return allServices.find(InstantiatorFactory.class);
                }
                // None of the above
                return null;
            }
            return null;
        }

        @Override
        public Object get(Type serviceType) throws UnknownServiceException, ServiceLookupException {
            Object result = find(serviceType);
            if (result == null) {
                return notFound(serviceType);
            }
            return result;
        }

        @Override
        public Object get(Type serviceType, Class<? extends Annotation> annotatedWith) throws UnknownServiceException, ServiceLookupException {
            return notFound(serviceType);
        }

        protected abstract Object notFound(Type serviceType);
    }

    /**
     * A limited service lookup for use during feature apply actions, exposing both safe and unsafe services.
     */
    private static class UnsafeServicesForApplyAction extends ServicesForApplyAction {
        private final String featureName;
        private final ProblemReporterInternal problemReporter; // Not used in this class
        private BuildModelRegistrarInternal buildModelRegistrar; // set after construction to share ObjectFactory created with this instance

        public UnsafeServicesForApplyAction(ServiceLookup allServices, TaskRegistrar taskRegistrar, ProjectFeatureLayout projectFeatureLayout, ConfigurationRegistrar configurationRegistrar, String featureName, ProblemReporterInternal problemReporter) {
            super(allServices, taskRegistrar, projectFeatureLayout, configurationRegistrar);
            this.featureName = featureName;
            this.problemReporter = problemReporter;
        }

        void setBuildModelRegistrar(BuildModelRegistrarInternal buildModelRegistrar) {
            this.buildModelRegistrar = buildModelRegistrar;
        }

        @Override
        public @Nullable Object find(Type serviceType) throws ServiceLookupException {
            Object found = super.find(serviceType);
            if (found != null) {
                return found;
            }

            // Context-specific service for build model registration, only available in unsafe apply actions
            if (serviceType instanceof Class) {
                Class<?> serviceClass = Cast.uncheckedNonnullCast(serviceType);
                if (serviceClass.isAssignableFrom(BuildModelRegistrar.class)) {
                    return buildModelRegistrar;
                }
            }

            // If not found in the safe/limited set, allow lookup from all services
            return allServices.find(serviceType);
        }

        @Override
        protected Object notFound(Type serviceType) {
            ProblemInternal problem = problemReporter.internalCreate(builder -> builder
                .id("unsafe-apply-action-uses-unknown-service", "An unsafe apply action is attempting to use an unknown service", GradleCoreProblemGroup.configurationUsage())
                .contextualLabel("Project feature '" + featureName + "' has an apply action that attempts to inject an unknown service with type '" + serviceType.getTypeName() + "'.")
                .details("Services of type " + serviceType.getTypeName() + " are not available for injection into project feature apply actions.")
                .solution("Remove the '" + serviceType.getTypeName() + "' injection from the apply action.")
            );
            problemReporter.reportError(problem);
            throw new UnknownServiceException(serviceType, TypeValidationProblemRenderer.renderMinimalInformationAbout(problem));
        }
    }

    /**
     * A limited service lookup for use during safe feature apply actions, exposing only a small set of safe services.
     */
    private static class SafeServicesForApplyAction extends ServicesForApplyAction {
        private final String featureName;
        private final ProblemReporterInternal problemReporter;

        public SafeServicesForApplyAction(ServiceLookup allServices, TaskRegistrar taskRegistrar, ProjectFeatureLayout projectFeatureLayout, ConfigurationRegistrar configurationRegistrar, String featureName, ProblemReporterInternal problemReporter) {
            super(allServices, taskRegistrar, projectFeatureLayout, configurationRegistrar);
            this.featureName = featureName;
            this.problemReporter = problemReporter;
        }

        @Override
        public @Nullable Object find(Type serviceType) throws ServiceLookupException {
            Object found = super.find(serviceType);
            if (found != null) {
                return found;
            }

            // Only allow access to a small set of safe services from the parent services
            Class<?> serviceClass = Cast.uncheckedNonnullCast(serviceType);
            for (Class<?> safeService : SAFE_APPLY_ACTION_SERVICES) {
                if (serviceClass.isAssignableFrom(safeService)) {
                    return allServices.find(serviceType);
                }
            }

            return null;
        }

        private static String getSafeServicesListExplanation() {
            TreeFormatter formatter = new TreeFormatter(true)
                .node("Only the following services are available in safe apply actions");
            formatter.startChildren();
            for (Class<?> safeService : SAFE_APPLY_ACTION_SERVICES) {
                formatter.node("").appendType(safeService);
            }
            formatter.endChildren();
            return formatter.toString();
        }

        @Override
        protected Object notFound(Type serviceType) {
            ProblemInternal problem = problemReporter.internalCreate(builder -> builder
                .id("safe-apply-action-uses-unsafe-service", "A safe apply action is attempting to use an unsafe service", GradleCoreProblemGroup.configurationUsage())
                .contextualLabel("Project feature '" + featureName + "' has a safe apply action that attempts to inject an unsafe service with type '" + serviceType.getTypeName() + "'.")
                .details(getSafeServicesListExplanation())
                .solution("Mark the apply action as unsafe.")
                .solution("Remove the '" + serviceType.getTypeName() + "' injection from the apply action.")
            );
            problemReporter.reportError(problem);
            throw new UnknownServiceException(serviceType, TypeValidationProblemRenderer.renderMinimalInformationAbout(problem));
        }
    }
}
