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

import kong.unirest.GetRequest;
import kong.unirest.HttpResponse;
import kong.unirest.JsonNode;
import kong.unirest.Unirest;
import kong.unirest.apache.ApacheClient;
import kong.unirest.json.JSONArray;
import kong.unirest.json.JSONObject;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.zeppelin.common.SessionInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import unirest.shaded.org.apache.http.client.HttpClient;
import unirest.shaded.org.apache.http.impl.client.HttpClients;

import javax.net.ssl.SSLContext;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Low level api for interacting with Zeppelin. Underneath, it use the zeppelin rest api.
 * You can use this class to operate Zeppelin note/paragraph,
 * e.g. get/add/delete/update/execute/cancel
 */
public class ZeppelinClient {
  private static final Logger LOGGER = LoggerFactory.getLogger(ZeppelinClient.class);

  private ClientConfig clientConfig;

  public ZeppelinClient(ClientConfig clientConfig) throws Exception {
    this.clientConfig = clientConfig;
    Unirest.config().defaultBaseUrl(clientConfig.getZeppelinRestUrl() + "/api");

    if (clientConfig.isUseKnox()) {
      try {
        SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy() {
          public boolean isTrusted(X509Certificate[] chain, String authType) {
            return true;
          }
        }).build();
        HttpClient customHttpClient = HttpClients.custom().setSSLContext(sslContext)
                .setSSLHostnameVerifier(new NoopHostnameVerifier()).build();
        Unirest.config().httpClient(ApacheClient.builder(customHttpClient));
      } catch (Exception e) {
        throw new Exception("Fail to setup httpclient of Unirest", e);
      }
    }
  }

  public ClientConfig getClientConfig() {
    return clientConfig;
  }

  /**
   * Throw exception if the status code is not 200.
   *
   * @param response
   * @throws Exception
   */
  private void checkResponse(HttpResponse<JsonNode> response) throws Exception {
    if (response.getStatus() == 302) {
      throw new Exception("Please login first");
    }
    if (response.getStatus() != 200) {
      String message = response.getStatusText();
      if (response.getBody() != null &&
              response.getBody().getObject() != null &&
              response.getBody().getObject().has("message")) {
        message = response.getBody().getObject().getString("message");
      }
      throw new Exception(String.format("Unable to call rest api, status: %s, statusText: %s, message: %s",
              response.getStatus(),
              response.getStatusText(),
              message));
    }
  }

  /**
   * Throw exception if the status in the json object is not `OK`.
   *
   * @param jsonNode
   * @throws Exception
   */
  private void checkJsonNodeStatus(JsonNode jsonNode) throws Exception {
    if (! "OK".equalsIgnoreCase(jsonNode.getObject().getString("status"))) {
      throw new Exception(StringEscapeUtils.unescapeJava(jsonNode.getObject().getString("message")));
    }
  }

  /**
   * Get Zeppelin version.
   *
   * @return
   * @throws Exception
   */
  public String getVersion() throws Exception {
    HttpResponse<JsonNode> response = Unirest
            .get("/version")
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
    return jsonNode.getObject().getJSONObject("body").getString("version");
  }
  
  /**
   * Request a new session id. It doesn't create session (interpreter process) in zeppelin server side, but just
   * create an unique session id.
   *
   * @param interpreter
   * @return
   * @throws Exception
   */
  public SessionInfo newSession(String interpreter) throws Exception {
    HttpResponse<JsonNode> response = Unirest
            .post("/session")
            .header("Content-Type", "application/json")
            .queryString("interpreter", interpreter)
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
    return createSessionInfoFromJson(jsonNode.getObject().getJSONObject("body"));
  }

  /**
   * Stop the session(interpreter process) in Zeppelin server.
   *
   * @param sessionId
   * @throws Exception
   */
  public void stopSession(String sessionId) throws Exception {
    HttpResponse<JsonNode> response = Unirest
            .delete("/session/{sessionId}")
            .routeParam("sessionId", sessionId)
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
  }

  /**
   * Get session info for the provided sessionId.
   * Return null if provided sessionId doesn't exist.
   *
   * @param sessionId
   * @throws Exception
   */
  public SessionInfo getSession(String sessionId) throws Exception {
    HttpResponse<JsonNode> response = Unirest
            .get("/session/{sessionId}")
            .routeParam("sessionId", sessionId)
            .asJson();
    if (response.getStatus() == 404) {
      String statusText = response.getStatusText();
      if (response.getBody().getObject().has("message")) {
        statusText = response.getBody().getObject().getString("message");
      }
      if (statusText.contains("No such session")) {
        return null;
      }
    }
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);

    JSONObject bodyObject = jsonNode.getObject().getJSONObject("body");
    return createSessionInfoFromJson(bodyObject);
  }

  /**
   * List all the sessions.
   *
   * @return
   * @throws Exception
   */
  public List<SessionInfo> listSessions() throws Exception {
    return listSessions(null);
  }

  /**
   * List all the sessions for the provided interpreter.
   *
   * @param interpreter
   * @return
   * @throws Exception
   */
  public List<SessionInfo> listSessions(String interpreter) throws Exception {
    GetRequest getRequest = Unirest.get("/session");
    if (interpreter != null) {
      getRequest.queryString("interpreter", interpreter);
    }
    HttpResponse<JsonNode> response = getRequest.asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
    JSONArray sessionJsonArray = jsonNode.getObject().getJSONArray("body");
    List<SessionInfo> sessionInfos = new ArrayList<>();
    for (int i = 0; i< sessionJsonArray.length();++i) {
      sessionInfos.add(createSessionInfoFromJson(sessionJsonArray.getJSONObject(i)));
    }
    return sessionInfos;
  }

  private SessionInfo createSessionInfoFromJson(JSONObject sessionJson) {
    SessionInfo sessionInfo = new SessionInfo();
    if (sessionJson.has("sessionId")) {
      sessionInfo.setSessionId(sessionJson.getString("sessionId"));
    }
    if (sessionJson.has("noteId")) {
      sessionInfo.setNoteId(sessionJson.getString("noteId"));
    }
    if (sessionJson.has("interpreter")) {
      sessionInfo.setInterpreter(sessionJson.getString("interpreter"));
    }
    if (sessionJson.has("state")) {
      sessionInfo.setState(sessionJson.getString("state"));
    }
    if (sessionJson.has("weburl")) {
      sessionInfo.setWeburl(sessionJson.getString("weburl"));
    }
    if (sessionJson.has("startTime")) {
      sessionInfo.setStartTime(sessionJson.getString("startTime"));
    }

    return sessionInfo;
  }

  /**
   * Login zeppelin with userName and password, throw exception if login fails.
   *
   * @param userName
   * @param password
   * @throws Exception
   */
  public void login(String userName, String password) throws Exception {
    if (clientConfig.isUseKnox()) {
      HttpResponse<String> response = Unirest.get(clientConfig.getKnoxSSOUrl() +
              "?originalUrl=" + clientConfig.getZeppelinRestUrl())
              .basicAuth(userName, password)
              .asString();
      if (response.getStatus() != 200) {
        throw new Exception(String.format("Knox SSO login failed, status: %s, statusText: %s",
                response.getStatus(),
                response.getStatusText()));
      }
      response = Unirest.get("/security/ticket")
              .asString();
      if (response.getStatus() != 200) {
        throw new Exception(String.format("Fail to get ticket after Knox SSO, status: %s, statusText: %s",
                response.getStatus(),
                response.getStatusText()));
      }
    } else {
      HttpResponse<JsonNode> response = Unirest
              .post("/login")
              .field("userName", userName)
              .field("password", password)
              .asJson();
      if (response.getStatus() != 200) {
        throw new Exception(String.format("Login failed, status: %s, statusText: %s",
                response.getStatus(),
                response.getStatusText()));
      }
    }
  }

  public String createNote(String notePath) throws Exception {
    return createNote(notePath, "");
  }

  /**
   * Create a new empty note with provided notePath and defaultInterpreterGroup
   *
   * @param notePath
   * @param defaultInterpreterGroup
   * @return
   * @throws Exception
   */
  public String createNote(String notePath, String defaultInterpreterGroup) throws Exception {
    JSONObject bodyObject = new JSONObject();
    bodyObject.put("notePath", notePath);
    bodyObject.put("defaultInterpreterGroup", defaultInterpreterGroup);
    HttpResponse<JsonNode> response = Unirest
            .post("/notebook")
            .header("Content-Type", "application/json")
            .body(bodyObject.toString())
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);

    return jsonNode.getObject().getString("body");
  }

  /**
   * Delete note with provided noteId.
   *
   * @param noteId
   * @throws Exception
   */
  public void deleteNote(String noteId) throws Exception {
    HttpResponse<JsonNode> response = Unirest
            .delete("/notebook/{noteId}")
            .routeParam("noteId", noteId)
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
  }

  /**
   * Clone a note to a specified notePath.
   *
   * @param noteId
   * @param destPath
   * @return
   * @throws Exception
   */
  public String cloneNote(String noteId, String destPath) throws Exception {
    JSONObject bodyObject = new JSONObject();
    bodyObject.put("name", destPath);
    HttpResponse<JsonNode> response = Unirest
            .post("/notebook/{noteId}")
            .routeParam("noteId", noteId)
            .header("Content-Type", "application/json")
            .body(bodyObject.toString())
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);

    return jsonNode.getObject().getString("body");
  }

  public void renameNote(String noteId, String newNotePath) throws Exception {
    JSONObject bodyObject = new JSONObject();
    bodyObject.put("name", newNotePath);

    HttpResponse<JsonNode> response = Unirest
            .put("/notebook/{noteId}/rename")
            .routeParam("noteId", noteId)
            .header("Content-Type", "application/json")
            .body(bodyObject.toString())
            .asJson();

    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
  }

  /**
   * Query {@link NoteResult} with provided noteId.
   *
   * @param noteId
   * @return
   * @throws Exception
   */
  public NoteResult queryNoteResult(String noteId) throws Exception {
    HttpResponse<JsonNode> response = Unirest
            .get("/notebook/{noteId}")
            .routeParam("noteId", noteId)
            .asJson();
    return extractNoteResultFromResponse(response);
  }

  /**
   * Query {@link NoteResult} with provided notePath.
   *
   * @param notePath
   * @return
   * @throws Exception
   */
  public NoteResult queryNoteResultByPath(String notePath) throws Exception {
    JSONObject bodyObject = new JSONObject();
    bodyObject.put("notePath", notePath);
    HttpResponse<JsonNode> response = Unirest
            .post("/notebook/getByPath")
            .header("Content-Type", "application/json")
            .body(bodyObject)
            .asJson();
    return extractNoteResultFromResponse(response);
  }

  private NoteResult extractNoteResultFromResponse(HttpResponse<JsonNode> response) throws Exception {
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);

    JSONObject noteJsonObject = jsonNode.getObject().getJSONObject("body");
    boolean isRunning = false;
    if (noteJsonObject.has("info")) {
      JSONObject infoJsonObject = noteJsonObject.getJSONObject("info");
      if (infoJsonObject.has("isRunning")) {
        isRunning = Boolean.parseBoolean(infoJsonObject.getString("isRunning"));
      }
    }
    String noteId = null;
    if (noteJsonObject.has("id")) {
      noteId = noteJsonObject.getString("id");
    }

    String notePath = null;
    if (noteJsonObject.has("path")) {
      notePath = noteJsonObject.getString("path");
    }

    List<ParagraphResult> paragraphResultList = new ArrayList<>();
    if (noteJsonObject.has("paragraphs")) {
      JSONArray paragraphJsonArray = noteJsonObject.getJSONArray("paragraphs");
      for (int i = 0; i< paragraphJsonArray.length(); ++i) {
        paragraphResultList.add(new ParagraphResult(paragraphJsonArray.getJSONObject(i)));
      }
    }

    return new NoteResult(noteId, notePath, isRunning, paragraphResultList);
  }

  /**
   * Execute note with provided noteId, return until note execution is completed.
   * Interpreter process will be stopped after note execution.
   *
   * @param noteId
   * @return
   * @throws Exception
   */
  public NoteResult executeNote(String noteId) throws Exception {
    return executeNote(noteId, new HashMap<>());
  }

  /**
   * Execute note with provided noteId and parameters, return until note execution is completed.
   * Interpreter process will be stopped after note execution.
   *
   * @param noteId
   * @param parameters
   * @return
   * @throws Exception
   */
  public NoteResult executeNote(String noteId, Map<String, String> parameters) throws Exception {
    submitNote(noteId, parameters);
    return waitUntilNoteFinished(noteId);
  }

  /**
   * Submit note to execute with provided noteId, return at once the submission is completed.
   * You need to query {@link NoteResult} by yourself afterwards until note execution is completed.
   * Interpreter process will be stopped after note execution.
   *
   * @param noteId
   * @return
   * @throws Exception
   */
  public NoteResult submitNote(String noteId) throws Exception  {
    return submitNote(noteId, new HashMap<>());
  }

  /**
   * Submit note to execute with provided noteId and parameters, return at once the submission is completed.
   * You need to query {@link NoteResult} by yourself afterwards until note execution is completed.
   * Interpreter process will be stopped after note execution.
   *
   * @param noteId
   * @param parameters
   * @return
   * @throws Exception
   */
  public NoteResult submitNote(String noteId, Map<String, String> parameters) throws Exception  {
    JSONObject bodyObject = new JSONObject();
    bodyObject.put("params", parameters);
    // run note in non-blocking and isolated way.
    HttpResponse<JsonNode> response = Unirest
            .post("/notebook/job/{noteId}")
            .routeParam("noteId", noteId)
            .queryString("blocking", "false")
            .queryString("isolated", "true")
            .header("Content-Type", "application/json")
            .body(bodyObject)
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
    return queryNoteResult(noteId);
  }

  /**
   * Cancel a running note.
   *
   * @param noteId
   * @throws Exception
   */
  public void cancelNote(String noteId) throws Exception {
    HttpResponse<JsonNode> response = Unirest
            .delete("/notebook/job/{noteId}")
            .routeParam("noteId", noteId)
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
  }

  /**
   * Import note with given note json content to the specified notePath.
   * 
   * @param notePath
   * @param noteContent
   * @return
   * @throws Exception
   */
  public String importNote(String notePath, String noteContent) throws Exception {
    JSONObject bodyObject = new JSONObject(noteContent);
    HttpResponse<JsonNode> response = Unirest
            .post("/notebook/import")
            .queryString("notePath", notePath)
            .header("Content-Type", "application/json")
            .body(bodyObject)
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
    return jsonNode.getObject().getString("body");
  }

  /**
   * Block there until note execution is completed.
   *
   * @param noteId
   * @return
   * @throws Exception
   */
  public NoteResult waitUntilNoteFinished(String noteId) throws Exception {
    while (true) {
      NoteResult noteResult = queryNoteResult(noteId);
      if (!noteResult.isRunning()) {
        return noteResult;
      }
      Thread.sleep(clientConfig.getQueryInterval());
    }
  }

  /**
   * Block there until note execution is completed, and throw exception if note execution is not completed
   * in <code>timeoutInMills</code>.
   *
   * @param noteId
   * @param timeoutInMills
   * @return
   * @throws Exception
   */
  public NoteResult waitUntilNoteFinished(String noteId, long timeoutInMills) throws Exception {
    long start = System.currentTimeMillis();
    while (true && (System.currentTimeMillis() - start) < timeoutInMills) {
      NoteResult noteResult = queryNoteResult(noteId);
      if (!noteResult.isRunning()) {
        return noteResult;
      }
      Thread.sleep(clientConfig.getQueryInterval());
    }
    throw new Exception("Note is not finished in " + timeoutInMills / 1000 + " seconds");
  }

  /**
   * Add paragraph to note with provided title and text.
   *
   * @param noteId
   * @param title
   * @param text
   * @return
   * @throws Exception
   */
  public String addParagraph(String noteId, String title, String text) throws Exception {
    JSONObject bodyObject = new JSONObject();
    bodyObject.put("title", title);
    bodyObject.put("text", text);
    HttpResponse<JsonNode> response = Unirest.post("/notebook/{noteId}/paragraph")
            .routeParam("noteId", noteId)
            .header("Content-Type", "application/json")
            .body(bodyObject.toString())
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);

    return jsonNode.getObject().getString("body");
  }

  /**
   * Update paragraph with specified title and text.
   *
   * @param noteId
   * @param paragraphId
   * @param title
   * @param text
   * @throws Exception
   */
  public void updateParagraph(String noteId, String paragraphId, String title, String text) throws Exception {
    JSONObject bodyObject = new JSONObject();
    bodyObject.put("title", title);
    bodyObject.put("text", text);
    HttpResponse<JsonNode> response = Unirest.put("/notebook/{noteId}/paragraph/{paragraphId}")
            .routeParam("noteId", noteId)
            .routeParam("paragraphId", paragraphId)
            .header("Content-Type", "application/json")
            .body(bodyObject.toString())
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
  }

  /**
   * Execute paragraph with parameters in specified session. If sessionId is null or empty string, then it depends on
   * the interpreter binding mode of Note(e.g. isolated per note), otherwise it will run in the specified session.
   *
   * @param noteId
   * @param paragraphId
   * @param sessionId
   * @param parameters
   * @return
   * @throws Exception
   */
  public ParagraphResult executeParagraph(String noteId,
                                          String paragraphId,
                                          String sessionId,
                                          Map<String, String> parameters) throws Exception {
    submitParagraph(noteId, paragraphId, sessionId, parameters);
    return waitUtilParagraphFinish(noteId, paragraphId);
  }

  /**
   * Execute paragraph with parameters.
   *
   * @param noteId
   * @param paragraphId
   * @param parameters
   * @return
   * @throws Exception
   */
  public ParagraphResult executeParagraph(String noteId,
                                          String paragraphId,
                                          Map<String, String> parameters) throws Exception {
    return executeParagraph(noteId, paragraphId, "", parameters);
  }

  /**
   * Execute paragraph in specified session. If sessionId is null or empty string, then it depends on
   * the interpreter binding mode of Note (e.g. isolated per note), otherwise it will run in the specified session.
   *
   * @param noteId
   * @param paragraphId
   * @param sessionId
   * @return
   * @throws Exception
   */
  public ParagraphResult executeParagraph(String noteId,
                                          String paragraphId,
                                          String sessionId) throws Exception {
    return executeParagraph(noteId, paragraphId, sessionId, new HashMap<>());
  }

  /**
   * Execute paragraph.
   *
   * @param noteId
   * @param paragraphId
   * @return
   * @throws Exception
   */
  public ParagraphResult executeParagraph(String noteId, String paragraphId) throws Exception {
    return executeParagraph(noteId, paragraphId, "", new HashMap<>());
  }

  /**
   * Submit paragraph to execute with provided parameters and sessionId. Return at once the submission is completed.
   * You need to query {@link ParagraphResult} by yourself afterwards until paragraph execution is completed.
   *
   * @param noteId
   * @param paragraphId
   * @param sessionId
   * @param parameters
   * @return
   * @throws Exception
   */
  public ParagraphResult submitParagraph(String noteId,
                                         String paragraphId,
                                         String sessionId,
                                         Map<String, String> parameters) throws Exception {
    JSONObject bodyObject = new JSONObject();
    bodyObject.put("params", parameters);
    HttpResponse<JsonNode> response = Unirest
            .post("/notebook/job/{noteId}/{paragraphId}")
            .routeParam("noteId", noteId)
            .routeParam("paragraphId", paragraphId)
            .queryString("sessionId", sessionId)
            .header("Content-Type", "application/json")
            .body(bodyObject.toString())
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
    return queryParagraphResult(noteId, paragraphId);
  }

  /**
   * Submit paragraph to execute with provided sessionId. Return at once the submission is completed.
   * You need to query {@link ParagraphResult} by yourself afterwards until paragraph execution is completed.
   *
   * @param noteId
   * @param paragraphId
   * @param sessionId
   * @return
   * @throws Exception
   */
  public ParagraphResult submitParagraph(String noteId,
                                         String paragraphId,
                                         String sessionId) throws Exception {
    return submitParagraph(noteId, paragraphId, sessionId, new HashMap<>());
  }

  /**
   * Submit paragraph to execute with provided parameters. Return at once the submission is completed.
   * You need to query {@link ParagraphResult} by yourself afterwards until paragraph execution is completed.
   *
   * @param noteId
   * @param paragraphId
   * @param parameters
   * @return
   * @throws Exception
   */
  public ParagraphResult submitParagraph(String noteId,
                                         String paragraphId,
                                         Map<String, String> parameters) throws Exception {
    return submitParagraph(noteId, paragraphId, "", parameters);
  }

  /**
   * Submit paragraph to execute. Return at once the submission is completed.
   * You need to query {@link ParagraphResult} by yourself afterwards until paragraph execution is completed.
   *
   * @param noteId
   * @param paragraphId
   * @return
   * @throws Exception
   */
  public ParagraphResult submitParagraph(String noteId, String paragraphId) throws Exception {
    return submitParagraph(noteId, paragraphId, "", new HashMap<>());
  }

  /**
   * This used by {@link ZSession} for creating or reusing a paragraph for executing another piece of code.
   *
   * @param noteId
   * @param maxParagraph
   * @return
   * @throws Exception
   */
  public String nextSessionParagraph(String noteId, int maxParagraph) throws Exception {
    HttpResponse<JsonNode> response = Unirest
            .post("/notebook/{noteId}/paragraph/next")
            .routeParam("noteId", noteId)
            .header("Content-Type", "application/json")
            .queryString("maxParagraph", maxParagraph)
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);

    return jsonNode.getObject().getString("message");
  }

  /**
   * Cancel a running paragraph.
   *
   * @param noteId
   * @param paragraphId
   * @throws Exception
   */
  public void cancelParagraph(String noteId, String paragraphId) throws Exception {
    HttpResponse<JsonNode> response = Unirest
            .delete("/notebook/job/{noteId}/{paragraphId}")
            .routeParam("noteId", noteId)
            .routeParam("paragraphId", paragraphId)
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
  }

  /**
   * Query {@link ParagraphResult}
   *
   * @param noteId
   * @param paragraphId
   * @return
   * @throws Exception
   */
  public ParagraphResult queryParagraphResult(String noteId, String paragraphId) throws Exception {
    HttpResponse<JsonNode> response = Unirest
            .get("/notebook/{noteId}/paragraph/{paragraphId}")
            .routeParam("noteId", noteId)
            .routeParam("paragraphId", paragraphId)
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);

    JSONObject paragraphJson = jsonNode.getObject().getJSONObject("body");
    return new ParagraphResult(paragraphJson);
  }

  /**
   * Query {@link ParagraphResult} until it is finished.
   *
   * @param noteId
   * @param paragraphId
   * @return
   * @throws Exception
   */
  public ParagraphResult waitUtilParagraphFinish(String noteId, String paragraphId) throws Exception {
    while (true) {
      ParagraphResult paragraphResult = queryParagraphResult(noteId, paragraphId);
      LOGGER.debug(paragraphResult.toString());
      if (paragraphResult.getStatus().isCompleted()) {
        return paragraphResult;
      }
      Thread.sleep(clientConfig.getQueryInterval());
    }
  }

  /**
   * Query {@link ParagraphResult} until it is finished or timeout.
   *
   * @param noteId
   * @param paragraphId
   * @param timeoutInMills
   * @return
   * @throws Exception
   */
  public ParagraphResult waitUtilParagraphFinish(String noteId, String paragraphId, long timeoutInMills) throws Exception {
    long start = System.currentTimeMillis();
    while (true && (System.currentTimeMillis() - start) < timeoutInMills) {
      ParagraphResult paragraphResult = queryParagraphResult(noteId, paragraphId);
      if (paragraphResult.getStatus().isCompleted()) {
        return paragraphResult;
      }
      Thread.sleep(clientConfig.getQueryInterval());
    }
    throw new Exception("Paragraph is not finished in " + timeoutInMills / 1000 + " seconds");
  }

  /**
   * Query {@link ParagraphResult} until it is running.
   *
   * @param noteId
   * @param paragraphId
   * @return
   * @throws Exception
   */
  public ParagraphResult waitUtilParagraphRunning(String noteId, String paragraphId) throws Exception {
    while (true) {
      ParagraphResult paragraphResult = queryParagraphResult(noteId, paragraphId);
      if (paragraphResult.getStatus().isRunning()) {
        return paragraphResult;
      }
      Thread.sleep(clientConfig.getQueryInterval());
    }
  }

  /**
   * This is equal to the restart operation in note page.
   *
   * @param noteId
   * @param interpreter
   */
  public void stopInterpreter(String noteId, String interpreter) throws Exception {
    JSONObject bodyObject = new JSONObject();
    bodyObject.put("noteId", noteId);
    HttpResponse<JsonNode> response = Unirest
            .put("/interpreter/setting/restart/{interpreter}")
            .routeParam("interpreter", interpreter)
            .header("Content-Type", "application/json")
            .body(bodyObject.toString())
            .asJson();
    checkResponse(response);
    JsonNode jsonNode = response.getBody();
    checkJsonNodeStatus(jsonNode);
  }
}
