/**
 * This file is part of Eclipse Steady.
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 * SPDX-FileCopyrightText: Copyright (c) 2018-2020 SAP SE or an SAP affiliate company and Eclipse Steady contributors
 */
package org.eclipse.steady.cia.util;

import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.eclipse.steady.cia.util.ArtifactDownloader.DefaultRequestCallback;
import org.eclipse.steady.cia.util.ArtifactDownloader.FileResponseExtractor;
import org.eclipse.steady.shared.enums.ProgrammingLanguage;
import org.eclipse.steady.shared.json.model.Artifact;
import org.eclipse.steady.shared.json.model.mavenCentral.MavenVersionsSearch;
import org.eclipse.steady.shared.json.model.mavenCentral.ResponseDoc;
import org.eclipse.steady.shared.util.VulasConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;

/**
 * <p>MavenCentralWrapper class.</p>
 */
public class MavenCentralWrapper implements RepositoryWrapper {

  private static final String baseUrl;

  private static String MAVEN_CENTRAL_REPO;

  private static Logger log = LoggerFactory.getLogger(MavenCentralWrapper.class);

  private static Integer mavenCentralDelay;

  private static Integer mavenCentralRetryCount;

  private static boolean CONFIGURED = false;

  private static Set<ProgrammingLanguage> SUPP_LANG = new HashSet<ProgrammingLanguage>();

  private RestTemplate rest_template;

  static {
    SUPP_LANG.add(ProgrammingLanguage.JAVA);
    MAVEN_CENTRAL_REPO =
        VulasConfiguration.getGlobal()
            .getConfiguration()
            .getString("vulas.lib-utils.mavencentral.repo", null);
    baseUrl =
        VulasConfiguration.getGlobal()
            .getConfiguration()
            .getString("vulas.lib-utils.mavencentral.search", null);
    mavenCentralDelay =
        VulasConfiguration.getGlobal()
            .getConfiguration()
            .getInteger("vulas.lib-utils.mavencentral.retrydelay", 10000);
    mavenCentralRetryCount =
        VulasConfiguration.getGlobal()
            .getConfiguration()
            .getInteger("vulas.lib-utils.mavencentral.retrycount", 3);
    if (MAVEN_CENTRAL_REPO != null && baseUrl != null) CONFIGURED = true;
  }

  public MavenCentralWrapper() {
    super();
    RestTemplateBuilder builder = new RestTemplateBuilder();
    this.rest_template =
        builder
            .setConnectTimeout(Duration.ofMillis(5000))
            .setReadTimeout(Duration.ofMillis(5000))
            .build();
  }

  /** {@inheritDoc} */
  @Override
  public Set<ProgrammingLanguage> getSupportedLanguages() {
    return SUPP_LANG;
  }

  private StringBuilder constructQ(String _g, String _a, String _v, String _c, String _p) {
    final StringBuilder query = new StringBuilder();
    query.append("g:\"").append(_g).append("\" AND ");
    query.append("a:\"").append(_a).append("\"");
    if (_v != null && !_v.equals("")) query.append(" AND v:\"").append(_v).append("\"");
    if (_c != null && !_c.equals("")) query.append(" AND l:\"").append(_c).append("\"");
    if (_p != null && !_p.equals("")) query.append(" AND p:\"").append(_p).append("\"");
    return query;
  }

  private MavenVersionsSearch getFromMavenCentral(Map<String, String> _params)
      throws InterruptedException {

    ResponseEntity<MavenVersionsSearch> responseEntity = null;

    for (Integer i = 1; i < this.mavenCentralRetryCount + 1; i++) {

      try {
        responseEntity = rest_template.getForEntity(baseUrl, MavenVersionsSearch.class, _params);
        break;
      } catch (HttpServerErrorException he) {
        MavenCentralWrapper.log.error(
            "HttpServerErrorException: Received status code ["
                + he.getStatusCode()
                + "] calling url ["
                + baseUrl
                + "] with app ["
                + _params.get("q")
                + "], retry from ["
                + i
                + "]");
        Thread.sleep(i * mavenCentralDelay);

      } catch (HttpClientErrorException he) {
        MavenCentralWrapper.log.warn(
            "HttpClientErrorException: Received status code ["
                + he.getStatusCode()
                + "] calling url ["
                + baseUrl
                + "] with app ["
                + _params.get("q")
                + "] :"
                + he.getMessage());
      }
    }

    if (responseEntity != null) return responseEntity.getBody();
    else {
      log.warn(
          "Maven central was unavailable and could not serve the request ["
              + baseUrl
              + "] with params ["
              + _params.get("q")
              + "]");
      return null;
    }
  }

  private Set<Artifact> getArtifactVersions(
      String mvnGroup,
      String artifact,
      String version,
      Boolean latest,
      String greaterThanVersion,
      String classifier,
      String packaging)
      throws Exception {
    Set<Artifact> result = new TreeSet<Artifact>();
    try {

      final Map<String, String> params = new HashMap<String, String>();
      // NOTE: we do not use the packaging in the query to maven as e.g. jackson-databind 2.9.5 has
      // packaging bundle instead of jar
      // same for classifier
      params.put("q", constructQ(mvnGroup, artifact, version, null, null).toString());
      params.put("core", "gav");
      params.put("rows", "989");
      params.put("wt", "json");

      // Make the query
      final MavenVersionsSearch search = this.getFromMavenCentral(params);

      // Set filters
      packaging = (packaging != null && packaging.equals("") ? null : packaging);
      classifier = (classifier != null && classifier.equals("") ? null : classifier);

      if (search != null) {
        // Client only wants the latest
        if (!search.getResponse().getDocs().isEmpty() && latest) {
          final TreeSet<ResponseDoc> filtered =
              this.filter(search.getResponse().getSortedDocs(), classifier, packaging);
          ResponseDoc latest_only = null;
          if (filtered.size() > 0) latest_only = filtered.last();
          if (latest_only != null) result.add(latest_only.toArtifact());

        }
        // Client only wants versions GT x
        else if (!search.getResponse().getDocs().isEmpty()
            && greaterThanVersion != null
            && !greaterThanVersion.equals("")) {

          // Find the timestamp of version X
          long timestamp = -1;
          for (ResponseDoc d : search.getResponse().getSortedDocs()) {
            if (d.getV().equals(greaterThanVersion)) {
              timestamp = d.getTimestamp();
              break;
            }
          }

          // Return bad request if version is not found
          if (timestamp == -1) {
            log.error(
                "Version ["
                    + greaterThanVersion
                    + "] not found in Maven Central for group ["
                    + mvnGroup
                    + "] and artifact ["
                    + artifact
                    + "]");
            return null;
          }

          // Find all with a timestamp after that of X
          for (ResponseDoc d : search.getResponse().getSortedDocs()) {
            if (d.getTimestamp() > timestamp && d.availableWith(classifier, packaging)) {
              result.add(d.toArtifact());
            }
          }

        }
        // Client wants all versions
        else if (!search.getResponse().getDocs().isEmpty()) {
          for (ResponseDoc d :
              this.filter(search.getResponse().getSortedDocs(), classifier, packaging)) {
            result.add(d.toArtifact());
          }
        }
      }

    } catch (Exception e) {
      log.error("Error: " + e.getMessage(), e);
      throw e;
    }
    return result;
  }

  /** {@inheritDoc} */
  @Override
  public Path downloadArtifact(Artifact _doc) throws Exception {
    final String url = MAVEN_CENTRAL_REPO;
    final StringBuilder b = new StringBuilder();
    b.append(url);
    b.append(_doc.getLibId().getMvnGroup().replace('.', '/')).append("/");
    b.append(_doc.getLibId().getArtifact()).append("/");
    b.append(_doc.getLibId().getVersion()).append("/");
    b.append(_doc.getM2Filename());

    // Make the query

    Path result = null;
    try {
      rest_template.execute(
          b.toString(),
          HttpMethod.GET,
          new DefaultRequestCallback(),
          new FileResponseExtractor(_doc, _doc.getAbsM2Path()));
      result = _doc.getAbsM2Path();
    } catch (HttpClientErrorException e) {
      MavenCentralWrapper.log.error(_doc + " not available at [" + b.toString() + "]");
    }
    return result;
  }

  /** {@inheritDoc} */
  @Override
  public Artifact getArtifactForDigest(String _sha1) throws RepoException {
    Artifact doc = null;
    try {
      // URL and params
      final StringBuilder query = new StringBuilder();
      query.append("1:\"").append(_sha1).append("\"");
      final Map<String, String> params = new HashMap<String, String>();
      params.put("q", query.toString());
      params.put("rows", "50");
      params.put("wt", "json");
      params.put("core", "gav");

      // Make the query

      final MavenVersionsSearch search =
          rest_template.getForObject(baseUrl, MavenVersionsSearch.class, params);

      // Return results
      final TreeSet<ResponseDoc> versions = new TreeSet<ResponseDoc>();
      versions.addAll(search.getResponse().getDocs());

      if (versions.isEmpty()) {
        log.info("No artifact found for SHA1 [" + _sha1 + "] in Maven Central");
      } else if (versions.size() == 1) {
        ResponseDoc d = versions.first();
        doc = new Artifact(d.getG(), d.getA(), d.getV());
        doc.setPackaging(d.getP());
        doc.setClassifier(d.getC());
        doc.setTimestamp(d.getTimestamp());
        doc.setProgrammingLanguage(ProgrammingLanguage.JAVA);
        log.info("Found artifact " + versions.first() + " for SHA1 [" + _sha1 + "]");
      } else {
        log.error(
            "Found ["
                + versions.size()
                + "] artifacts for SHA1 ["
                + _sha1
                + "], should be none or one only");
        throw new RepoException(
            "Found ["
                + versions.size()
                + "] artifacts for SHA1 ["
                + _sha1
                + "], should be none or one only");
      }
    } catch (Exception e) {
      log.error(
          "Error when searching for [" + _sha1 + "] in repo [" + baseUrl + "]: " + e.getMessage(),
          e);
      e.printStackTrace();
      throw new RepoException(
          "Error when searching for [" + _sha1 + "] in repo [" + baseUrl + "]: " + e.getMessage(),
          e);
    }
    return doc;
  }

  /** {@inheritDoc} */
  @Override
  public Set<Artifact> getAllArtifactVersions(
      String group, String artifact, String classifier, String packaging) throws Exception {
    return this.getArtifactVersions(group, artifact, null, false, null, classifier, packaging);
  }

  /** {@inheritDoc} */
  @Override
  public Set<Artifact> getGreaterArtifactVersions(
      String group, String artifact, String greaterThanVersion, String classifier, String packaging)
      throws Exception {
    return this.getArtifactVersions(
        group, artifact, null, false, greaterThanVersion, classifier, packaging);
  }

  /** {@inheritDoc} */
  @Override
  public Artifact getLatestArtifactVersion(
      String group, String artifact, String classifier, String packaging) throws Exception {
    Set<Artifact> a =
        this.getArtifactVersions(group, artifact, null, true, null, classifier, packaging);
    List<Artifact> list = new ArrayList<Artifact>(a);
    if (list.size() > 1) {
      throw new Exception(
          "More than one latest version found ["
              + a.size()
              + "] for GA ["
              + group
              + ":"
              + artifact
              + "]");
    }
    if (list.isEmpty()) return null;
    return list.get(0);
  }

  /** {@inheritDoc} */
  @Override
  public Artifact getArtifactVersion(
      String group,
      String artifact,
      String version,
      String classifier,
      String packaging,
      ProgrammingLanguage lang)
      throws Exception {
    Set<Artifact> a =
        this.getArtifactVersions(group, artifact, version, false, null, classifier, packaging);
    List<Artifact> list = new ArrayList<Artifact>(a);
    if (list.size() > 1) {
      throw new Exception(
          "More than one artifact found ["
              + a.size()
              + "] for GAV ["
              + group
              + ":"
              + artifact
              + ":"
              + version
              + (classifier != null ? ":" + classifier : "")
              + "]");
    }
    if (list.isEmpty()) return null;
    return list.get(0);
  }

  /**
   * <p>getArtifactForClass.</p>
   *
   * @param classname a {@link java.lang.String} object.
   * @param rows a {@link java.lang.String} object.
   * @param classifierFilter a {@link java.lang.String} object.
   * @param packagingFilter a {@link java.lang.String} object.
   * @return a {@link java.util.Set} object.
   * @throws java.lang.Exception if any.
   */
  public Set<Artifact> getArtifactForClass(
      String classname, String rows, String classifierFilter, String packagingFilter)
      throws Exception {
    final StringBuilder query = new StringBuilder();
    query.append("fc:\"").append(classname).append("\"");
    final Map<String, String> params = new HashMap<String, String>();
    params.put("q", query.toString());
    params.put("rows", rows);
    params.put("core", "gav");
    params.put("wt", "json");

    final MavenVersionsSearch search = this.getFromMavenCentral(params);

    final TreeSet<Artifact> all = new TreeSet<Artifact>();
    if (search != null) {
      for (ResponseDoc d :
          this.filter(search.getResponse().getSortedDocs(), classifierFilter, packagingFilter))
        all.add(d.toArtifact());
    }
    return all;
  }

  private TreeSet<ResponseDoc> filter(
      Collection<ResponseDoc> _input, String _classifier, String _packaging) {
    final TreeSet<ResponseDoc> filtered = new TreeSet<ResponseDoc>();
    for (ResponseDoc doc : _input) {
      if (doc.availableWith(_classifier, _packaging)) filtered.add(doc);
    }
    return filtered;
  }

  /** {@inheritDoc} */
  @Override
  public boolean isConfigured() {
    return CONFIGURED;
  }
}
