/*
 * 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.impl;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.maven.api.Artifact;
import org.apache.maven.api.ArtifactCoordinates;
import org.apache.maven.api.DownloadedArtifact;
import org.apache.maven.api.Repository;
import org.apache.maven.api.Session;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.cache.BatchRequestException;
import org.apache.maven.api.cache.MavenExecutionException;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton;
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.Request;
import org.apache.maven.api.services.RequestTrace;
import org.apache.maven.api.services.Result;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.transfer.ArtifactNotFoundException;

import static java.util.Objects.requireNonNull;

@Named
@Singleton
public class DefaultArtifactResolver implements ArtifactResolver {

    @Override
    public ArtifactResolverResult resolve(ArtifactResolverRequest request)
            throws ArtifactResolverException, IllegalArgumentException {
        requireNonNull(request, "request");
        InternalSession session = InternalSession.from(request.getSession());
        return session.request(request, this::doResolve);
    }

    record ResolverRequest(Session session, RequestTrace trace, ArtifactRequest request) implements Request<Session> {
        @Nonnull
        @Override
        public Session getSession() {
            return session;
        }

        @Nullable
        @Override
        public RequestTrace getTrace() {
            return trace;
        }
    }

    record ResolverResult(ResolverRequest request, ArtifactResult result) implements Result<ResolverRequest> {
        @Nonnull
        @Override
        public ResolverRequest getRequest() {
            return request;
        }
    }

    protected ArtifactResolverResult doResolve(ArtifactResolverRequest request) {
        InternalSession session = InternalSession.from(request.getSession());
        RequestTraceHelper.ResolverTrace trace = RequestTraceHelper.enter(session, request);
        try {
            List<RemoteRepository> repositories = session.toResolvingRepositories(
                    request.getRepositories() != null ? request.getRepositories() : session.getRemoteRepositories());

            List<ResolverRequest> requests = new ArrayList<>();
            for (ArtifactCoordinates coords : request.getCoordinates()) {
                ArtifactRequest req = new ArtifactRequest();
                req.setRepositories(repositories);
                req.setArtifact(session.toArtifact(coords));
                req.setRequestContext(trace.context());
                req.setTrace(trace.trace());
                requests.add(new ResolverRequest(session, trace.mvnTrace(), req));
            }
            List<ResolverResult> results = session.requests(requests, list -> {
                try {
                    List<ArtifactRequest> resolverRequests =
                            list.stream().map(ResolverRequest::request).toList();
                    List<ArtifactResult> resolverResults =
                            session.getRepositorySystem().resolveArtifacts(session.getSession(), resolverRequests);
                    List<ResolverResult> res = new ArrayList<>(resolverResults.size());
                    for (int i = 0; i < resolverResults.size(); i++) {
                        res.add(new ResolverResult(list.get(i), resolverResults.get(i)));
                    }
                    return res;
                } catch (ArtifactResolutionException e) {
                    throw new MavenExecutionException(e);
                }
            });

            return toResult(request, results.stream());
        } catch (BatchRequestException e) {
            String message;
            if (e.getResults().size() == 1) {
                message = e.getResults().iterator().next().error().getMessage();
            } else {
                message = "Unable to resolve artifacts: " + e.getMessage();
            }
            throw new ArtifactResolverException(message, e, toResult(request, e));
        } finally {
            RequestTraceHelper.exit(trace);
        }
    }

    ArtifactResolverResult toResult(ArtifactResolverRequest request, BatchRequestException exception) {
        return toResult(
                request,
                exception.getResults().stream()
                        .map(rr -> {
                            if (rr.result() != null) {
                                return rr.result();
                            } else if (rr.error() != null) {
                                return new ResolverResult(null, ((ArtifactResolutionException) rr.error()).getResult());
                            } else {
                                throw new IllegalStateException("Unexpected result: " + rr);
                            }
                        })
                        .map(ResolverResult.class::cast));
    }

    ArtifactResolverResult toResult(ArtifactResolverRequest request, Stream<ResolverResult> results) {
        InternalSession session = InternalSession.from(request.getSession());
        Map<ArtifactCoordinates, ArtifactResolverResult.ResultItem> items = results.map(resolverResult -> {
                    ArtifactResult result = resolverResult.result();
                    DownloadedArtifact artifact = result.getArtifact() != null
                            ? session.getArtifact(DownloadedArtifact.class, result.getArtifact())
                            : null;
                    ArtifactCoordinates coordinates = session.getArtifact(
                                    result.getRequest().getArtifact())
                            .toCoordinates();
                    // null repository (ArtifactResult.NO_REPOSITORY); can come ONLY in error exceptions
                    // when no local/remote/workspace repository was involved in error
                    // (usually resolving when every involved repo "abstained" for some reason)
                    Repository repository = result.getRepository() != null
                            ? session.getRepository(result.getRepository()).orElse(null)
                            : null;
                    // Build mapped exceptions, filtering out NO_REPOSITORY sentinel entries
                    // (those represent errors with no associated repository). Keep a flat list
                    // of all exceptions so isMissing() can still check all of them.
                    Map<Repository, List<Exception>> mappedExceptions = new HashMap<>();
                    List<Exception> allExceptions = new ArrayList<>();
                    for (var entry : result.getMappedExceptions().entrySet()) {
                        allExceptions.addAll(entry.getValue());
                        session.getRepository(entry.getKey())
                                .ifPresent(repo -> mappedExceptions.merge(repo, entry.getValue(), (a, b) -> {
                                    var merged = new ArrayList<>(a);
                                    merged.addAll(b);
                                    return merged;
                                }));
                    }
                    return new DefaultArtifactResolverResultItem(
                            coordinates,
                            artifact,
                            Map.copyOf(mappedExceptions),
                            List.copyOf(allExceptions),
                            repository,
                            result.getArtifact() != null ? result.getArtifact().getPath() : null);
                })
                .collect(Collectors.toMap(DefaultArtifactResolverResultItem::coordinates, Function.identity()));

        return new DefaultArtifactResolverResult(request, items);
    }

    record DefaultArtifactResolverResultItem(
            @Nonnull ArtifactCoordinates coordinates,
            @Nullable DownloadedArtifact artifact,
            @Nonnull Map<Repository, List<Exception>> exceptions,
            @Nonnull List<Exception> allExceptions,
            @Nullable Repository repository,
            Path path)
            implements ArtifactResolverResult.ResultItem {
        @Override
        public ArtifactCoordinates getCoordinates() {
            return coordinates;
        }

        @Override
        public DownloadedArtifact getArtifact() {
            return artifact;
        }

        @Override
        public Map<Repository, List<Exception>> getExceptions() {
            return exceptions;
        }

        @Override
        public Repository getRepository() {
            return repository;
        }

        @Override
        public Path getPath() {
            return path;
        }

        @Override
        public boolean isResolved() {
            return getPath() != null;
        }

        @Override
        public boolean isMissing() {
            // Use allExceptions (not the mapped exceptions map) so that exceptions
            // recorded under ArtifactResult.NO_REPOSITORY are still considered.
            // Guard against vacuous truth: an empty list means no exceptions at all,
            // not "all exceptions are ArtifactNotFoundException".
            return !allExceptions.isEmpty()
                    && allExceptions.stream().allMatch(e -> e instanceof ArtifactNotFoundException)
                    && !isResolved();
        }
    }

    record DefaultArtifactResolverResult(
            ArtifactResolverRequest request, Map<ArtifactCoordinates, ArtifactResolverResult.ResultItem> results)
            implements ArtifactResolverResult {

        DefaultArtifactResolverResult(ArtifactResolverRequest request, Map<ArtifactCoordinates, ResultItem> results) {
            this.request = request;
            this.results = Map.copyOf(results);
        }

        @Override
        @Nonnull
        public ArtifactResolverRequest getRequest() {
            return request;
        }

        @Nonnull
        @Override
        public Collection<DownloadedArtifact> getArtifacts() {
            return results.values().stream().map(ResultItem::getArtifact).collect(Collectors.toList());
        }

        @Override
        public Path getPath(@Nonnull Artifact artifact) {
            ResultItem resultItem = results.get(artifact.toCoordinates());
            return resultItem != null ? resultItem.getPath() : null;
        }

        @Override
        public @Nonnull Map<? extends ArtifactCoordinates, ResultItem> getResults() {
            return results;
        }

        @Override
        public ResultItem getResult(ArtifactCoordinates coordinates) {
            return results.get(coordinates);
        }
    }
}
