/*
 * Copyright (c) 2012-2017 Snowflake Computing Inc. All rights reserved.
 */

package net.snowflake.ingest.connection;

import com.fasterxml.jackson.databind.ObjectMapper;
import net.snowflake.ingest.SimpleIngestManager;
import net.snowflake.ingest.utils.StagedFileWrapper;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.util.List;
import java.util.Properties;
import java.util.UUID;

/**
 * This class handles constructing the URIs for our
 * requests as well as putting together the payloads we'll
 * be sending
 *
 * @author obabarinsa
 */
public final class RequestBuilder
{
  //a logger for all of our needs in this class
  private static final Logger LOGGER =
          LoggerFactory.getLogger(RequestBuilder.class.getName());

  //the security manager who will handle token generation
  private SecurityManager securityManager;

  //the default connection scheme is HTTPS
  private static final String DEFAULT_SCHEME = "https";

  //whatever the actual scheme is
  private String scheme;

  //the default port is 443
  private static final int DEFAULT_PORT = 443;

  //the actual port number
  private final int port;

  //the default host is snowflakecomputing.com
  private static final String DEFAULT_HOST = "snowflakecomputing.com";

  //whatever the actual host is
  private final String host;

  //the endpoint format string for inserting files
  private static final String INGEST_ENDPOINT_FORMAT =
                            "/v1/data/pipes/%s/insertFiles";

  //the endpoint for history queries
  private static final String HISTORY_ENDPOINT_FORMAT =
                            "/v1/data/pipes/%s/insertReport";

  // the endpoint for history time range queries
  private static final String HISTORY_RANGE_ENDPOINT_FORMAT =
                            "/v1/data/pipes/%s/loadHistoryScan";

  //optional number of max seconds of items to fetch(eg. in the last hour)
  private static final String RECENT_HISTORY_IN_SECONDS =
                                                            "recentSeconds";

  //optional. if not null, tells us where to start the next request
  private static final String HISTORY_BEGIN_MARK = "beginMark";

  // Start time for the history range
  private static final String HISTORY_RANGE_START_INCLUSIVE = "startTimeInclusive";

  // End time for the history range; is optional
  private static final String HISTORY_RANGE_END_EXCLUSIVE = "endTimeExclusive";

  //the request id parameter name
  private static final String REQUEST_ID = "requestId";

  //the string name for the HTTP auth bearer
  private static final String BEARER_PARAMETER = "Bearer ";

  //and object mapper for all marshalling and unmarshalling
  private static final ObjectMapper objectMapper = new ObjectMapper();

  // Don't change!
  private static final String CLIENT_NAME = "SnowpipeJavaSDK";

  private static final String DEFAULT_VERSION = "0.1.0";
  private static final String RESOURCES_FILE = "project.properties";
  private static final Properties PROPERTIES = loadProperties();

  private static final String USER_AGENT = getUserAgent();

  private static Properties loadProperties()
  {
    Properties properties = new Properties();
    properties.put("version", DEFAULT_VERSION);

    try
    {
      URL res = SimpleIngestManager.class.getClassLoader().
                                          getResource(RESOURCES_FILE);
      if (res == null)
      {
        throw new UncheckedIOException(new FileNotFoundException(RESOURCES_FILE));
      }

      URI uri;
      try
      {
        uri = res.toURI();
      }
      catch (URISyntaxException ex)
      {
        throw new IllegalArgumentException(ex);
      }

      try (InputStream is = Files.newInputStream(Paths.get(uri)))
      {
        properties.load(is);
      }
      catch (IOException ex)
      {
        throw new UncheckedIOException("Failed to load resource", ex);
      }
    }
    catch(Exception e)
    {
      LOGGER.warn("Could not read version info: " + e.toString());
    }

    return properties;
  }

  private static String getUserAgent()
  {
    final String clientVersion = PROPERTIES.getProperty("version");
    final String javaVersion = System.getProperty("java.version");
    final String platform = System.getProperty("os.name") +
                            System.getProperty("os.version") +
                            System.getProperty("os.arch");

    // {client-name}/{version}/{java-version}/{platform}
    final String userAgentFormat = "%s/%s/%s/%s";
    return String.format(userAgentFormat,
                                    CLIENT_NAME,
                                    clientVersion,
                                    javaVersion,
                                    platform);
  }
  /**
   * A simple POJO for generating our POST body to the insert endpoint
   *
   * @author obabarinsa
   */
  private static class IngestRequest
  {
    //the list of files we're loading
    public List<StagedFileWrapper> files;
  }

  /**
   * RequestBuilder - general usage constructor
   *
   * @param accountName - the name of the Snowflake account to which we're connecting
   * @param userName - the username of the entity loading files
   * @param keyPair - the Public/Private key pair we'll use to authenticate
   */
  public RequestBuilder(String accountName, String userName, KeyPair keyPair)
  {
    this(accountName, userName, keyPair,
        DEFAULT_SCHEME, DEFAULT_HOST, DEFAULT_PORT);
  }

  /**
   * RequestBuilder - this constructor is for testing purposes only
   *
   * @param accountName - the account name to which we're connecting
   * @param userName - for whom are we connecting?
   * @param keyPair - our auth credentials
   * @param schemeName - are we HTTP or HTTPS?
   * @param hostName - the host for this snowflake instance
   * @param portNum - the port number
   */
  public RequestBuilder(String accountName,
                        String userName,
                        KeyPair keyPair,
                        String schemeName,
                        String hostName,
                        int portNum)
  {
    //none of these arguments should be null
    if (accountName == null || userName == null || keyPair == null)
    {
      throw new IllegalArgumentException();
    }

    //create our security/token manager
    securityManager = new SecurityManager(accountName, userName, keyPair);

    //stash references to the account and user name as well
    String account = accountName.toUpperCase();
    String user = userName.toUpperCase();

    //save our host, scheme and port info
    port = portNum;
    scheme = schemeName;
    host = hostName;

    LOGGER.info("Creating a RequestBuilder with arguments : " +
            "Account : {}, User : {}, Scheme : {}, Host : {}, Port : {}", account,
        user, scheme, host, port);
  }

  /**
   * Given a request UUID, construct a URIBuilder for the common parts
   * of any Ingest Service request
   *
   * @param requestId the UUID with which we want to label this request
   * @return a URI builder we can use to finish build the URI
   */
  private URIBuilder makeBaseURI(UUID requestId)
  {
    //We can't make a request with no id
    if (requestId == null)
    {
      LOGGER.error("RequestId is null!");
      throw new IllegalArgumentException();
    }

    //construct the builder object
    URIBuilder builder = new URIBuilder();

    //set the scheme
    builder.setScheme(scheme);

    //set the host name
    builder.setHost(host);

    //set the port name
    builder.setPort(port);

    //set the request id
    builder.setParameter(REQUEST_ID, requestId.toString());

    return builder;
  }

  /**
   * makeInsertURI - Given a request UUID, and a fully qualified pipe name
   * make a URI for the Ingest Service inserting
   *
   * @param requestId the UUID we'll use as the label
   * @param pipe the pipe name
   * @return URI for the insert request
   */
  private URI makeInsertURI(UUID requestId, String pipe)
          throws URISyntaxException
  {
    //if the pipe name is null, we have to abort
    if (pipe == null)
    {
      LOGGER.error("Table argument is null");
      throw new IllegalArgumentException();
    }

    //get the base endpoint uri
    URIBuilder builder = makeBaseURI(requestId);

    //add the path for the URI
    builder.setPath(String.format(INGEST_ENDPOINT_FORMAT, pipe));

    //build the final URI
    return builder.build();
  }


  /**
   * makeHistoryURI - Given a request UUID, and a fully qualified pipe name
   * make a URI for the history reporting
   *
   * @param requestId the label for this request
   * @param pipe the pipe name
   * @param recentSeconds history only for items in the recentSeconds window
   * @param beginMark mark from which history should be fetched
   * @return URI for the insert request
   */
  private URI makeHistoryURI(UUID requestId,
                             String pipe,
                             Integer recentSeconds,
                             String beginMark)
                throws URISyntaxException
  {
    //if the table name is null, we have to abort
    if (pipe == null)
    {
      throw new IllegalArgumentException();
    }

    //get the base endpoint UIR
    URIBuilder builder = makeBaseURI(requestId);

    //set the path for the URI
    builder.setPath(String.format(HISTORY_ENDPOINT_FORMAT, pipe));

    if (recentSeconds != null)
    {
      builder.setParameter(RECENT_HISTORY_IN_SECONDS,
                           String.valueOf(recentSeconds));
    }

    if (beginMark != null)
    {
      builder.setParameter(HISTORY_BEGIN_MARK, beginMark);
    }

    LOGGER.info("Final History URIBuilder - {}", builder.toString());
    //build the final URI
    return builder.build();
  }

  /**
   * makeHistoryURI - Given a request UUID, and a fully qualified pipe name
   * make a URI for the history reporting
   *
   * @param requestId the label for this request
   * @param pipe the pipe name
   * @param startTimeInclusive Start time inclusive of scan range, in ISO-8601 format.
   *                  Missing millisecond part in string will lead to a zero
   *                  milliseconds. This is a required query parameter, and a
   *                  400 will be returned if this query parameter is missing
   * @param endTimeExclusive End time exclusive of scan range. If this query parameter
   *             is missing or user provided value is later than current millis,
   *             then current millis is used.
   * @return URI for the insert request
   */
  private URI makeHistoryRangeURI(UUID requestId,
                             String pipe,
                             String startTimeInclusive,
                             String endTimeExclusive)
          throws URISyntaxException
  {
    //if the table name is null, we have to abort
    if (pipe == null)
    {
      throw new IllegalArgumentException();
    }

    //get the base endpoint UIR
    URIBuilder builder = makeBaseURI(requestId);

    //set the path for the URI
    builder.setPath(String.format(HISTORY_RANGE_ENDPOINT_FORMAT, pipe));

    if (startTimeInclusive != null)
    {
      builder.setParameter(HISTORY_RANGE_START_INCLUSIVE, startTimeInclusive);
    }

    if (endTimeExclusive != null)
    {
      builder.setParameter(HISTORY_RANGE_END_EXCLUSIVE, endTimeExclusive);
    }

    LOGGER.info("Final History URIBuilder - {}", builder.toString());
    //build the final URI
    return builder.build();
  }
  /**
   * generateFilesJSON - Given a list of files, make some json to represent it
   *
   * @param files the list of files we want to send
   * @return the string json blob
   */
  private String generateFilesJSON(List<StagedFileWrapper> files)
  {
    //if the files argument is null, throw
    if (files == null)
    {
      LOGGER.info("Null files argument in RequestBuilder");
      throw new IllegalArgumentException();
    }

    //create pojo
    IngestRequest pojo = new IngestRequest();
    pojo.files = files;

    //serialize to a string
    try
    {
      return objectMapper.writeValueAsString(pojo);
    }
    //if we have an exception we need to log and throw
    catch (Exception e)
    {
      LOGGER.error("Unable to Generate JSON Body for Insert request");
      throw new RuntimeException();
    }
  }

  /**
   * addUserAgent - adds the user agent header to a request
   *
   * @param request the URI request
   */
  private static void addUserAgent(HttpUriRequest request)
  {
    request.setHeader(HttpHeaders.USER_AGENT, USER_AGENT);
  }

  /**
   * addToken - adds a the JWT token to a request
   *
   * @param request the URI request
   * @param token the token to add
   */
  private static void addToken(HttpUriRequest request, String token)
  {
    request.setHeader(HttpHeaders.AUTHORIZATION, BEARER_PARAMETER + token);
  }

  private static void addHeaders(HttpUriRequest request,
                                 String token)
  {
    addUserAgent(request);
    //add the auth token
    addToken(request, token);
  }

  /**
   * generateInsertRequest - given a table, stage and list of files,
   * make a request for the insert endpoint
   *
   * @param requestId a UUID we will use to label this request
   * @param pipe a fully qualified pipe name
   * @param files a list of files
   * @return a post request with all the data we need
   * @throws URISyntaxException if the URI components provided are improper
   */
  public HttpPost generateInsertRequest(UUID requestId, String pipe,
                                        List<StagedFileWrapper> files)
      throws URISyntaxException
  {
    //make the insert URI
    URI insertURI = makeInsertURI(requestId, pipe);
    LOGGER.info("Created Insert Request : {} ", insertURI);

    //Make the post request
    HttpPost post = new HttpPost(insertURI);

    addHeaders(post, securityManager.getToken());

    //the entity for the containing the json
    final StringEntity entity = new StringEntity(generateFilesJSON(files),
                                                 ContentType.APPLICATION_JSON);
    post.setEntity(entity);

    return post;
  }

  /**
   * generateHistoryRequest - given a requestId and a pipe, make a history request
   *
   * @param requestId a UUID we will use to label this request
   * @param pipe a fully qualified pipe name
   * @param recentSeconds history only for items in the recentSeconds window
   * @param beginMark mark from which history should be fetched
   * @return a get request with all the data we need
   * @throws URISyntaxException - If the URI components provided are improper
   */
  public HttpGet generateHistoryRequest(UUID requestId,
                                        String pipe,
                                        Integer recentSeconds,
                                        String beginMark)
      throws URISyntaxException
  {
    //make the history URI
    URI historyURI = makeHistoryURI(requestId, pipe, recentSeconds, beginMark);

    //make the get request
    HttpGet get = new HttpGet(historyURI);

    addHeaders(get, securityManager.getToken());

    return get;
  }

  /**
  * generateHistoryRangeRequest -
   * given a requestId and a pipe, get history for all ingests between
   *  time ranges start-end
  *
  * @param requestId a UUID we will use to label this request
  * @param pipe a fully qualified pipe name
  * @param startTimeInclusive Start time inclusive of scan range, in ISO-8601 format.
  *                  Missing millisecond part in string will lead to a zero
  *                  milliseconds. This is a required query parameter, and a
  *                  400 will be returned if this query parameter is missing
  * @param endTimeExclusive End time exclusive of scan range. If this query parameter
  *             is missing or user provided value is later than current millis,
  *                           then current millis is used.
  * @return URI for the insert request
  */
  public HttpGet generateHistoryRangeRequest(UUID requestId,
                                             String pipe,
                                             String startTimeInclusive,
                                             String endTimeExclusive)
      throws URISyntaxException
  {
    URI historyRangeURI = makeHistoryRangeURI(requestId,
                                              pipe,
                                              startTimeInclusive,
                                              endTimeExclusive);

    HttpGet get = new HttpGet(historyRangeURI);

    addHeaders(get, securityManager.getToken());

    return get;
  }

  /**
   * Closes the resources being used by RequestBuilder object.
   * {@link SecurityManager} is one such resource which uses a threadpool
   * which needs to be shutdown once SimpleIngestManager is done interacting
   * with Snowpipe Service (Rest APIs)
   * @throws Exception
   */
  public void closeResources()
  {
    securityManager.close();
  }
}