const { getCachedDocumentNodeFromSchema } = require('@graphql-codegen/plugin-helpers')
const { visit } = require('graphql')
const { camelCase } = require('lodash')

function capitalizeFirstLetter(string) {
  return string.charAt(0).toUpperCase() + string.slice(1)
}

// `inList` flips to true the moment we descend into a ListType, so a NonNullType
// nested under a List is recognized as element-non-null (`[T!]`) rather than
// overwriting the outer list's `required` flag. The element-non-null is encoded
// as `list: [true]` — the shape gql-query-builder reads to emit `[T!]`.
function inputTypeToObject(inputType, result, inList = false) {
  if (inputType.kind === 'NonNullType') {
    if (inList) result['list'] = [true]
    else result['required'] = true
    inputTypeToObject(inputType.type, result, inList)
    return
  }
  if (inputType.kind === 'ListType') {
    if (!Array.isArray(result['list'])) result['list'] = true
    inputTypeToObject(inputType.type, result, true)
    return
  }
  if (inputType.kind === 'NamedType') {
    result['type'] = inputType.name.value
  }
}

module.exports = {
  inputTypeToObject,
  plugin(schema, _documents, _config) {
    const astNode = getCachedDocumentNodeFromSchema(schema) // Transforms the GraphQLSchema into ASTNode

    const operationNode = []
    const typeNode = []
    const inputNode = []

    const scalarType = ['ID', 'String', 'Boolean', 'Int', 'Float']

    const result = visit(astNode, {
      ObjectTypeDefinition(node) {
        if (node.name.value === 'Query' || node.name.value === 'Mutation') {
          node.fields.forEach((field) => {
            let fieldType = {}
            let fieldVariables = []
            inputTypeToObject(field.type, fieldType)
            field.arguments.forEach((argument) => {
              let fieldVariablesItem = { name: argument.name.value }
              if (argument.defaultValue) fieldVariablesItem.value = argument.defaultValue.value
              if (argument.type) inputTypeToObject(argument.type, fieldVariablesItem)
              fieldVariables.push(fieldVariablesItem)
            })
            operationNode.push({
              name: field.name.value,
              variables: fieldVariables,
              type: fieldType.type,
              operationType: node.name.value,
            })
          })
        }
        else {
          let fields = []
          node.fields.forEach((field) => {
            let fieldVariablesItem = { name: field.name.value }
            if (field.type) inputTypeToObject(field.type, fieldVariablesItem)
            fields.push(fieldVariablesItem)
          })
          typeNode.push({
            name: node.name.value,
            fields: fields,
          })
        }
      },
      InputObjectTypeDefinition(node) {
        let fields = []
        node.fields.forEach((field) => {
          let fieldVariablesItem = { name: field.name.value }
          if (field.defaultValue) fieldVariablesItem.value = field.defaultValue.value
          if (field.type) inputTypeToObject(field.type, fieldVariablesItem)
          fields.push(fieldVariablesItem)
        })
        inputNode.push({
          name: node.name.value,
          variables: fields,
        })
      },
      ScalarTypeDefinition(node) {
        scalarType.push(node.name.value)
      },
    })

    typeNode.map((node) => {
      let scalars = []
      let relations = []
      node.fields.forEach((field) => {
        if (scalarType.includes(field.type)) scalars.push(field.name)
        else relations.push(field)
      })
      node.scalars = scalars
      node.relations = relations
    })

    return `
        import * as graphQlClass from "~/types/graphql"
        import { useGraphQlOperation } from "#imports";
        import type {
            graphqlInputObject,
            graphqlOperationObject,
            graphqlTypeObject,
            graphqlVariable
          } from "#build/types/graphqlOperation";
        
        export const scalarType = ${JSON.stringify(scalarType)}
        
        function operationCall${'<' + 'T,U>'}(operationType:string, operation: string,fields?: ${'Array' + '<string | Object>'},variables?: U,cache: boolean | number = false) {
            return useGraphQlOperation${'<' + 'T>'}(operationType,operation,fields,variables as {[p: string] : any} | undefined,cache)
        }
        
        function createGraphQLOperation${'<' + 'T,U>'}(operationType: "Query" | "Mutation",name: string,variables?: graphqlVariable[],fields?: graphqlTypeObject): graphqlOperationObject${'<' + 'T,U>'} {
            let returnGraphqlOperation : ${'Record' + '<string,any>'} = {
                operationType,
                name,
                variables,
                fields,
            }
            if (operationType === "Mutation") {
              returnGraphqlOperation['call'] = async function(fields?: ${'Array' + '<string | Object>'}, variables?: U): Promise${'<' + 'T>'} {
                  return operationCall(operationType,name, fields, variables);
              }
            } else {
              returnGraphqlOperation['call'] = async function(fields?: ${'Array' + '<string | Object>'}, variables?: U,cache: boolean | number = false): Promise${'<' + 'T>'} {
                  return operationCall(operationType,name, fields, variables,cache);
              }
            }
            return returnGraphqlOperation as graphqlOperationObject${'<' + 'T,U>'}
        }
        
        interface graphqlTypeObjects extends ${'Record<string,' + 'graphqlTypeObject>'} {
        ${typeNode.map(node => `${node.name}:  graphqlTypeObject`).join('\n')}
        }
        
        export const graphqlType: graphqlTypeObjects= {
        ${typeNode.map(node => `${node.name} : {
        fields : ${JSON.stringify(node.scalars)},
        relations: ${JSON.stringify(node.relations)}
        } as graphqlTypeObject `).join(',')}
        }
        
        interface graphqlInputObjects extends ${'Record<string,' + 'graphqlInputObject>'} {
        ${inputNode.map(node => `${node.name}:  graphqlInputObject`).join('\n')}
        }
        
        export const graphqlInputType: graphqlInputObjects = {
        ${inputNode.map(node => `${node.name} : {
        variables: ${JSON.stringify(node.variables)},
        }`).join(',')}
        }
        
        interface graphqlOperationObjects extends ${'Record<string,' + 'graphqlOperationObject' + '<' + 'any' + ',any>' + '>'} {
        ${operationNode.map(node => `${node.name}:  graphqlOperationObject${'<graphQlClass' + '.' + node.operationType + '[\'' + node.name + '\'],' + ((node.variables.length) ? 'graphQlClass.' + node.operationType + capitalizeFirstLetter(camelCase(node.name)) + 'Args' : 'undefined') + '>'}`).join('\n')}
        }
        
        export const graphqlOperation: graphqlOperationObjects = {
        ${operationNode.map(node => `${node.name}:  createGraphQLOperation${'<graphQlClass' + '.' + node.operationType + '[\'' + node.name + '\'],' + ((node.variables.length) ? 'graphQlClass.' + node.operationType + capitalizeFirstLetter(camelCase(node.name)) + 'Args' : 'undefined') + '>'}('${node.operationType}','${node.name}',${JSON.stringify(node.variables)},${typeNode.find(type => type.name === node.type) ? 'graphqlType.' + node.type : 'undefined'})`).join(',')}
        }
        `
  },
}
