import { OperationTypeNode } from 'graphql'
import { getFieldFromPath } from './getFieldFromPath'
import { LinkedType } from './linkTypeMap'
import { applyTypeMapperToVariable } from './applyTypeMapperToVariable'
import { TypeMapper } from './applyTypeMapperToResponse'

export interface Args {
  [arg: string]: any | undefined
}

export interface Fields {
  [field: string]: Request
}

export type Request = boolean | number | Fields | [Args, Fields?]

export interface Variables {
  [name: string]: {
    value: any
    typing: [string, LinkedType]
  }
}

export interface Context {
  root: LinkedType
  varCounter: number
  variables: Variables
  fragmentCounter: number
  fragments: string[]
}

export interface Gql {
  query: string
  variables: { [name: string]: any }
}

const parseRequest = (request: Request | undefined, ctx: Context, path: string[]): string => {
  if (Array.isArray(request)) {
    const [args, fields] = request
    const argNames = Object.keys(args)

    if (argNames.length === 0) {
      return parseRequest(fields, ctx, path)
    }

    const field = getFieldFromPath(ctx.root, path)

    return `(${argNames.map(argName => {
      ctx.varCounter++
      const varName = `v${ctx.varCounter}`

      const typing = field.args && field.args[argName]

      if (!typing) {
        throw new Error(`no typing defined for argument \`${argName}\` in path \`${path.join('.')}\``)
      }

      ctx.variables[varName] = {
        value: args[argName],
        typing,
      }

      return `${argName}:$${varName}`
    })})${parseRequest(fields, ctx, path)}`
  } else if (typeof request === 'object') {
    const fields = request
    const fieldNames = Object.keys(fields)

    if (fieldNames.length === 0) {
      throw new Error('field selection should not be empty')
    }

    const type = path.length > 0 ? getFieldFromPath(ctx.root, path).type : ctx.root
    const scalarFields = type.scalar

    let scalarFieldsFragment: string | undefined

    if (~fieldNames.indexOf('__scalar')) {
      if (!scalarFields) {
        throw new Error(`type ${type.name} has no scalar fields`)
      }

      ctx.fragmentCounter++
      scalarFieldsFragment = `f${ctx.fragmentCounter}`

      ctx.fragments.push(`fragment ${scalarFieldsFragment} on ${type.name}{${scalarFields.join(',')}}`)
    }

    return `{${fieldNames
      .filter(f => f !== '__scalar')
      .map(f => {
        const parsed = parseRequest(fields[f], ctx, [...path, f])

        if (f.startsWith('on_')) {
          ctx.fragmentCounter++
          const implementationFragment = `f${ctx.fragmentCounter}`

          const typeMatch = f.match(/^on_(.+)/)

          if (!typeMatch || !typeMatch[1]) throw new Error('match failed')

          ctx.fragments.push(`fragment ${implementationFragment} on ${typeMatch[1]}${parsed}`)

          return `...${implementationFragment}`
        } else {
          return `${f}${parsed}`
        }
      })
      .concat(scalarFieldsFragment ? [`...${scalarFieldsFragment}`] : [])
      .join(',')}}`
  } else {
    return ''
  }
}

export const requestToGql = (
  operation: OperationTypeNode,
  root: LinkedType,
  fields: Fields,
  typeMapper?: TypeMapper,
): Gql => {
  const ctx: Context = { root, varCounter: 0, variables: {}, fragmentCounter: 0, fragments: [] }
  const result = parseRequest(fields, ctx, [])

  const varNames = Object.keys(ctx.variables)

  const varsString = varNames.length > 0 ? `(${varNames.map(v => `$${v}:${ctx.variables[v].typing[0]}`)})` : ''

  return {
    query: [`${operation}${varsString}${result}`, ...ctx.fragments].join(','),
    variables: Object.keys(ctx.variables).reduce<{ [name: string]: any }>((r, v) => {
      r[v] = applyTypeMapperToVariable(ctx.variables[v].value, ctx.variables[v].typing[1], typeMapper)
      return r
    }, {}),
  }
}
