{- |
Module      : GraphQLdbi
Description : Here are methods to interpret your GraphQL-SQL query and process the Persistent style results
License     : IPS
Maintainer  : jasonsychau@live.ca
Stability   : provisional

<https://graphql.github.io/ Here> is a link to the official documents. You can learn and get a feel of how can you implement this package which is a GraphQL-to-SQL translator with Persistent package style return-value processing to make a GraphQL format return object string.

This module is made to enable interpreting your GraphQL queries. The expected query type is a single string to comprise all your GraphQL queries and fragments. Expected variable type is a single string to your variable-to-value definitions.

When errors are encountered, this module is simply going to throw an uncaught Exception. The Exception name is some hint to where was the error encountered. More information is provided in the below function description.

Not all GraphQL features are currently supported. For a list to updates and current state, you should check the GitHub updates and example <https://github.com/jasonsychau/graphql-w-persistent page>.

-}
module GraphQLdbi (
    -- * Functions
    -- | Here is three methods to the package interpretation, validation, and formatting features.
    module GraphQLdbi,
    -- * Server exceptions
    -- | These are server exceptions to make error handling.
    module Model.ServerExceptions
    ) where

import Data.Text (Text)
import Control.Monad.IO.Class (liftIO,MonadIO())
import Control.Exception (throw)
import Components.Parsers.QueryParser (validateQuery,parseStringToObjects,processString)
import Components.ObjectHandlers.ServerObjectValidator (checkObjectsAttributes,replaceObjectsVariables)
import Components.QueryComposers.SQLQueryComposer (makeSqlQueries)
import Components.Parsers.ServerSchemaJsonParser (fetchArguments)
import Components.ObjectHandlers.ServerObjectTrimmer (mergeDuplicatedRootObjects)
import Components.Parsers.VariablesParser (parseVariables)
import Components.DataProcessors.ListDataProcessor (processReturnedValues)
import Model.ServerExceptions
import Model.ServerObjectTypes (RootObject,SchemaSpecs,QueryData)


{- |
   This function is to parse your schema. The return values are passed to the other two functions to interpret your schema. You can define your schema with tuples and lists without reading a json file as defined in below function, but you will not receive type and duplicate checking from this function.

   __Schema:__

   The schema json file is formated as one json object to describe the GraphQL objects and the object heirarchy.
   Only "PrimitiveObjects" are valid descendants to "ParentalObjects".
   Only the shared object fields and shared scalar fields are parental object fields.
   If a parental object is a declared field from a primitive object, all descendants are supposed to be declared in the primitive object database relationships.
   When declaring any primitive or parental object, the pseudonym is supposed to list all possible reference words to the object (in root object or nested object)
   Primitive objects are expected to have a unique "id" column in the declared database table.
   Database relationships are defined as an ordered sequence from the identity table to the target association table.
   The order is identity table, identity table join field(s), target table, target table join field(s), then (multiple) triplets (of intermediate table, intermediate table to-join field, then intermediate table from-join field in order of nearness from identity table) if relevant.
   If multiple fields are defining the join, one declares every field with a space " " separation.
   For example, the declaration is [A,"a b", B, "c d"] if the join condition is A.a=B.c and A.b = B.d.
   You can add schema associations as declaring more fields in the primitive objects.

   __Scalar fields:__

   Scalar fields are declared with a type, and they are cast to corresponding JSON format when making the GraphQL result.
   Valid scalar field types are Text, ByteString, Int, Double, Rational, Bool, Day, TimeOfDay, or UTCTime.

   __Function exceptions:__

   Exceptions are returned when error is faced. This method returns errors of:

    * ImportSchemaException (when there is a problem with reading your schema)
    * ImportSchemaServerNameException (when there is a problem with reading your servername argument)
    * ImportSchemaPseudonymsException (when there is a problem with reading your pseudonyms list argument)
    * ImportSchemaScalarFieldsException (when there is a problem with reading your scalarfields list argument)
    * ImportSchemaObjectFieldsException (when there is a problem with reading your objectfields list argument)
    * ImportSchemaDatabaseTablesException (when there is a problem with reading your databasetables list argument)
    * ImportSchemaDatabaseRelationshipsException (when there is a problem with reading your databaserelationships list argument)
    * ImportSchemaDuplicateException (when two or more Server objects are sharing the same name)

   __Important note:__
   
   It is assumed that all nested objects are corresponding to a database table with a primary key that's named "id".

   __Schema format example:__

   Here is an example schema to look at formatting:

   @
    {
      "ParentalObjects":[
        {
          "ServerName":"Taxonomy",
          "Pseudonyms":[
            "taxonomy",
            "Taxonomy"
          ],
          "ServerChildren":[
            "Breed",
            "Species",
            "Family",
            "Genus"
          ]
        }
      ],
      "PrimitiveObjects":[
        {
          "ServerName": "Person",
          "Pseudonyms": [
            "person",
            "Person",
            "owner"
          ],
          "ScalarFields": [
            {
              "Name": "id",
              "Type": "Int",
              "Arguments": []
            },
            {
              "Name": "name",
              "Type": "Text",
              "Arguments": []
            },
            {
              "Name": "gender",
              "Type": "Int",
              "Arguments": [
                {
                  "Name": "as",
                  "Options": [
                    {
                      "Name": "MALEFEMALE",
                      "Type": "Text",
                      "Prefix": "CASE WHEN ",
                      "Suffix": "==1 THEN 'MALE' ELSE 'FEMALE' END"
                    }
                  ]
                }
              ]
            }
          ],
          "ObjectFields": [
            "pet",
            "breed",
            "species",
            "genus",
            "family",
            "taxonomy"
          ],
          "DatabaseTable": "person",
          "DatabaseRelationships": [
            ["person","id","pet","id","pet_ownership","owner_id","animal_id"],
            ["person","id","breed","id","pet_ownership","owner_id","animal_id","pet","id","id","pet_type","pet_id","breed_id"],
            ["person","id","species","id","pet_ownership","owner_id","animal_id","pet","id","id","pet_type","pet_id","breed_id","breed","id","species_id"],
            ["person","id","genus","id","pet_ownership","owner_id","animal_id","pet","id","id","pet_type","pet_id","breed_id","breed","id","species_id","species","id","genus_id"],
            ["person","id","family","id","pet_ownership","owner_id","animal_id","pet","id","id","pet_type","pet_id","breed_id","breed","id","species_id","species","id","genus_id","genus","id","family_id"]
          ]
        },
        {
          "ServerName": "Family",
          "Pseudonyms": [
            "Family",
            "family"
          ],
          "ScalarFields": [
            {
              "Name": "id",
              "Type": "Int",
              "Arguments": []
            },
            {
              "Name": "name",
              "Type": "Text",
              "Arguments": []
            }
          ],
          "ObjectFields": [
            "genus",
            "species",
            "breed",
            "pet",
            "person"
          ],
          "DatabaseTable": "family",
          "DatabaseRelationships": [
            ["family","id","person","id","genus","family_id","id","species","genus_id","id","breed","species_id","id","pet_type","breed_id","pet_id","pet_ownership","animal_id","owner_id"],
            ["family","id","pet","id","genus","family_id","id","species","genus_id","id","breed","species_id","id","pet_type","breed_id","pet_id"],
            ["family","id","genus","family_id"],
            ["family","id","species","genus_id","genus","family_id","id"],
            ["family","id","breed","species_id","genus","family_id","id","species","genus_id","id"]
          ]
        },
        {
          "ServerName": "Genus",
          "Pseudonyms": [
            "Genus",
            "genus"
          ],
          "ScalarFields": [
            {
              "Name": "id",
              "Type": "Int",
              "Arguments": []
            },
            {
              "Name": "name",
              "Type": "Text",
              "Arguments": []
            }
          ],
          "ObjectFields": [
            "family",
            "species",
            "breed",
            "pet",
            "person"
          ],
          "DatabaseTable": "genus",
          "DatabaseRelationships": [
            ["genus","id","person","id","species","genus_id","id","breed","species_id","id","pet_type","breed_id","pet_id","pet_ownership","animal_id","owner_id"],
            ["genus","id","pet","id","species","genus_id","id","breed","species_id","id","pet_type","breed_id","pet_id"],
            ["genus","family_id","family","id"],
            ["genus","id","species","genus_id"],
            ["genus","id","breed","species_id","species","genus_id","id"]
          ]
        },
        {
          "ServerName": "Species",
          "Pseudonyms": [
            "Species",
            "species"
          ],
          "ScalarFields": [
            {
              "Name": "id",
              "Type": "Int",
              "Arguments": []
            },
            {
              "Name": "name",
              "Type": "Text",
              "Arguments": []
            }
          ],
          "ObjectFields": [
            "family",
            "genus",
            "breed",
            "pet",
            "person"
          ],
          "DatabaseTable": "species",
          "DatabaseRelationships": [
            ["species","id","person","id","breed","species_id","id","pet_type","breed_id","pet_id","pet_ownership","animal_id","owner_id"],
            ["species","id","pet","id","breed","species_id","id","pet_type","breed_id","pet_id"],
            ["species","id","breed","species_id"],
            ["species","genus_id","genus","id"],
            ["species","id","family","genus_id","genus","species_id","id"]
          ]
        },
        {
          "ServerName": "Breed",
          "Pseudonyms": [
            "Breed",
            "breed"
          ],
          "ScalarFields": [
            {
              "Name": "id",
              "Type": "Int",
              "Arguments": []
            },
            {
              "Name": "name",
              "Type": "Text",
              "Arguments": []
            }
          ],
          "ObjectFields": [
            "family",
            "genus",
            "species",
            "pet",
            "person"
          ],
          "DatabaseTable": "breed",
          "DatabaseRelationships": [
            ["breed","id","person","id","pet_type","breed_id","pet_id","pet_ownership","animal_id","owner_id"],
            ["breed","id","pet","id","pet_type","breed_id","pet_id"],
            ["breed","species_id","species","id"],
            ["breed","species_id","genus","id","species","id","genus_id"],
            ["breed","species_id","family","id","species","id","genus_id","genus","id","family_id"]
          ]
        },
        {
          "ServerName": "Pet",
          "Pseudonyms": [
            "pet",
            "Pet"
          ],
          "ScalarFields": [
            {
              "Name": "id",
              "Type": "Int",
              "Arguments": []
            },
            {
              "Name": "name",
              "Type": "Text",
              "Arguments": []
            },
            {
              "Name": "gender",
              "Type": "Int",
              "Arguments": [
                {
                  "Name": "as",
                  "Options": [
                    {
                      "Name": "MALEFEMALE",
                      "Type": "Text",
                      "Prefix": "CASE WHEN ",
                      "Suffix": "==1 THEN 'MALE' ELSE 'FEMALE' END"
                    }
                  ]
                }
              ]
            }
          ],
          "ObjectFields": [
            "owner",
            "breed",
            "species",
            "genus",
            "family",
            "taxonomy"
          ],
          "DatabaseTable": "pet",
          "DatabaseRelationships": [
            ["pet","id","person","id","pet_ownership","animal_id","owner_id"],
            ["pet","id","breed","id","pet_type","pet_id","breed_id"],
            ["pet","id","species","id","pet_type","pet_id","breed_id","breed","id","species_id"],
            ["pet","id","genus","id","pet_type","pet_id","breed_id","breed","id","species_id","species","id","genus_id"],
            ["pet","id","family","id","pet_type","pet_id","breed_id","breed","id","species_id","species","id","genus_id","genus","id","family_id"]
          ]
        }
      ]
    }
   @

   __Schema recommendation:__

   If you add every possible chain from every PrimitiveObject to every other PrimitiveObject, you can extend the schema coverage and query capacities.

   __Closing remark:__

   As the last remark, I'll turn attention to a quote from specifications.

   @"In contrast, GraphQL only returns the data that's explicitly requested, so new capabilities can be added via new types and new fields on those types without creating a breaking change. This has lead to a common practice of always avoiding breaking changes and serving a versionless API."@ - <https://graphql.github.io/learn/best-practices/>
  
   With respect to that, you should be sure to be explicit with your schema representation. You should give all planned details, and you should make adjustments when you feel to evolve your server to make graph-like associations.   
-}
processSchema :: (MonadIO m)
              => FilePath       -- ^ This is the path to schema json file
              -> m SchemaSpecs  -- ^ The return value is a monad for your schema data.
processSchema fp = do
          schema <- liftIO $ fetchArguments fp
          return schema
{- |
  This function is to parse, validate, and interpret your query with variables. If you don't have variables, you may pass an empty string. The function is expecting your schema as SchemaSpec type from the above function. You can alternatively define you schema with lists and tuples. The order is

  @
  ([(ServerObjectName,[ServerObjectPseudonyms])],[(ServerObjectName,[(ScalarName,ScalarType)])],[(ServerObjectName,[NestedObjectNames])],[(ServerObjectName,ServerObjectDatabaseTableName)],[(StartingDatabaseTableName,EndingDatabaseTableName,[PathBetweenDatabaseTables])],[(ParentServerObjectName,[ParentPseudonyms],[ServerObjectDescendants])])
  @

  You also can use the above function to print your schema data and paste it into your script file.

  __Function return value:__

  The returned values are one tuple to contain information to pass into your database interface and to later reuse in below data processing function. 
  The tuple is (PackagedObjects,SQLQueries). The second tuple member is a collection of queries. 
  An example is given on this <https://github.com/jasonsychau/graphql-w-persistent page> to iterate the queries to your database.
  The example is also showing how is the first member passed to the data processing function.

  __Function exceptions:__

  Exceptions are returned when error is faced. This method returns errors of:

  * SyntaxException (when there is a problem with the given GraphQL query syntax)
  * ParseFragmentException (when there is a problem with a Fragment syntax)
  * EmptyQueryException (when the query is left blank)
  * InvalidObjectException (when there is problem in nested object syntax, an object is not recognized, or server data is misinterpreted)
  * InvalidObjectSubFieldException (when a subfield is requested from nested object that is not having ownership of the subfield)
  * InvalidScalarException (when there is syntax error in listing scalar fields, or server data is misinterpreted)
  * NullArgumentException (when a transformation is set but no argument is given though we do not yet support transformations) 
  * CreatingSqlQueryObjectFieldsException (when there are too many nested object subfields than nested objects to hold them or when there is a problem with the aligning of server object relationship argument - that's the fifth schema argument)
  * RelationshipConfigurationException (when the relationship schema fifth arugment is incorrect number of String values, when an unrecognized pairing is found, or when two table field cardinalities are not same)
  * FailedObjectEqualityException (when we could not match identical queries)
  * DuplicateRootObjectsException (when there is an overlapping query)

  * MissingVariableValueException (when we are missing variables in the variables-value string to the query)
  * InvalidVariableNameException (when we cannot find a variable in the query with given variables-value string)
  * MismatchedVariableTypeException (when query variable type is not matching object scalar type)
  * InvalidVariableTypeException (when variable type is invalid or missing)
  * ReadVariablesException (when the variables json string syntax is incorrect or was not correctly read)
  * VariablesSyntaxException (when the query variables declaration is invalid syntax)

  __Schema format example:__

  You should refer to the above function details for information on schema format and details.

  __Closing remark:__

  As the last remark, I'll turn attention to a quote from specifications.

  @"In contrast, GraphQL only returns the data that's explicitly requested, so new capabilities can be added via new types and new fields on those types without creating a breaking change. This has lead to a common practice of always avoiding breaking changes and serving a versionless API."@ - <https://graphql.github.io/learn/best-practices/>

  With respect to that, you should be sure to be explicit with your schema representation. You should give all planned details, and you should make adjustments when you feel to evolve your server to make graph-like associations.   
-}
processQueryString :: SchemaSpecs             -- ^ This is the data about your schema
                   -> String                  -- ^ This is the GraphQL query
                   -> String                  -- ^ This is the variables string
                   -> (QueryData,[[String]])  -- ^ The return value is a tuple with package objects and list with grouped sql query strings.
processQueryString (svrobjs,sss,sos,sodn,sor,soa) qry vars =
          let dvars = parseVariables vars qry
              str = processString qry
              objs = if (validateQuery str)==True then (parseStringToObjects str svrobjs soa dvars) else throw SyntaxException
              robjs = mergeDuplicatedRootObjects $ replaceObjectsVariables sss soa objs dvars
              (tbls,qrys) = if (checkObjectsAttributes robjs sss sos soa)==True then (makeSqlQueries robjs sss sodn sor soa) else (throw InvalidObjectSubfieldException)
          in (zip robjs tbls,qrys)
{- |
  After casting all database results to Text (example is given in <https://github.com/jasonsychau/graphql-w-persistent examples> page), you may use this function to process data to GraphQL format.

  The second argument is the package objects given from function processQueryString.
  
  The return result is a string to resemble the GraphQL return value.

  __Formatting notes (IMPORTANT):__

  Nulls are expected as "Unexpected null". This is the Persistent default and important when processing ids to separate different results. One can translate nulls to "Unexpected null" when making Text types of query results.

  __Function exceptions:__

  Exceptions are returned when error is faced. This method returns errors of:

  * InvalidObjectException (when server data is misinterpreted or an unrecognized server object is met)
  * InvalidScalarException (when server data is misinterpreted)
  * EOFDataProcessingException (when the given data is shorter than expected in reference to the given serve objects)
  * InvalidArgumentException (when there is an internal argument error - you should not observe this)
  * InvalidVariableTypeException (when an unrecognized base data type is met)
  * InvalidObjectScalarFieldException (when an unrecognized object-scalar pair is met)
-}
processQueryData :: SchemaSpecs   -- ^ This is the schema data from the first method
                 -> QueryData     -- ^ This is the package objects that is from processQueryString function.
                 -> [[[[Text]]]]  -- ^ This is the database query results value in the same order and after casting to Text values.
                 -> String        -- ^ The return value is a string type to describe the GraphQL-organized return values.
processQueryData (_,sss,_,sodn,_,soa) cc dt =
          let (ro,tb) = unzip cc
          in processReturnedValues sss sodn soa ro tb dt