/*
 * 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
 *
 *   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.streams.neo4j;

import org.apache.streams.graph.QueryGraphHelper;
import org.apache.streams.jackson.StreamsJacksonMapper;
import org.apache.streams.pojo.json.Activity;
import org.apache.streams.pojo.json.ActivityObject;
import org.apache.streams.util.PropertyUtil;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;

import org.apache.commons.lang3.StringEscapeUtils;
import org.javatuples.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.stringtemplate.v4.ST;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;

/**
 * Supporting class for interacting with neo4j via rest API
 */
public class CypherQueryGraphHelper implements QueryGraphHelper, Serializable {

  private static final ObjectMapper MAPPER = StreamsJacksonMapper.getInstance()
    .configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false)
    .configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, false);

  private static final Logger LOGGER = LoggerFactory.getLogger(CypherQueryGraphHelper.class);

  public static final String getVertexLongIdStatementTemplate = "MATCH (v) WHERE ID(v) = <id> RETURN v";
  public static final String getVertexStringIdStatementTemplate = "MATCH (v {id: '<id>'} ) RETURN v";
  public static final String getVerticesLabelIdStatementTemplate = "MATCH (v:<type>) RETURN v";

  public final static String createVertexStatementTemplate = "MATCH (x {id: '<id>'}) "+
      "CREATE UNIQUE (v:`<type>` { props }) "+
      "ON CREATE SET v <labels> "+
      "RETURN v";

  public final static String mergeVertexStatementTemplate = "MERGE (v:`<type>` {id: '<id>'}) "+
      "ON CREATE SET v <labels>, v = { props }, v.`@timestamp` = timestamp() "+
      "ON MATCH SET v <labels>, v = { props }, v.`@timestamp` = timestamp() "+
      "RETURN v";

  public final static String createEdgeStatementTemplate = "MATCH (s:`<s_type>` {id: '<s_id>'}),(d:`<d_type>` {id: '<d_id>'}) "+
      "CREATE UNIQUE (s)-[r:`<r_type>` <r_props>]->(d) "+
      "RETURN r";

  public Pair<String, Map<String, Object>> getVertexRequest(String streamsId) {

    ST getVertex = new ST(getVertexStringIdStatementTemplate);
    getVertex.add("id", streamsId);

    Pair<String, Map<String, Object>> queryPlusParameters = new Pair(getVertex.render(), null);

    LOGGER.debug("getVertexRequest", queryPlusParameters.toString());

    return queryPlusParameters;
  }

  /**
   * getVertexRequest.
   * @param vertexId numericId
   * @return pair (streamsId, parameterMap)
   */
  public Pair<String, Map<String, Object>> getVertexRequest(Long vertexId) {

    ST getVertex = new ST(getVertexLongIdStatementTemplate);
    getVertex.add("id", vertexId);

    Pair<String, Map<String, Object>> queryPlusParameters = new Pair(getVertex.render(), null);

    LOGGER.debug("getVertexRequest", queryPlusParameters.toString());

    return queryPlusParameters;

  }

  /**
   * createVertexRequest.
   * @param activityObject activityObject
   * @return pair (query, parameterMap)
   */
  public Pair<String, Map<String, Object>> createVertexRequest(ActivityObject activityObject) {

    Objects.requireNonNull(activityObject.getObjectType());

    List<String> labels = getLabels(activityObject);

    ST createVertex = new ST(createVertexStatementTemplate);
    createVertex.add("id", activityObject.getId());
    createVertex.add("type", activityObject.getObjectType());

    if ( labels.size() > 0 ) {
      createVertex.add("labels", String.join(" ", labels));
    }

    String query = createVertex.render();

    ObjectNode object = MAPPER.convertValue(activityObject, ObjectNode.class);
    object = PropertyUtil.getInstance(MAPPER).cleanProperties(object);
    Map<String, Object> props = PropertyUtil.getInstance(MAPPER).flattenToMap(object);

    Pair<String, Map<String, Object>> queryPlusParameters = new Pair(createVertex.render(), props);

    LOGGER.debug("createVertexRequest: ({},{})", query, props);

    return queryPlusParameters;
  }

  /**
   * getVerticesRequest gets all vertices with a label.
   * @param labelId labelId
   * @return pair (query, parameterMap)
   */
  public Pair<String, Map<String, Object>> getVerticesRequest(String labelId) {
    ST getVertex = new ST(getVerticesLabelIdStatementTemplate);
    getVertex.add("type", labelId);

    Pair<String, Map<String, Object>> queryPlusParameters = new Pair(getVertex.render(), null);

    LOGGER.debug("getVertexRequest", queryPlusParameters.toString());

    return queryPlusParameters;
  }

  /**
   * mergeVertexRequest.
   * @param activityObject activityObject
   * @return pair (query, parameterMap)
   */
  public Pair<String, Map<String, Object>> mergeVertexRequest(ActivityObject activityObject) {

    Objects.requireNonNull(activityObject.getObjectType());

    Pair queryPlusParameters = new Pair(null, new HashMap<>());

    List<String> labels = getLabels(activityObject);

    ST mergeVertex = new ST(mergeVertexStatementTemplate);
    mergeVertex.add("id", activityObject.getId());
    mergeVertex.add("type", activityObject.getObjectType());
    if ( labels.size() > 0 ) {
      mergeVertex.add("labels", String.join(" ", labels));
    }
    String query = mergeVertex.render();

    ObjectNode object = MAPPER.convertValue(activityObject, ObjectNode.class);
    object = PropertyUtil.getInstance(MAPPER).cleanProperties(object);
    Map<String, Object> props = PropertyUtil.getInstance(MAPPER).flattenToMap(object);

    LOGGER.debug("mergeVertexRequest: ({},{})", query, props);

    queryPlusParameters = queryPlusParameters.setAt0(query);
    queryPlusParameters = queryPlusParameters.setAt1(props);

    return queryPlusParameters;
  }

  /**
   * createActorObjectEdge.
   * @param activity activity
   * @return pair (query, parameterMap)
   */
  public Pair<String, Map<String, Object>> createActorObjectEdge(Activity activity) {

    Pair queryPlusParameters = new Pair(null, new HashMap<>());

    ObjectNode object = MAPPER.convertValue(activity, ObjectNode.class);
    object = PropertyUtil.getInstance(MAPPER).cleanProperties(object);
    Map<String, Object> props = PropertyUtil.getInstance(MAPPER).flattenToMap(object);

    ST mergeEdge = new ST(createEdgeStatementTemplate);
    mergeEdge.add("s_id", activity.getActor().getId());
    mergeEdge.add("s_type", activity.getActor().getObjectType());
    mergeEdge.add("d_id", activity.getObject().getId());
    mergeEdge.add("d_type", activity.getObject().getObjectType());
    mergeEdge.add("r_id", activity.getId());
    mergeEdge.add("r_type", activity.getVerb());
    mergeEdge.add("r_props", getActorObjectEdgePropertyCreater(props));

    String statement = mergeEdge.render();
    queryPlusParameters = queryPlusParameters.setAt0(statement);
    queryPlusParameters = queryPlusParameters.setAt1(props);

    LOGGER.debug("createActorObjectEdge: ({},{})", statement, props);

    return queryPlusParameters;
  }

  /**
   * createActorTargetEdge.
   * @param activity activity
   * @return pair (query, parameterMap)
   */
  public Pair<String, Map<String, Object>> createActorTargetEdge(Activity activity) {

    Pair queryPlusParameters = new Pair(null, new HashMap<>());

    ObjectNode object = MAPPER.convertValue(activity, ObjectNode.class);
    object = PropertyUtil.getInstance(MAPPER).cleanProperties(object);
    Map<String, Object> props = PropertyUtil.getInstance(MAPPER).flattenToMap(object);

    ST mergeEdge = new ST(createEdgeStatementTemplate);
    mergeEdge.add("s_id", activity.getActor().getId());
    mergeEdge.add("s_type", activity.getActor().getObjectType());
    mergeEdge.add("d_id", activity.getTarget().getId());
    mergeEdge.add("d_type", activity.getTarget().getObjectType());
    mergeEdge.add("r_id", activity.getId());
    mergeEdge.add("r_type", activity.getVerb());
    mergeEdge.add("r_props", getActorTargetEdgePropertyCreater(props));

    String statement = mergeEdge.render();
    queryPlusParameters = queryPlusParameters.setAt0(statement);
    queryPlusParameters = queryPlusParameters.setAt1(props);

    LOGGER.debug("createActorObjectEdge: ({},{})", statement, props);

    return queryPlusParameters;
  }

  /**
   * getPropertyValueSetter.
   * @param map paramMap
   * @return PropertyValueSetter string
   */
  public static String getPropertyValueSetter(Map<String, Object> map, String symbol) {
    StringBuilder builder = new StringBuilder();
    for( Map.Entry<String, Object> entry : map.entrySet()) {
      if( entry.getValue() instanceof String ) {
        String propVal = (String)(entry.getValue());
        builder.append("," + symbol + ".`" + entry.getKey() + "` = '" + StringEscapeUtils.escapeJava(propVal) + "'");
      }
    }
    return builder.toString();
  }

  /**
   * getPropertyParamSetter.
   * @param map paramMap
   * @return PropertyParamSetter string
   */
  public static String getPropertyParamSetter(Map<String, Object> map, String symbol) {
    StringBuilder builder = new StringBuilder();
    for( Map.Entry<String, Object> entry : map.entrySet()) {
      if( entry.getValue() instanceof String ) {
        String propVal = (String)(entry.getValue());
        builder.append("," + symbol + ".`" + entry.getKey() + "` = '" + StringEscapeUtils.escapeJava(propVal) + "'");
      }
    }
    return builder.toString();
  }

  /**
   * getPropertyCreater.
   * @param map paramMap
   * @return PropertyCreater string
   */
  public static String getPropertyCreater(Map<String, Object> map) {
    StringBuilder builder = new StringBuilder();
    builder.append("{ ");
    List<String> parts = new ArrayList<>();
    for( Map.Entry<String, Object> entry : map.entrySet()) {
      if( entry.getValue() instanceof String ) {
        String propVal = (String) (entry.getValue());
        parts.add("`"+entry.getKey() + "`:'" + StringEscapeUtils.escapeJava(propVal) + "'");
      }
    }
    builder.append(String.join(" ", parts));
    builder.append(" }");
    return builder.toString();
  }

  private String getActorObjectEdgePropertyCreater(Map<String, Object> map) {
    StringBuilder builder = new StringBuilder();
    builder.append("{ ");
    List<String> parts = new ArrayList<>();
    for( Map.Entry<String, Object> entry : map.entrySet()) {
      if( entry.getValue() instanceof String ) {
        String propVal = (String) (entry.getValue());
        if( !entry.getKey().contains(".")) {
          parts.add("`"+entry.getKey() + "`: '" + StringEscapeUtils.escapeJava(propVal) + "'");
        }
      }
    }
    builder.append(String.join(", ", parts));
    builder.append(" }");
    return builder.toString();
  }

  private String getActorTargetEdgePropertyCreater(Map<String, Object> map) {
    StringBuilder builder = new StringBuilder();
    builder.append("{ ");
    List<String> parts = new ArrayList<>();
    for( Map.Entry<String, Object> entry : map.entrySet()) {
      if( entry.getValue() instanceof String ) {
        String propVal = (String) (entry.getValue());
        if( !entry.getKey().contains(".")) {
          parts.add("`"+entry.getKey() + "`: '" + StringEscapeUtils.escapeJava(propVal) + "'");
        } else if( entry.getKey().startsWith("object.") && !entry.getKey().contains(".id")) {
          parts.add("`"+entry.getKey().substring("object.".length()) + "`: '" + StringEscapeUtils.escapeJava(propVal) + "'");
        }
      }
    }
    builder.append(String.join(", ", parts));
    builder.append(" }");
    return builder.toString();
  }

  /**
   * getLabels.
   * @param activityObject activityObject
   * @return PropertyCreater string
   */
  public static List<String> getLabels(ActivityObject activityObject) {
    List<String> labels = Collections.singletonList(":streams");
    if ( activityObject.getAdditionalProperties().containsKey("labels") ) {
      List<String> extraLabels = (List<String>)activityObject.getAdditionalProperties().get("labels");
      for ( String extraLabel : extraLabels ) {
        labels.add(":" + extraLabel);
      }
    }
    return labels;
  }

}
