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

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.gson.JsonSyntaxException;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;

import org.apache.logging.log4j.Logger;
import org.eclipse.steady.ConstructChange;
import org.eclipse.steady.backend.BackendConnectionException;
import org.eclipse.steady.backend.BackendConnector;
import org.eclipse.steady.kb.command.Command;
import org.eclipse.steady.kb.model.Commit;
import org.eclipse.steady.kb.model.Note;
import org.eclipse.steady.kb.model.Vulnerability;
import org.eclipse.steady.kb.util.ConstructSet;
import org.eclipse.steady.kb.util.Metadata;
import org.eclipse.steady.shared.enums.BugOrigin;
import org.eclipse.steady.shared.enums.ContentMaturityLevel;
import org.eclipse.steady.shared.json.JsonBuilder;

import net.minidev.json.JSONObject;

/**
 * <p>ImportVulnerability class.</p>
 */
public class ImportVulnerability implements Task {
  private static final String OVERWRITE_OPTION = "o";
  private static final String DIRECTORY_OPTION = "d";
  private static final String VERBOSE_OPTION = "v";

  private static final Logger log = org.apache.logging.log4j.LogManager.getLogger();
  private BackendConnector backendConnector = null;

  /** {@inheritDoc} */
  public void execute(
      Vulnerability vuln, HashMap<String, Object> args, BackendConnector _backendConnector)
      throws BackendConnectionException, JsonSyntaxException, IOException {
    String vulnId = vuln.getVulnId();
    this.backendConnector = _backendConnector;

    Boolean overwrite = (Boolean) args.get(OVERWRITE_OPTION);
    if (!overwrite && getBackendConnector().isBugExisting(vulnId)) {
      log.info("Bug [{}] already exists in backend, analysis will be skipped", vulnId);
      return;
    }

    List<Commit> commits = new ArrayList<Commit>();
    File file = new File((String) args.get(DIRECTORY_OPTION));

    File commitDirs[] =
        file.listFiles(
            new FileFilter() {
              @Override
              public boolean accept(File file) {
                return file.isDirectory();
              }
            });

    for (File commitDir : commitDirs) {
      String dir = commitDir.getAbsolutePath();
      Commit commit = null;
      commit = Metadata.getCommitMetadata(dir);

      if (commit != null) {
        commits.add(commit);
      }
    }

    Set<ConstructChange> changes = null;
    Map<String, Set<ConstructChange>> allChanges = new HashMap<String, Set<ConstructChange>>();
    for (Commit commit : commits) {
      changes = ConstructSet.identifyConstructChanges(commit, allChanges);
      if ((Boolean) args.get(VERBOSE_OPTION)) {
        for (ConstructChange chg : changes) {
          log.info(chg.toString());
        }
      }
    }

    final String json = toJSON(vuln, commits, allChanges);
    _backendConnector.uploadChangeList(vulnId, json);
  }

  /**
   * <p>
   * toJSON.
   * </p>
   *
   * @param _vulnerability a {@link org.eclipse.steady.kb.model.Vulnerability} object.
   * @param _commits {@link java.util.List}.
   * @param _allChanges a {@link java.util.Map}.
   * @return a {@link java.lang.String} object.
   */
  private String toJSON(
      Vulnerability _vulnerability,
      List<Commit> _commits,
      Map<String, Set<ConstructChange>> _allChanges)
      throws ConcurrentModificationException {
    final StringBuilder b = new StringBuilder();
    b.append(" { ");
    b.append(" \"bugId\" : \"").append(_vulnerability.getVulnId()).append("\", ");

    List<String> aliases = _vulnerability.getAliases();
    if (aliases != null && !aliases.isEmpty()) {
      b.append(" \"bugIdAlt\" : \"");
      boolean isFirstAliase = true;
      for (String aliase : aliases) {
        if (!isFirstAliase) {
          b.append(",");
        }
        isFirstAliase = false;
        b.append(aliase);
      }

      b.append("\", ");
    }

    b.append(" \"maturity\" : \"" + ContentMaturityLevel.DRAFT.toString() + "\", ");
    b.append(" \"origin\" : \"" + BugOrigin.PUBLIC.toString() + "\", ");

    List<Note> notes = _vulnerability.getNotes();

    Set<String> links = new HashSet<>();
    Set<String> descriptions = new HashSet<>();
    if (notes != null) {
      for (Note note : notes) {
        if (note.getText() != null && !note.getText().isEmpty()) {
          descriptions.add(JSONObject.escape(note.getText()).trim());
        }

        List<String> noteLinks = note.getLinks();
        if (noteLinks != null) links.addAll(noteLinks);
      }
    }

    String description = getCVEDescription(_vulnerability);
    // remove NVD description from descriptions from statement
    // write NVD description as description
    if (description != null) {
      description.trim();
      descriptions.remove(JSONObject.escape(description));
      b.append(" \"description\" : \"");
      b.append(JSONObject.escape(description));
      b.append("\" ,");
    }
    // if other descriptions available, put them as Alt
    if (!descriptions.isEmpty()) {
      b.append(" \"descriptionAlt\" : \"");
      int descSize = descriptions.size();
      Object[] descArr = descriptions.toArray();
      b.append(descSize);
      b.append(" descriptions from different sources:");
      for (int descNum = 0; descNum < descArr.length; descNum++) {
        b.append("[").append(descNum).append("] ").append(descArr[descNum]);
      }
      b.append("\" ,");
    }

    boolean nextLinks = false;
    if (links != null && !links.isEmpty()) {
      b.append(" \"reference\" : [");
      for (String link : links) {
        if (nextLinks) {
          b.append(",");
        }
        nextLinks = true;
        b.append(JsonBuilder.escape(link));
      }

      b.append("], ");
    }

    b.append(" \"constructChanges\" : [ ");
    int i = 0;
    final Set<ConstructChange> consol_ch =
        ConstructSet.getConsolidatedChanges(_commits, _allChanges);
    for (ConstructChange c : consol_ch) {
      b.append(c.toJSON());
      if (++i < consol_ch.size()) b.append(", ");
    }
    b.append(" ] } ");
    return b.toString();
  }

  private String getCVEDescription(Vulnerability _vulnerability) {
    String cveString;
    String vulnId = _vulnerability.getVulnId();
    try {
      cveString = getBackendConnector().getCVE(vulnId);
    } catch (BackendConnectionException e) {
      log.error("Error connecting to NVD service. {}", e.getCause(), e);
      return null;
    }

    /* TODO: check the nvd service. Always sends null even if the service is wrong/down or the CVE passed is wrong */
    if (cveString == null) {
      log.warn("Could not get the CVE description for bugId {}", vulnId);
      return null;
    }

    final Configuration conf = Configuration.defaultConfiguration();
    final Object document = conf.jsonProvider().parse(cveString);

    final String summary = JsonPath.read(document, "$.summary");
    if (summary == null) {
      log.warn("No description found for [{}]", vulnId);
    }
    return summary;
  }

  /** {@inheritDoc} */
  @Override
  public Command.NAME getCommandName() {
    return Command.NAME.IMPORT;
  }

  private BackendConnector getBackendConnector() {
    return backendConnector;
  }
}
