/*
 * Copyright 2003-2015 JetBrains s.r.o.
 *
 * 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 jetbrains.mps.ide.blame.command;

import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import jetbrains.mps.ide.blame.perform.Query;
import jetbrains.mps.ide.blame.perform.Response;
import jetbrains.mps.util.annotation.ToRemove;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Wrapper class for requests to YouTrack
 */
public class Command {
  public static final String YOUTRACK_BASE_URL = "https://youtrack.jetbrains.com";
  public static final String ISSUE_BASE_URL = YOUTRACK_BASE_URL + "/issue/";

  private static final String LOGIN = "/rest/user/login";
  private static final String POST_ISSUE = "/rest/issue/";
  private static final String ISSUE_COMMAND_FORMAT = "/rest/issue/%s/execute";
  private static final String LIST_VERSIONS = "/rest/admin/customfield/versionBundle/MPS%20Versions";

  private static final String PROJECT = "MPS";
  private static final String EXCEPTION = "Auto-reported Exception";

  private static final String LOGIN_PARAM_NAME = "login";
  private static final String PASSWORD_PARAM_NAME = "password";
  private static final String PROJECT_PARAM_NAME = "project";
  private static final String SUMMARY_PARAM_NAME = "summary";
  private static final String DESCRIPTION_PARAM_NAME = "description";
  private static final String TYPE_PARAM_NAME = "type";
  private static final String PERMITTED_GROUP_PARAM_NAME = "permittedGroup";
  private static final String MPS_GROUP_NAME = "mps-developers";
  private static final String COMMAND_PARAM_NAME = "command";
  private static final String COMMAND_ADD_PARAM_NAME = COMMAND_PARAM_NAME + " add";
  private static final String SUBSYSTEM_FIELD = "Subsystem";
  private static final String AFFECTED_VERSIONS_FIELD = "Affected versions";

  private static final String RESPONSE_FAILED = "Can't update issue";
  private static final String RESPONSE_SUCCEEDED = "Issue updated";

  private static final int DEFAULT_TIMEOUT = 5000;

  private final HttpClient c;

  public Command() {
    c = new HttpClient();
    setTimeouts(DEFAULT_TIMEOUT);
  }

  /**
   * Sets timeout for waiting for response from server.
   * By default it is set to {@link Command#DEFAULT_TIMEOUT} = 5000
   *
   * @param timeoutMillis timeout in milliseconds
   */
  public final void setTimeouts(int timeoutMillis) {
    // Final method, because called in constructor - avoiding road to hell.
    HttpClientParams params = c.getParams();
    params.setConnectionManagerTimeout(timeoutMillis);
    params.setSoTimeout(timeoutMillis);
    c.setParams(params);
  }


  /**
   * Logs into YouTrack with provided credentials
   *
   * @param query with user credentials
   * @return server response if authentication with provided credentials succeeded
   * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)}
   */
  public Response login(Query query) throws IOException {
    PostMethod p = new PostMethod(YOUTRACK_BASE_URL + LOGIN);
    p.addParameter(LOGIN_PARAM_NAME, query.getUser());
    p.addParameter(PASSWORD_PARAM_NAME, query.getPassword());
    c.executeMethod(p);

    int statusCode = p.getStatusCode();
    String responseString = p.getResponseBodyAsString();
    if (statusCode != 200 || !responseString.contains("ok")) {
      return new Response("Can't login into issue tracker", responseString, false, null);
    } else {
      return new Response("Logged in correctly", responseString, true, null);
    }
  }

  /**
   * Creates new issue in YouTrack for MPS projects.
   * <br/>
   * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid or anonymous credentials first.
   *
   * @param summary     title of the issue
   * @param description description of issue (system info, build info, steps to reproduce, exception message and stack traces)
   * @param hidden      determine if issue will be visible only to creator and mps-developers group
   * @param files       list of files to be attached to the issue
   * @return response with created issue id if response succeeded
   * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)}
   */
  @NotNull
  public Response postIssue(String summary, String description, boolean hidden, File... files) throws IOException {
    PostMethod p = new PostMethod(YOUTRACK_BASE_URL + POST_ISSUE);
    p.addParameter(PROJECT_PARAM_NAME, PROJECT);
    p.addParameter(SUMMARY_PARAM_NAME, summary);
    p.addParameter(DESCRIPTION_PARAM_NAME, description);
    p.addParameter(TYPE_PARAM_NAME, EXCEPTION);
    if (hidden) {
      p.addParameter(PERMITTED_GROUP_PARAM_NAME, MPS_GROUP_NAME);
    }
    if (files.length != 0) {
      List<Part> parts = new ArrayList<>();
      for (NameValuePair nameValuePair : p.getParameters()) {
        parts.add(new StringPart(nameValuePair.getName(), nameValuePair.getValue()));
      }
      for (File file : files) {
        parts.add(new FilePart(file.getName(), file));
      }
      p.setRequestEntity(new MultipartRequestEntity(parts.toArray(new Part[parts.size()]), p.getParams()));
    }
    c.executeMethod(p);

    int statusCode = p.getStatusCode();
    String responseString = p.getResponseBodyAsString();
    if (statusCode == 200) {
      return new Response("Issue posted", responseString, true, null);
    } else {
      return new Response("Can't post issue", responseString, false, null);
    }
  }

  /**
   * This is utility method for short call of more general method {@link Command#setIssueField(java.lang.String, java.lang.String, java.lang.String)}
   * <br/>
   * Used to update issue subsystem.
   * <br/>
   * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid or anonymous credentials first.
   *
   * @param issueId   YouTrack issue with this id will be updated
   * @param subsystem subsystem of the bug
   * @return server response to update query
   * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)}
   */
  @NotNull
  public Response setIssueSubsystem(@NotNull String issueId, @NotNull String subsystem) throws IOException {
    return setIssueField(issueId, SUBSYSTEM_FIELD, subsystem);
  }

  /**
   * This is utility method for short call of more general method {@link Command#setIssueField(java.lang.String, java.lang.String, java.lang.String)}
   * <br/>
   * Used to update issue affected version.
   * <br/>
   * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid or anonymous credentials first.
   *
   * @param issueId         YouTrack issue with this id will be updated
   * @param affectedVersion current MPS version containing bug
   * @return server response to update query
   * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)}
   */
  @NotNull
  public Response setIssueAffectedVersion(@NotNull String issueId, @NotNull String affectedVersion) throws IOException {
    return setIssueField(issueId, AFFECTED_VERSIONS_FIELD, affectedVersion);
  }

  /**
   * <p>
   * Sets new value to specified field of <a href="https://youtrack.jetbrains.com">YouTrack</a> issue with provided ID.
   * <br/>
   * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid or anonymous credentials first.
   * </p>
   * <p>
   * ATTENTION: old value of field, if it was already set, will be overridden.
   * <br/>
   * If you need to update filed value and preserve old one, use {@link Command#updateIssueField(java.lang.String, java.lang.String, java.lang.String)}
   *
   * @param issueId YouTrack issue with this id will be updated
   * @param field   of issue to change (as shown in web)
   * @param value   value which will be set to issue field
   * @return server response to update query
   * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)}
   */
  public Response setIssueField(@NotNull String issueId, @NotNull String field, @NotNull String value) throws IOException {
    return modifyIssueField(issueId, COMMAND_PARAM_NAME, field, value);
  }

  /**
   * <p>
   * Adds new value to specified field of <a href="https://youtrack.jetbrains.com">YouTrack</a> issue with provided ID.
   * <br/>
   * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid or anonymous credentials first.
   * </p>
   * ATTENTION: new value of field, will be added to current value.
   * <br/>
   * If you need to override old value, use {@link Command#setIssueField(java.lang.String, java.lang.String, java.lang.String)}
   *
   * @param issueId YouTrack issue with this id will be updated
   * @param field   of issue to update (as shown in web)
   * @param value   value which will added to field
   * @return server response to update query
   * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)}
   */
  public Response updateIssueField(@NotNull String issueId, @NotNull String field, @NotNull String value) throws IOException {
    return modifyIssueField(issueId, COMMAND_ADD_PARAM_NAME, field, value);
  }

  /**
   * Implementation for {@link Command#updateIssueField(java.lang.String, java.lang.String, java.lang.String)}
   * and {@link Command#setIssueField(java.lang.String, java.lang.String, java.lang.String)}
   */
  private Response modifyIssueField(@NotNull String issueId, @NotNull String commandString, @NotNull String field, @NotNull String value) throws IOException {
    PostMethod p = new PostMethod(YOUTRACK_BASE_URL + String.format(ISSUE_COMMAND_FORMAT, issueId));
    p.addParameter(commandString, String.format("%s %s", field, value));
    c.executeMethod(p);

    int statusCode = p.getStatusCode();
    String responseString = p.getResponseBodyAsString();
    if (statusCode == 200) {
      return new Response(RESPONSE_SUCCEEDED, responseString, true, null);
    } else {
      return new Response(RESPONSE_FAILED, responseString, false, null);
    }
  }

  /**
   * Requests for list of versions.
   * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid credentials first.
   * To retrieve list, user must be part of mps-developers group
   *
   * @return server response for list of versions request
   * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)}
   */
  @TestOnly
  @NotNull
  public Response listVersions() throws IOException {
    GetMethod p = new GetMethod(YOUTRACK_BASE_URL + LIST_VERSIONS);
    c.executeMethod(p);

    int statusCode = p.getStatusCode();
    String responseString = p.getResponseBodyAsString();
    return new Response("List MPS versions", responseString, statusCode == 200, null);
  }

  /**
   * Utility method to encapsulate response parsing
   *
   * @param response with list of versions from YouTrack
   * @return set of versions as strings or {@code null} if request returned fail response
   */
  @TestOnly
  @Nullable
  public Set<String> extractVersions(Response response) {
    Element e = response.getResponseXml();

    if (e == null) {
      return null;
    }

    Set<String> availableVersions = new HashSet<>();
    for (Element v : e.getChildren("version")) {
      availableVersions.add(v.getText());
    }
    return availableVersions;
  }

  /**
   * Unused method.
   * <p>
   * Use {@code ApplicationInfo.getInstance().getFullVersion()} instead
   */
  @Deprecated
  @ToRemove(version = 2017.1)
  public static String getVersion() {
    String version = ApplicationInfo.getInstance().getMajorVersion() + "." + ApplicationInfo.getInstance().getMinorVersion();
    return ApplicationInfoEx.getInstanceEx().isEAP() ? version + " EAP" : version;
  }
}
