package aql

import (
	"encoding/json"
	"errors"
	"fmt"

	"git.science.uu.nl/datastrophe/query-conversion/aql/entity"
)

/*
ConvertQuery converts a json string to an AQL query

Parameters: jsonMsg is the JSON file directly outputted by the drag and drop query builder in the frontend

Return: a string containing the corresponding AQL query, a string containing the database name and an error */
func (s *Service) ConvertQuery(jsonMsg *[]byte) (*string, *string, error) {

	jsonStruct, err := convertJSONToStruct(jsonMsg)
	if err != nil {
		return nil, nil, err
	}

	// Check to make sure all indexes exist
	// How many entities are there
	numEntities := len(jsonStruct.Entities) - 1
	// How many relations there are
	numRelations := len(jsonStruct.Relations) - 1

	// Make sure no entity should be returned that is outside the range of that list
	for _, e := range jsonStruct.Return.Entities {
		// If this entity references an entity that is outside the range
		if e > numEntities || e < 0 {
			return nil, nil, errors.New("non-existing entity referenced in return")
		}
	}

	// Make sure that no relation mentions a non-existing entity
	for _, r := range jsonStruct.Relations {
		if r.EntityFrom > numEntities || r.EntityTo > numEntities {
			return nil, nil, errors.New("non-exisiting entity referenced in relation")
		}
	}

	// Make sure no non-existing relation is tried to be returned
	for _, r := range jsonStruct.Return.Relations {
		if r > numRelations || r < 0 {
			return nil, nil, errors.New("non-existing relation referenced in return")
		}
	}

	result := createQuery(jsonStruct)
	return result, &jsonStruct.DatabaseName, nil
}

/* convertJSONtoStruct reads a JSON file and sorts the data into the appropriate structs
Parameters: jsonMsg is the JSON file directly outputted by the drag and drop query builder in the frontend

Return: parsedJSON is a struct with the same structure and holding the same data as jsonMsg
*/
func convertJSONToStruct(jsonMsg *[]byte) (*entity.QueryParsedJSON, error) {
	jsonStruct := entity.QueryParsedJSON{}
	err := json.Unmarshal(*jsonMsg, &jsonStruct)

	if err != nil {
		return nil, err
	}

	return &jsonStruct, nil
}

/* createQuery generates a query based on the json file provided
Parameters: jsonQuery is a parsedJSON struct holding all the data needed to form a query

Return: a string containing the corresponding AQL query and an error
*/
func createQuery(jsonQuery *entity.QueryParsedJSON) *string {
	// Note: Case #4, where there is an edge only query (without any entity), is not supported by frontend

	// If a modifier is used, disable the limit
	if len(jsonQuery.Modifiers) > 0 {
		jsonQuery.Limit = -1
	}

	var (
		relationsToReturn []string
		nodesToReturn     []string
		nodeUnion         string
		relationUnion     string
	)

	// Loop over all relations
	ret := ""

	for i, relation := range jsonQuery.Relations {

		relationName := fmt.Sprintf("r%v", i)

		if relation.EntityFrom >= 0 {
			// if there is a from-node
			// create the let for this node
			fromName := fmt.Sprintf("n%v", relation.EntityFrom)

			ret += *createNodeLet(&jsonQuery.Entities[relation.EntityFrom], &fromName)

			ret += *createRelationLetWithFromEntity(&relation, relationName, &jsonQuery.Entities, jsonQuery.Limit)
		} else if relation.EntityTo >= 0 {
			// if there is only a to-node
			toName := fmt.Sprintf("n%v", relation.EntityTo)

			ret += *createNodeLet(&jsonQuery.Entities[relation.EntityTo], &toName)

			ret += *createRelationLetWithOnlyToEntity(&relation, relationName, &jsonQuery.Entities, jsonQuery.Limit)
			// Add this relation to the list
		} else {
			fmt.Println("Relation-only queries are currently not supported")
			continue
		}

		// Add this relation to the list
		relationsToReturn = append(relationsToReturn, relationName)
	}

	// Add node let statements for nodes that are not yet returned
	// Create a set from all the entity-from's and entity-to's, to check if they are returned
	nodeSet := make(map[int]bool)
	for _, relation := range jsonQuery.Relations {
		nodeSet[relation.EntityFrom] = true
		nodeSet[relation.EntityTo] = true
	}

	// Check if the entities to return are already returned
	for _, entityIndex := range jsonQuery.Return.Entities {
		if !nodeSet[entityIndex] {
			// If not, return this node
			name := fmt.Sprintf("n%v", entityIndex)
			ret += *createNodeLet(&jsonQuery.Entities[entityIndex], &name)

			// Add this node to the list
			nodesToReturn = append(nodesToReturn, name)
		}
	}

	//If there are modifiers within the query, we run a different set of checks which focus on quantifiable aspects
	if len(jsonQuery.Modifiers) > 0 {
		modifier := jsonQuery.Modifiers[0]
		// There is a distinction between (relations and entities) and (relations or entities)
		if len(jsonQuery.Return.Relations) > 0 && len(jsonQuery.Return.Entities) > 0 {

			var pathDistinction string // .vertices or .edges

			// Select the correct addition to the return of r0[**]
			if modifier.SelectedType == "entity" {
				// ASSUMING THERE IS ONLY 1 RELATION
				if jsonQuery.Relations[0].EntityFrom == modifier.ID {
					pathDistinction = fmt.Sprintf(".vertices[%v]", jsonQuery.Relations[0].Depth.Min-1)

				} else {
					pathDistinction = fmt.Sprintf(".vertices[%v]", jsonQuery.Relations[0].Depth.Max)

				}
			} else {
				pathDistinction = ".edges[**]"
			}

			// Getting the attribute if there is one
			if modifier.AttributeIndex != -1 {
				if modifier.SelectedType == "entity" {
					pathDistinction += fmt.Sprintf(".%v", jsonQuery.Entities[modifier.ID].Constraints[modifier.AttributeIndex].Attribute)

				} else {
					pathDistinction += fmt.Sprintf(".%v", jsonQuery.Relations[modifier.ID].Constraints[modifier.AttributeIndex].Attribute)

				}
			}

			// If count is used it has to be replaced with Length + unique else use the modifier type
			if modifier.Type == "COUNT" {
				ret += fmt.Sprintf("RETURN LENGTH (unique(r0[*]%v))", pathDistinction)

			} else {
				ret += fmt.Sprintf("RETURN %v (r0[*]%v)", modifier.Type, pathDistinction)

			}

		} else {
			// Check if the modifier is on an attribute
			if modifier.AttributeIndex == -1 {
				ret += fmt.Sprintf("RETURN LENGTH (n%v)", modifier.ID)
			} else {
				var attribute string

				// Selecting the right attribute from either the entity constraint or relation constraint
				if modifier.SelectedType == "entity" {
					attribute = jsonQuery.Entities[modifier.ID].Constraints[modifier.AttributeIndex].Attribute

				} else {
					attribute = jsonQuery.Relations[modifier.ID].Constraints[modifier.AttributeIndex].Attribute

				}

				// If count is used it has to be replaced with Length + unique else use the modifier type
				if modifier.Type == "COUNT" {
					ret += fmt.Sprintf("RETURN LENGTH (unique(n%v[*].%v))", modifier.ID, attribute)

				} else {
					ret += fmt.Sprintf("RETURN %v (n%v[*].%v)", modifier.Type, modifier.ID, attribute)

				}
			}
		}

	} else {

		// Create UNION statements that create unique lists of all the nodes and relations
		// Thus removing all duplicates
		nodeUnion = "\nLET nodes = first(RETURN UNION_DISTINCT("
		for _, relation := range relationsToReturn {
			nodeUnion += fmt.Sprintf("flatten(%v[**].vertices), ", relation)
		}

		for _, node := range nodesToReturn {
			nodeUnion += fmt.Sprintf("%v,", node)
		}
		nodeUnion += "[],[]))\n"

		relationUnion = "LET edges = first(RETURN UNION_DISTINCT("
		for _, relation := range relationsToReturn {
			relationUnion += fmt.Sprintf("flatten(%v[**].edges), ", relation)
		}
		relationUnion += "[],[]))\n"

		ret += nodeUnion + relationUnion
		ret += "RETURN {\"vertices\":nodes, \"edges\":edges }"

	}

	return &ret
}

/* createNodeLet generates a 'LET' statement for a node related query
Parameters: node is an entityStruct containing the information of a single node,
name is the autogenerated name of the node consisting of "n" + the index of the node

Return: a string containing a single LET-statement in AQL
*/
func createNodeLet(node *entity.QueryEntityStruct, name *string) *string {
	header := fmt.Sprintf("LET %v = (\n\tFOR x IN %v \n", *name, node.Type)
	footer := "\tRETURN x\n)\n"
	constraints := *createConstraintStatements(&node.Constraints, "x", false)

	ret := header + constraints + footer
	return &ret
}

/* createRelationLetWithFromEntity generates a 'LET' statement for relations with an 'EntityFrom' property and optionally an 'EntitiyTo' property
Parameters: relation is a relation struct containing the information of a single relation,
name is the autogenerated name of the node consisting of "r" + the index of the relation,
entities is a list of entityStructs that are needed to form the relation LET-statement

Return: a string containing a single LET-statement in AQL
*/
func createRelationLetWithFromEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int) *string {
	header := fmt.Sprintf("LET %v = (\n\tFOR x IN n%v \n", name, relation.EntityFrom)
	forStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v OUTBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Type)

	// Guarantees that there is no path returned with a duplicate edge
	// This way there are no cycle paths possible, TODO: more research about this needed
	optionStmtn := "\tOPTIONS { uniqueEdges: \"path\" }\n"

	vFilterStmnt := ""
	if relation.EntityTo != -1 {
		// If there is a to-node, generate the filter statement
		toConstraints := (*entities)[relation.EntityTo].Constraints
		vFilterStmnt += *createConstraintStatements(&toConstraints, "v", false)

		// Add a WITH statement if the collection of entityTo is not yet included
		if (*entities)[(*relation).EntityFrom].Type != (*entities)[(*relation).EntityTo].Type {
			header = fmt.Sprintf("WITH %v\n %v", (*entities)[(*relation).EntityTo].Type, header)
		}
	}

	relationFilterStmnt := *createConstraintStatements(&relation.Constraints, "p", true)

	// Dont use a limit on quantifing queries
	footer := ""
	if limit != -1 {
		footer += fmt.Sprintf("\tLIMIT %v \n", limit)
	}
	footer += "RETURN DISTINCT p )\n"

	ret := header + forStatement + optionStmtn + vFilterStmnt + relationFilterStmnt + footer
	return &ret
}

/* createRelationLetWithOnlyToEntity generates a 'LET' statement for relations with only an 'EntityTo' property
Parameters: relation is a relation struct containing the information of a single relation,
name is the autogenerated name of the node consisting of "r" + the index of the relation,
entities is a list of entityStructs that are needed to form the relation LET-statement

Return: a string containing a single LET-statement in AQL
*/
func createRelationLetWithOnlyToEntity(relation *entity.QueryRelationStruct, name string, entities *[]entity.QueryEntityStruct, limit int) *string {
	header := fmt.Sprintf("LET %v = (\n\tFOR x IN n%v \n", name, relation.EntityTo)
	forStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v INBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Type)

	// Guarantees that there is no path returned with a duplicate edge
	// This way there are no cycle paths possible, TODO: more research about this needed
	optionStmtn := "\tOPTIONS { uniqueEdges: \"path\" }\n"

	relationFilterStmnt := *createConstraintStatements(&relation.Constraints, "p", true)

	// Dont use a limit on quantifing queries
	footer := ""
	if limit != -1 {
		footer += fmt.Sprintf("\tLIMIT %v \n", limit)
	}
	footer += "RETURN DISTINCT p )\n"

	ret := header + forStatement + optionStmtn + relationFilterStmnt + footer
	return &ret
}