/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.maven.project;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.maven.ProjectCycleException;
import org.apache.maven.RepositoryUtils;
import org.apache.maven.api.ArtifactCoordinates;
import org.apache.maven.api.Language;
import org.apache.maven.api.LocalRepository;
import org.apache.maven.api.ProjectScope;
import org.apache.maven.api.SessionData;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.DependencyManagement;
import org.apache.maven.api.model.DeploymentRepository;
import org.apache.maven.api.model.Extension;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.model.Profile;
import org.apache.maven.api.model.ReportPlugin;
import org.apache.maven.api.model.Resource;
import org.apache.maven.api.services.ArtifactResolver;
import org.apache.maven.api.services.ArtifactResolverException;
import org.apache.maven.api.services.ArtifactResolverRequest;
import org.apache.maven.api.services.ArtifactResolverResult;
import org.apache.maven.api.services.BuilderProblem.Severity;
import org.apache.maven.api.services.ModelBuilder;
import org.apache.maven.api.services.ModelBuilderException;
import org.apache.maven.api.services.ModelBuilderRequest;
import org.apache.maven.api.services.ModelBuilderResult;
import org.apache.maven.api.services.ModelProblem;
import org.apache.maven.api.services.ModelProblem.Version;
import org.apache.maven.api.services.ModelProblemCollector;
import org.apache.maven.api.services.ModelSource;
import org.apache.maven.api.services.ModelTransformer;
import org.apache.maven.api.services.ProblemCollector;
import org.apache.maven.api.services.Source;
import org.apache.maven.api.services.Sources;
import org.apache.maven.api.services.model.LifecycleBindingsInjector;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.InvalidRepositoryException;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.bridge.MavenRepositorySystem;
import org.apache.maven.impl.DefaultSourceRoot;
import org.apache.maven.impl.InternalSession;
import org.apache.maven.impl.resolver.ArtifactDescriptorUtils;
import org.apache.maven.internal.impl.InternalMavenSession;
import org.apache.maven.model.building.DefaultModelProblem;
import org.apache.maven.model.building.FileModelSource;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.model.building.ModelSource2;
import org.apache.maven.model.root.RootLocator;
import org.apache.maven.plugin.PluginManagerException;
import org.apache.maven.plugin.PluginResolutionException;
import org.apache.maven.plugin.version.PluginVersionResolutionException;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.LocalRepositoryManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * DefaultProjectBuilder
 */
@Named
@Singleton
public class DefaultProjectBuilder implements ProjectBuilder {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final ModelBuilder modelBuilder;
    private final ProjectBuildingHelper projectBuildingHelper;
    private final MavenRepositorySystem repositorySystem;
    private final ProjectDependenciesResolver dependencyResolver;
    private final RootLocator rootLocator;
    private final LifecycleBindingsInjector lifecycleBindingsInjector;

    @SuppressWarnings("checkstyle:ParameterNumber")
    @Inject
    public DefaultProjectBuilder(
            ModelBuilder modelBuilder,
            ProjectBuildingHelper projectBuildingHelper,
            MavenRepositorySystem repositorySystem,
            RepositorySystem repoSystem,
            ProjectDependenciesResolver dependencyResolver,
            RootLocator rootLocator,
            LifecycleBindingsInjector lifecycleBindingsInjector) {
        this.modelBuilder = modelBuilder;
        this.projectBuildingHelper = projectBuildingHelper;
        this.repositorySystem = repositorySystem;
        this.dependencyResolver = dependencyResolver;
        this.rootLocator = rootLocator;
        this.lifecycleBindingsInjector = lifecycleBindingsInjector;
    }
    // ----------------------------------------------------------------------
    // MavenProjectBuilder Implementation
    // ----------------------------------------------------------------------

    @Override
    public ProjectBuildingResult build(File pomFile, ProjectBuildingRequest request) throws ProjectBuildingException {
        try (BuildSession bs = new BuildSession(request)) {
            Path path = pomFile.toPath();
            return bs.build(false, path, Sources.buildSource(path));
        }
    }

    @Deprecated
    @Override
    public ProjectBuildingResult build(
            org.apache.maven.model.building.ModelSource modelSource, ProjectBuildingRequest request)
            throws ProjectBuildingException {
        return build(toSource(modelSource), request);
    }

    @Deprecated
    static ModelSource toSource(org.apache.maven.model.building.ModelSource modelSource) {
        if (modelSource instanceof FileModelSource fms) {
            return Sources.buildSource(fms.getPath());
        } else {
            return new WrapModelSource(modelSource);
        }
    }

    @Override
    public ProjectBuildingResult build(ModelSource modelSource, ProjectBuildingRequest request)
            throws ProjectBuildingException {
        try (BuildSession bs = new BuildSession(request)) {
            return bs.build(false, null, modelSource);
        }
    }

    @Override
    public ProjectBuildingResult build(Artifact artifact, ProjectBuildingRequest request)
            throws ProjectBuildingException {
        return build(artifact, false, request);
    }

    @Override
    public ProjectBuildingResult build(Artifact artifact, boolean allowStubModel, ProjectBuildingRequest request)
            throws ProjectBuildingException {
        try (BuildSession bs = new BuildSession(request)) {
            return bs.build(false, artifact, allowStubModel);
        }
    }

    @Override
    public List<ProjectBuildingResult> build(List<File> pomFiles, boolean recursive, ProjectBuildingRequest request)
            throws ProjectBuildingException {
        try (BuildSession bs = new BuildSession(request)) {
            return bs.build(pomFiles, recursive);
        }
    }

    private static class StubModelSource implements ModelSource {
        private final String xml;
        private final Artifact artifact;

        StubModelSource(String xml, Artifact artifact) {
            this.xml = xml;
            this.artifact = artifact;
        }

        @Override
        @Nullable
        public ModelSource resolve(@Nonnull ModelLocator modelLocator, @Nonnull String relative) {
            return null;
        }

        @Override
        @Nullable
        public Path getPath() {
            return null;
        }

        @Override
        @Nonnull
        public InputStream openStream() throws IOException {
            return new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
        }

        @Override
        @Nonnull
        public String getLocation() {
            return artifact.getId();
        }

        @Override
        @Nullable
        public Source resolve(@Nonnull String relative) {
            return null;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            StubModelSource that = (StubModelSource) o;
            return Objects.equals(xml, that.xml) && Objects.equals(artifact, that.artifact);
        }

        @Override
        public int hashCode() {
            return Objects.hash(xml, artifact);
        }
    }

    private static class WrapModelSource implements ModelSource {
        private final org.apache.maven.model.building.ModelSource modelSource;

        WrapModelSource(org.apache.maven.model.building.ModelSource modelSource) {
            this.modelSource = modelSource;
        }

        @Override
        @Nullable
        public ModelSource resolve(@Nonnull ModelLocator modelLocator, @Nonnull String relative) {
            return null;
        }

        @Override
        @Nullable
        public Path getPath() {
            return null;
        }

        @Override
        @Nonnull
        public InputStream openStream() throws IOException {
            return modelSource.getInputStream();
        }

        @Override
        @Nonnull
        public String getLocation() {
            return modelSource.getLocation();
        }

        @Override
        @Nullable
        public Source resolve(@Nonnull String relative) {
            if (modelSource instanceof ModelSource2 ms) {
                return toSource(ms.getRelatedSource(relative));
            } else {
                return null;
            }
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            WrapModelSource that = (WrapModelSource) o;
            return Objects.equals(modelSource, that.modelSource);
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(modelSource);
        }
    }

    class BuildSession implements AutoCloseable {
        private final ProjectBuildingRequest request;
        private final InternalSession session;
        private final ModelBuilder.ModelBuilderSession modelBuilderSession;
        private final Map<String, MavenProject> projectIndex = new ConcurrentHashMap<>(256);

        BuildSession(ProjectBuildingRequest request) {
            this.request = request;
            InternalSession session = InternalSession.from(request.getRepositorySession());
            Path basedir = request.getLocalRepository() != null
                    ? request.getLocalRepository().getBasedirPath()
                    : null;
            if (basedir != null) {
                LocalRepository localRepository = session.createLocalRepository(basedir);
                session = InternalSession.from(session.withLocalRepository(localRepository));
            }
            this.session = session;
            this.modelBuilderSession = modelBuilder.newSession();
            // Save the ModelBuilderSession for later retrieval by the DefaultConsumerPomBuilder.
            // Use replace(key, null, value) to make sure the *main* session, i.e. the one used
            // to load the projects, is stored. This is to avoid the session being overwritten
            // if a plugin uses the ProjectBuilder.
            this.session
                    .getData()
                    .replace(SessionData.key(ModelBuilder.ModelBuilderSession.class), null, modelBuilderSession);
        }

        @Override
        public void close() {}

        ProjectBuildingResult build(boolean parent, Path pomFile, ModelSource modelSource)
                throws ProjectBuildingException {
            ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader();

            try {
                MavenProject project = request.getProject();

                ProblemCollector<ModelProblem> problemCollector = null;
                Throwable error = null;

                if (project == null) {
                    project = new MavenProject();
                    project.setFile(pomFile != null ? pomFile.toFile() : null);

                    boolean reactorMember = pomFile != null
                            && session.getProjects() != null // this is for UTs
                            && session.getProjects().stream()
                                    .anyMatch(
                                            p -> p.getPomPath().toAbsolutePath().equals(pomFile.toAbsolutePath()));
                    boolean isStandalone = pomFile == null
                            && modelSource != null
                            && modelSource.getLocation().startsWith("jar:")
                            && modelSource.getLocation().endsWith("/org/apache/maven/project/standalone.xml");

                    ModelBuilderRequest.ModelBuilderRequestBuilder builder = getModelBuildingRequest();
                    ModelBuilderRequest.RequestType type = reactorMember
                                    || isStandalone
                                    || (pomFile != null
                                            && this.request.isProcessPlugins()
                                            && this.request.getValidationLevel()
                                                    == ModelBuildingRequest.VALIDATION_LEVEL_STRICT)
                            ? ModelBuilderRequest.RequestType.BUILD_EFFECTIVE
                            : (parent
                                    ? ModelBuilderRequest.RequestType.CONSUMER_PARENT
                                    : ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY);
                    MavenProject theProject = project;
                    ModelBuilderRequest request = builder.source(modelSource)
                            .requestType(type)
                            .locationTracking(true)
                            .lifecycleBindingsInjector(
                                    (m, r, p) -> injectLifecycleBindings(m, r, p, theProject, this.request))
                            .build();

                    if (pomFile != null) {
                        project.setRootDirectory(rootLocator.findRoot(pomFile.getParent()));
                    }

                    ModelBuilderResult result;
                    try {
                        result = modelBuilderSession.build(request);
                    } catch (ModelBuilderException e) {
                        result = e.getResult();
                        if (result == null || result.getEffectiveModel() == null) {
                            throw new ProjectBuildingException(
                                    e.getModelId(), e.getMessage(), pomFile != null ? pomFile.toFile() : null, e);
                        }
                        // validation error, continue project building and delay failing to help IDEs
                        error = e;
                    }

                    problemCollector = result.getProblemCollector();

                    initProject(project, result);
                }

                DependencyResolutionResult resolutionResult = null;

                if (request.isResolveDependencies()) {
                    projectBuildingHelper.selectProjectRealm(project);
                    resolutionResult = resolveDependencies(project);
                }

                ProjectBuildingResult result =
                        new DefaultProjectBuildingResult(project, convert(problemCollector), resolutionResult);

                if (error != null) {
                    ProjectBuildingException e = new ProjectBuildingException(List.of(result));
                    e.initCause(error);
                    throw e;
                }

                return result;
            } finally {
                Thread.currentThread().setContextClassLoader(oldContextClassLoader);
            }
        }

        ProjectBuildingResult build(boolean parent, Artifact artifact, boolean allowStubModel)
                throws ProjectBuildingException {
            org.eclipse.aether.artifact.Artifact pomArtifact = RepositoryUtils.toArtifact(artifact);
            pomArtifact = ArtifactDescriptorUtils.toPomArtifact(pomArtifact);

            boolean localProject;

            try {
                ArtifactCoordinates coordinates = session.createArtifactCoordinates(session.getArtifact(pomArtifact));
                ArtifactResolverRequest req = ArtifactResolverRequest.builder()
                        .session(session)
                        .repositories(request.getRemoteRepositories().stream()
                                .map(RepositoryUtils::toRepo)
                                .map(session::getRemoteRepository)
                                .toList())
                        .coordinates(List.of(coordinates))
                        .build();
                ArtifactResolverResult res =
                        session.getService(ArtifactResolver.class).resolve(req);
                ArtifactResolverResult.ResultItem resItem = res.getResult(coordinates);

                pomArtifact = InternalMavenSession.from(session).toArtifact(resItem.getArtifact());
                localProject = resItem.getRepository() instanceof org.apache.maven.api.WorkspaceRepository;
            } catch (ArtifactResolverException e) {
                if (e.getResult().getResults().values().iterator().next().isMissing() && allowStubModel) {
                    return build(parent, null, createStubModelSource(artifact));
                }
                throw new ProjectBuildingException(
                        artifact.getId(), "Error resolving project artifact: " + e.getMessage(), e);
            }

            Path pomFile = pomArtifact.getPath();

            if (!artifact.isResolved() && "pom".equals(artifact.getType())) {
                artifact.selectVersion(pomArtifact.getVersion());
                artifact.setFile(pomFile.toFile());
                artifact.setResolved(true);
            }

            if (localProject) {
                return build(parent, pomFile, Sources.buildSource(pomFile));
            } else {
                return build(
                        parent,
                        null,
                        Sources.resolvedSource(
                                pomFile,
                                artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getVersion()));
            }
        }

        List<ProjectBuildingResult> build(List<File> pomFiles, boolean recursive) throws ProjectBuildingException {
            List<ProjectBuildingResult> results = doBuild(pomFiles, recursive);
            if (results.stream()
                    .flatMap(r -> r.getProblems().stream())
                    .anyMatch(p -> p.getSeverity() != org.apache.maven.model.building.ModelProblem.Severity.WARNING)) {
                org.apache.maven.model.building.ModelProblem cycle = results.stream()
                        .flatMap(r -> r.getProblems().stream())
                        .filter(p -> p.getException() instanceof CycleDetectedException)
                        .findAny()
                        .orElse(null);
                if (cycle != null) {
                    throw new RuntimeException(new ProjectCycleException(
                            "The projects in the reactor contain a cyclic reference: " + cycle.getMessage(),
                            (CycleDetectedException) cycle.getException()));
                }
                throw new ProjectBuildingException(results);
            }

            return results;
        }

        List<ProjectBuildingResult> doBuild(List<File> pomFiles, boolean recursive) {
            ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader();
            try {
                return pomFiles.stream()
                        .map(pomFile -> build(pomFile, recursive))
                        .flatMap(List::stream)
                        .collect(Collectors.toList());
            } finally {
                Thread.currentThread().setContextClassLoader(oldContextClassLoader);
            }
        }

        @SuppressWarnings("checkstyle:parameternumber")
        private List<ProjectBuildingResult> build(File pomFile, boolean recursive) {
            ModelBuilderResult result;
            try {
                ModelTransformer injector = (m, r, p) -> {
                    MavenProject project = projectIndex.computeIfAbsent(m.getId(), f -> new MavenProject());
                    return injectLifecycleBindings(m, r, p, project, request);
                };
                ModelBuilderRequest modelBuildingRequest = getModelBuildingRequest()
                        .source(Sources.buildSource(pomFile.toPath()))
                        .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
                        .locationTracking(true)
                        .recursive(recursive)
                        .lifecycleBindingsInjector(injector)
                        .build();
                result = modelBuilderSession.build(modelBuildingRequest);
            } catch (ModelBuilderException e) {
                result = e.getResult();
                if (result == null || result.getEffectiveModel() == null) {
                    return List.of(new DefaultProjectBuildingResult(
                            e.getModelId(), pomFile, convert(e.getProblemCollector())));
                }
            }

            List<ProjectBuildingResult> results = new ArrayList<>();
            List<ModelBuilderResult> allModels = results(result).toList();
            for (ModelBuilderResult r : allModels) {
                if (r.getEffectiveModel() != null) {
                    File pom = r.getSource().getPath().toFile();
                    MavenProject project =
                            projectIndex.get(r.getEffectiveModel().getId());
                    Path rootDirectory =
                            rootLocator.findRoot(pom.getParentFile().toPath());
                    project.setRootDirectory(rootDirectory);
                    project.setFile(pom);
                    project.setExecutionRoot(pom.equals(pomFile));
                    initProject(project, r);
                    project.setCollectedProjects(results(r)
                            .filter(cr -> cr != r && cr.getEffectiveModel() != null)
                            .map(cr -> projectIndex.get(cr.getEffectiveModel().getId()))
                            .collect(Collectors.toList()));

                    DependencyResolutionResult resolutionResult = null;
                    if (request.isResolveDependencies()) {
                        resolutionResult = resolveDependencies(project);
                    }
                    results.add(new DefaultProjectBuildingResult(
                            project, convert(r.getProblemCollector()), resolutionResult));
                } else {
                    results.add(new DefaultProjectBuildingResult(null, convert(r.getProblemCollector()), null));
                }
            }
            return results;
        }

        private Stream<ModelBuilderResult> results(ModelBuilderResult result) {
            return Stream.concat(result.getChildren().stream().flatMap(this::results), Stream.of(result));
        }

        private List<org.apache.maven.model.building.ModelProblem> convert(
                ProblemCollector<ModelProblem> problemCollector) {
            if (problemCollector == null) {
                return null;
            }
            ArrayList<org.apache.maven.model.building.ModelProblem> problems = new ArrayList<>();
            problemCollector.problems().map(BuildSession::convert).forEach(problems::add);
            if (problemCollector.problemsOverflow()) {
                problems.add(
                        0,
                        new DefaultModelProblem(
                                "Too many model problems reported (listed problems are just a subset of reported problems)",
                                org.apache.maven.model.building.ModelProblem.Severity.WARNING,
                                null,
                                null,
                                -1,
                                -1,
                                null,
                                null));
                return new ArrayList<>(problems) {
                    @Override
                    public int size() {
                        return problemCollector.totalProblemsReported();
                    }
                };
            } else {
                return problems;
            }
        }

        private static org.apache.maven.model.building.ModelProblem convert(ModelProblem p) {
            return new DefaultModelProblem(
                    p.getMessage(),
                    org.apache.maven.model.building.ModelProblem.Severity.valueOf(
                            p.getSeverity().name()),
                    org.apache.maven.model.building.ModelProblem.Version.valueOf(
                            p.getVersion().name()),
                    p.getSource(),
                    p.getLineNumber(),
                    p.getColumnNumber(),
                    p.getModelId(),
                    p.getException());
        }

        @SuppressWarnings({"checkstyle:methodlength", "deprecation"})
        private void initProject(MavenProject project, ModelBuilderResult result) {
            project.setModel(new org.apache.maven.model.Model(result.getEffectiveModel()));
            project.setOriginalModel(new org.apache.maven.model.Model(result.getFileModel()));

            initParent(project, result);

            Artifact projectArtifact = repositorySystem.createArtifact(
                    project.getGroupId(), project.getArtifactId(), project.getVersion(), null, project.getPackaging());
            project.setArtifact(projectArtifact);

            // only set those on 2nd phase, ignore on 1st pass
            if (project.getFile() != null) {
                Build build = project.getBuild().getDelegate();
                List<org.apache.maven.api.model.Source> sources = build.getSources();
                Path baseDir = project.getBaseDirectory();
                boolean hasScript = false;
                boolean hasMain = false;
                boolean hasTest = false;
                for (var source : sources) {
                    var src = new DefaultSourceRoot(session, baseDir, source);
                    project.addSourceRoot(src);
                    Language language = src.language();
                    if (Language.JAVA_FAMILY.equals(language)) {
                        ProjectScope scope = src.scope();
                        if (ProjectScope.MAIN.equals(scope)) {
                            hasMain = true;
                        } else {
                            hasTest |= ProjectScope.TEST.equals(scope);
                        }
                    } else {
                        hasScript |= Language.SCRIPT.equals(language);
                    }
                }
                /*
                 * `sourceDirectory`, `testSourceDirectory` and `scriptSourceDirectory`
                 * are ignored if the POM file contains at least one <source> element
                 * for the corresponding scope and language. This rule exists because
                 * Maven provides default values for those elements which may conflict
                 * with user's configuration.
                 */
                if (!hasScript) {
                    project.addScriptSourceRoot(build.getScriptSourceDirectory());
                }
                if (!hasMain) {
                    project.addCompileSourceRoot(build.getSourceDirectory());
                }
                if (!hasTest) {
                    project.addTestCompileSourceRoot(build.getTestSourceDirectory());
                }
                for (Resource resource : project.getBuild().getDelegate().getResources()) {
                    project.addSourceRoot(new DefaultSourceRoot(baseDir, ProjectScope.MAIN, resource));
                }
                for (Resource resource : project.getBuild().getDelegate().getTestResources()) {
                    project.addSourceRoot(new DefaultSourceRoot(baseDir, ProjectScope.TEST, resource));
                }
            }

            project.setActiveProfiles(
                    Stream.concat(result.getActivePomProfiles().stream(), result.getActiveExternalProfiles().stream())
                            .map(org.apache.maven.model.Profile::new)
                            .toList());

            project.setInjectedProfileIds("external", getProfileIds(result.getActiveExternalProfiles()));
            project.setInjectedProfileIds(
                    result.getEffectiveModel().getId(), getProfileIds(result.getActivePomProfiles()));

            //
            // All the parts that were taken out of MavenProject for Maven 4.0.0
            //

            project.setProjectBuildingRequest(request);

            // pluginArtifacts
            Set<Artifact> pluginArtifacts = new HashSet<>();
            for (Plugin plugin : project.getModel().getDelegate().getBuild().getPlugins()) {
                Artifact artifact = repositorySystem.createPluginArtifact(new org.apache.maven.model.Plugin(plugin));

                if (artifact != null) {
                    pluginArtifacts.add(artifact);
                }
            }
            project.setPluginArtifacts(pluginArtifacts);

            // reportArtifacts
            Set<Artifact> reportArtifacts = new HashSet<>();
            for (ReportPlugin report :
                    project.getModel().getDelegate().getReporting().getPlugins()) {
                Plugin pp = Plugin.newBuilder()
                        .groupId(report.getGroupId())
                        .artifactId(report.getArtifactId())
                        .version(report.getVersion())
                        .build();

                Artifact artifact = repositorySystem.createPluginArtifact(new org.apache.maven.model.Plugin(pp));

                if (artifact != null) {
                    reportArtifacts.add(artifact);
                }
            }
            project.setReportArtifacts(reportArtifacts);

            // extensionArtifacts
            Set<Artifact> extensionArtifacts = new HashSet<>();
            List<Extension> extensions =
                    project.getModel().getDelegate().getBuild().getExtensions();
            if (extensions != null) {
                for (Extension ext : extensions) {
                    String version;
                    if (ext.getVersion() == null || ext.getVersion().isEmpty()) {
                        version = "RELEASE";
                    } else {
                        version = ext.getVersion();
                    }

                    Artifact artifact = repositorySystem.createArtifact(
                            ext.getGroupId(), ext.getArtifactId(), version, null, "jar");

                    if (artifact != null) {
                        extensionArtifacts.add(artifact);
                    }
                }
            }
            project.setExtensionArtifacts(extensionArtifacts);

            // managedVersionMap
            Map<String, Artifact> map = Collections.emptyMap();
            final DependencyManagement dependencyManagement =
                    project.getModel().getDelegate().getDependencyManagement();
            if (dependencyManagement != null
                    && dependencyManagement.getDependencies() != null
                    && !dependencyManagement.getDependencies().isEmpty()) {
                map = new LazyMap<>(() -> {
                    Map<String, Artifact> tmp = new HashMap<>();
                    for (Dependency d : dependencyManagement.getDependencies()) {
                        Artifact artifact =
                                repositorySystem.createDependencyArtifact(new org.apache.maven.model.Dependency(d));
                        if (artifact != null) {
                            tmp.put(d.getManagementKey(), artifact);
                        }
                    }
                    return Collections.unmodifiableMap(tmp);
                });
            }
            project.setManagedVersionMap(map);

            // release artifact repository
            if (project.getDistributionManagement() != null
                    && project.getDistributionManagement().getRepository() != null) {
                try {
                    DeploymentRepository r = project.getModel()
                            .getDelegate()
                            .getDistributionManagement()
                            .getRepository();
                    if (r.getId() != null
                            && !r.getId().isEmpty()
                            && r.getUrl() != null
                            && !r.getUrl().isEmpty()) {
                        ArtifactRepository repo = MavenRepositorySystem.buildArtifactRepository(
                                new org.apache.maven.model.DeploymentRepository(r));
                        repositorySystem.injectProxy(request.getRepositorySession(), List.of(repo));
                        repositorySystem.injectAuthentication(request.getRepositorySession(), List.of(repo));
                        project.setReleaseArtifactRepository(repo);
                    }
                } catch (InvalidRepositoryException e) {
                    throw new IllegalStateException(
                            "Failed to create release distribution repository for " + project.getId(), e);
                }
            }

            // snapshot artifact repository
            if (project.getDistributionManagement() != null
                    && project.getDistributionManagement().getSnapshotRepository() != null) {
                try {
                    DeploymentRepository r = project.getModel()
                            .getDelegate()
                            .getDistributionManagement()
                            .getSnapshotRepository();
                    if (r.getId() != null
                            && !r.getId().isEmpty()
                            && r.getUrl() != null
                            && !r.getUrl().isEmpty()) {
                        ArtifactRepository repo = MavenRepositorySystem.buildArtifactRepository(
                                new org.apache.maven.model.DeploymentRepository(r));
                        repositorySystem.injectProxy(request.getRepositorySession(), List.of(repo));
                        repositorySystem.injectAuthentication(request.getRepositorySession(), List.of(repo));
                        project.setSnapshotArtifactRepository(repo);
                    }
                } catch (InvalidRepositoryException e) {
                    throw new IllegalStateException(
                            "Failed to create snapshot distribution repository for " + project.getId(), e);
                }
            }

            // remote repositories
            List<ArtifactRepository> remoteRepositories = request.getRemoteRepositories();
            try {
                remoteRepositories = projectBuildingHelper.createArtifactRepositories(
                        project.getModel().getRepositories(), remoteRepositories, request);
            } catch (Exception e) {
                result.getProblemCollector()
                        .reportProblem(new org.apache.maven.impl.model.DefaultModelProblem(
                                "",
                                Severity.ERROR,
                                Version.BASE,
                                project.getModel().getDelegate(),
                                -1,
                                -1,
                                e));
            }
            project.setRemoteArtifactRepositories(remoteRepositories);
        }

        private void initParent(MavenProject project, ModelBuilderResult result) {
            Model parentModel = result.getParentModel();

            if (parentModel != null) {
                final String parentGroupId = getGroupId(parentModel);
                final String parentVersion = getVersion(parentModel);

                project.setParentArtifact(repositorySystem.createProjectArtifact(
                        parentGroupId, parentModel.getArtifactId(), parentVersion));

                MavenProject parent = projectIndex.get(parentModel.getId());
                if (parent == null) {
                    //
                    // At this point the DefaultModelBuildingListener has fired, and it populates the
                    // remote repositories with those found in the pom.xml, along with the existing externally
                    // defined repositories.
                    //
                    request.getRemoteRepositories().addAll(project.getRemoteArtifactRepositories());
                    Path parentPomFile = parentModel.getPomFile();
                    if (parentPomFile != null) {
                        project.setParentFile(parentPomFile.toFile());
                        try {
                            parent = build(true, parentPomFile, Sources.buildSource(parentPomFile))
                                    .getProject();
                        } catch (ProjectBuildingException e) {
                            // MNG-4488 where let invalid parents slide on by
                            if (logger.isDebugEnabled()) {
                                // Message below is checked for in the MNG-2199 core IT.
                                logger.warn("Failed to build parent project for " + project.getId(), e);
                            } else {
                                // Message below is checked for in the MNG-2199 core IT.
                                logger.warn("Failed to build parent project for " + project.getId());
                            }
                        }
                    } else {
                        Artifact parentArtifact = project.getParentArtifact();
                        try {
                            parent = build(true, parentArtifact, false).getProject();
                        } catch (ProjectBuildingException e) {
                            // MNG-4488 where let invalid parents slide on by
                            if (logger.isDebugEnabled()) {
                                // Message below is checked for in the MNG-2199 core IT.
                                logger.warn("Failed to build parent project for " + project.getId(), e);
                            } else {
                                // Message below is checked for in the MNG-2199 core IT.
                                logger.warn("Failed to build parent project for " + project.getId());
                            }
                        }
                    }
                }
                project.setParent(parent);
                if (project.getParentFile() == null && parent != null) {
                    project.setParentFile(parent.getFile());
                }
            }
        }

        private ModelBuilderRequest.ModelBuilderRequestBuilder getModelBuildingRequest() {
            ModelBuilderRequest.ModelBuilderRequestBuilder modelBuildingRequest = ModelBuilderRequest.builder();

            modelBuildingRequest.session(session);
            modelBuildingRequest.requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT);
            modelBuildingRequest.profiles(
                    request.getProfiles() != null
                            ? request.getProfiles().stream()
                                    .map(org.apache.maven.model.Profile::getDelegate)
                                    .toList()
                            : null);
            modelBuildingRequest.activeProfileIds(request.getActiveProfileIds());
            modelBuildingRequest.inactiveProfileIds(request.getInactiveProfileIds());
            modelBuildingRequest.systemProperties(toMap(request.getSystemProperties()));
            modelBuildingRequest.userProperties(toMap(request.getUserProperties()));
            modelBuildingRequest.repositoryMerging(ModelBuilderRequest.RepositoryMerging.valueOf(
                    request.getRepositoryMerging().name()));
            modelBuildingRequest.repositories(request.getRemoteRepositories().stream()
                    .map(r -> session.getRemoteRepository(RepositoryUtils.toRepo(r)))
                    .toList());
            return modelBuildingRequest;
        }

        private DependencyResolutionResult resolveDependencies(MavenProject project) {
            DependencyResolutionResult resolutionResult;

            RepositorySystemSession session = this.session.getSession();
            try {
                DefaultDependencyResolutionRequest resolution =
                        new DefaultDependencyResolutionRequest(project, session);
                resolutionResult = dependencyResolver.resolve(resolution);
            } catch (DependencyResolutionException e) {
                resolutionResult = e.getResult();
            }

            Set<Artifact> artifacts = new LinkedHashSet<>();
            if (resolutionResult.getDependencyGraph() != null) {
                RepositoryUtils.toArtifacts(
                        artifacts,
                        resolutionResult.getDependencyGraph().getChildren(),
                        Collections.singletonList(project.getArtifact().getId()),
                        null);

                // Maven 2.x quirk: an artifact always points at the local repo, regardless whether resolved or not
                LocalRepositoryManager lrm = session.getLocalRepositoryManager();
                for (Artifact artifact : artifacts) {
                    if (!artifact.isResolved()) {
                        Path path = lrm.getAbsolutePathForLocalArtifact(RepositoryUtils.toArtifact(artifact));
                        artifact.setFile(path.toFile());
                    }
                }
            }
            project.setResolvedArtifacts(artifacts);
            project.setArtifacts(artifacts);

            return resolutionResult;
        }
    }

    private List<String> getProfileIds(List<Profile> profiles) {
        return profiles.stream().map(Profile::getId).collect(Collectors.toList());
    }

    private static ModelSource createStubModelSource(Artifact artifact) {
        String xml = "<?xml version='1.0'?>" + "<project>"
                + "<modelVersion>4.0.0</modelVersion>"
                + "<groupId>"
                + artifact.getGroupId() + "</groupId>" + "<artifactId>"
                + artifact.getArtifactId() + "</artifactId>" + "<version>"
                + artifact.getBaseVersion() + "</version>" + "<packaging>"
                + artifact.getType() + "</packaging>" + "</project>";
        return new StubModelSource(xml, artifact);
    }

    static String getGroupId(Model model) {
        String groupId = model.getGroupId();
        if (groupId == null && model.getParent() != null) {
            groupId = model.getParent().getGroupId();
        }
        return groupId;
    }

    static String getVersion(Model model) {
        String version = model.getVersion();
        if (version == null && model.getParent() != null) {
            version = model.getParent().getVersion();
        }
        return version;
    }

    private static Map<String, String> toMap(Properties properties) {
        if (properties != null && !properties.isEmpty()) {
            return properties.entrySet().stream()
                    .collect(Collectors.toMap(e -> String.valueOf(e.getKey()), e -> String.valueOf(e.getValue())));
        } else {
            return null;
        }
    }

    static class LazyMap<K, V> extends AbstractMap<K, V> {
        private final Supplier<Map<K, V>> supplier;
        private volatile Map<K, V> delegate;

        LazyMap(Supplier<Map<K, V>> supplier) {
            this.supplier = supplier;
        }

        @Override
        public Set<Entry<K, V>> entrySet() {
            if (delegate == null) {
                synchronized (this) {
                    if (delegate == null) {
                        delegate = supplier.get();
                    }
                }
            }
            return delegate.entrySet();
        }
    }

    private Model injectLifecycleBindings(
            Model model,
            ModelBuilderRequest request,
            ModelProblemCollector problems,
            MavenProject project,
            ProjectBuildingRequest projectBuildingRequest) {
        org.apache.maven.model.Model model3 = new org.apache.maven.model.Model(model);
        List<ArtifactRepository> remoteRepositories = projectBuildingRequest.getRemoteRepositories();
        List<ArtifactRepository> pluginRepositories = projectBuildingRequest.getPluginArtifactRepositories();
        try {
            pluginRepositories = projectBuildingHelper.createArtifactRepositories(
                    model3.getPluginRepositories(), pluginRepositories, projectBuildingRequest);
        } catch (Exception e) {
            problems.add(Severity.ERROR, Version.BASE, "Invalid plugin repository: " + e.getMessage(), e);
        }
        project.setPluginArtifactRepositories(pluginRepositories);

        if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT) {
            try {
                ProjectRealmCache.CacheRecord record =
                        projectBuildingHelper.createProjectRealm(project, model3, projectBuildingRequest);

                project.setClassRealm(record.getRealm());
                project.setExtensionDependencyFilter(record.getExtensionArtifactFilter());
            } catch (PluginResolutionException | PluginManagerException | PluginVersionResolutionException e) {

                problems.add(Severity.ERROR, Version.BASE, "Unresolvable build extension: " + e.getMessage(), e);
            }
            projectBuildingHelper.selectProjectRealm(project);
        }

        // (re)build the regular repos after extensions are loaded to allow for custom layouts
        try {
            remoteRepositories = projectBuildingHelper.createArtifactRepositories(
                    model3.getRepositories(), remoteRepositories, projectBuildingRequest);
        } catch (Exception e) {
            problems.add(Severity.ERROR, Version.BASE, "Invalid artifact repository: " + e.getMessage(), e);
        }
        project.setRemoteArtifactRepositories(remoteRepositories);

        if (projectBuildingRequest.isProcessPlugins()) {
            return lifecycleBindingsInjector.injectLifecycleBindings(model3.getDelegate(), request, problems);
        } else {
            return model3.getDelegate();
        }
    }
}
