import gql from 'graphql-tag'
import { useRouter, useRoute } from 'vue-router'

import { useI18n } from 'vue-i18n'

import { useMutation, useQuery, useResult } from '@vue/apollo-composable'
import {getCurrentInstance , computed, ref, inject, reactive} from 'vue'
import { useForm } from 'vee-validate'

import { getCsrfToken } from '@/components/hooks/auth'

import useFeedback from '@/components/hooks/feedback'

const apiUrl = window.location.protocol + "//" + window.location.hostname + ':' + process.env.VUE_APP_API_PORT
const createDebounce = function() {
  let timeout = null
  return function (fnc, delayMs) {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      fnc()
    }, delayMs || 200)
  }
}

const toCamel = function (s) {
  const camel = s.replace(/([-_][a-z])/ig, ($1) => {
    return $1.toUpperCase()
      .replace('-', '')
      .replace('_', '')
  })

  return camel.charAt(0).toUpperCase() + camel.slice(1)
}

const getFieldsInput = function (definition, data, extraDefinitions) {
  const input = {}

  for (const fieldName in data) {
    if (fieldName !== 'id') {
      if (definition.fields[fieldName] !== undefined && (!definition.fields[fieldName].primary_key || definition.fields[fieldName].has_one) && definition.fields[fieldName].editable) {
        if (data[fieldName] !== null || !definition.fields[fieldName].generated_default) {
          if (definition.fields[fieldName].has_one !== undefined) {

            if(fieldName == 'field_data_row'){
              data[fieldName].field_data_value_set.forEach(function(field){
                if(input['customFields'] == undefined){
                  input['customFields'] = []
                }

                input['customFields'].push({field: field.field.id, value: field.value})
              })
            }

            if (extraDefinitions && extraDefinitions[fieldName] !== undefined) {
              input[fieldName] = getFieldsInput(extraDefinitions[fieldName], data[fieldName], extraDefinitions[fieldName].extraDefinitions)
            } else {
              input[fieldName] = data[fieldName] !== null && data[fieldName].id ? data[fieldName].id : null
            }
          } else if(definition.fields[fieldName].type === 'FileField') {
            input[fieldName] = data[fieldName]
          } else if(['PhoneField', 'FaxField'].includes(definition.fields[fieldName].type)) {
            if(data[fieldName] !== null){
              const match = data[fieldName].match( /\+?\d+/g )
              input[fieldName] = match ? match.join('') : null
            }
          } else {
            input[fieldName] = data[fieldName]
          }
        }
      } else if (definition.fields[fieldName] !== undefined && definition.fields[fieldName].belongs_to) {
        input[fieldName] = data[fieldName]
      }
    }
  }
  return input
}

const hasFileInput = function(data) {
  let found = false
  for (const fieldName in data) {
    if(typeof(data[fieldName]) === File){
      found = true
    }else if(typeof(data[fieldName]) === Object){
      found = hasFileInput(data[fieldName])
    }
    if(found){
      break
    }
  }

  return found
}

const api = {
  cleanInput: function(definition, data, extraDefinitions) {
    return {input: getFieldsInput(definition, data, extraDefinitions)}
  },
  hasFileInput,
  create: function (definition) {
    const query = definition.model

    return gql`mutation ${query}_create($input: Create${toCamel(query)}Input!) {
                    ${query}_create(input: $input) {
                      ${query}{
                          id
                          id_disambiguated
                          node_description
                      }
                    }
                  }`
  },
  update: function (definition) {
    const query = definition.model

    return gql`mutation ${query}_update($id: ID!, $input: Patch${toCamel(query)}Input!) {
                    ${query}_update(id: $id, input: $input) {
                      ${query}{
                          id
                          id_disambiguated
                          node_description
                      }
                    }
                  }`
  },
  delete: function (definition) {
    const query = definition.model
    return gql`mutation ${query}_delete($id: ID!)
          {
            ${query}_delete (id: $id)
            {
              found
              deleted_id
            }
          }`
  },
  get: function (definition, extraDefinitions) {
    const query = definition.model

    const getFieldsQuery = function (definition, extraDefinitions) {
      let fieldsQuery = ''
      for (const fieldName in definition.fields) {
        if (definition.fields[fieldName].has_one !== undefined) {
          let extraFields = ''
          if (extraDefinitions && extraDefinitions[fieldName] !== undefined) {
            extraFields = getFieldsQuery(extraDefinitions[fieldName])
          }

          if(fieldName == 'field_data_row'){
            extraFields += `field_data_value_set{
                  value
                  field{
                    id
                    identifier
                  }
              }`
          }

          fieldsQuery += `${fieldName}{
                id
                id_disambiguated
                node_description
                ${extraFields}
              }`
        } else if (definition.fields[fieldName].belongs_to === undefined) {
          fieldsQuery += fieldName + '\n\r'
        }
      }

      return fieldsQuery
    }

    const fieldsQuery = getFieldsQuery(definition, extraDefinitions)

    return gql`query get_${query}($id: ID!) {
                    ${query}(id: $id) {
                          id
                          id_disambiguated
                          node_description
                          ${fieldsQuery}
                      }
                    } `
  },
  list: function (definition, columns) {
    if(!definition || !('model' in definition)) {
      throw 'Invalid definition in api.list'
    }

    const query = definition.model + '_set'
    const fields = {}

    columns.forEach((fieldName) => {
      if (fieldName === 'node_description') {
        fields[fieldName] = {}
      } else if (fieldName === 'timestamps') {
        fields.create_date = {}
        fields.create_username = {}
        fields.mod_date = {}
        fields.mod_username = {}
      } else if (fieldName.includes('__')) {
        let childFields = fields
        fieldName.split('__').forEach((childFieldName) => {
          if (childFields[childFieldName] === undefined) {
            childFields[childFieldName] = {}
          }
          childFields = childFields[childFieldName]
        })
      } else if (definition.fields[fieldName] && definition.fields[fieldName].has_one === undefined && definition.fields[fieldName].belongs_to === undefined) {
        fields[fieldName] = {}
      } else if (definition.fields[fieldName]) {
        fields[fieldName] = { id: {}, id_disambiguated: {}, node_description: {} }
      } else {
        fields[fieldName] = {}
        //throw 'Unknown field '+fieldName+' in model '+definition.model
      }
    })

    const getFieldsQuery = function (fields) {
      let fieldsQuery = ''

      for (const fieldName in fields) {
        if (Object.keys(fields[fieldName]).length === 0) {
          fieldsQuery += fieldName + '\n\r'
        } else {
          fieldsQuery += fieldName + '{\n\r' + getFieldsQuery(fields[fieldName]) + '}\r\n'
        }
      }

      return fieldsQuery
    }
    const fieldsQuery = getFieldsQuery(fields)

    return gql`query get_${query}($order: [String]!, $filters: [SearchObjectType]!, $limit: Int, $after: String) {
                    ${query}(order: $order, filters: $filters, first: $limit, after: $after) {
                      total_count
                      pageInfo {
                        endCursor
                        hasNextPage
                      }
                      edges {
                        node {
                          id
                          id_disambiguated
                          node_description
                          ${fieldsQuery}
                        }
                      }
                    }
                  }
                `
  },
  count: function (definition) {
    if(!definition || !('model' in definition)) {
      throw 'Invalid definition in api.list'
    }

    const query = definition.model + '_set'

    return gql`query count_${query}($filters: [SearchObjectType]!) {
                    ${query}(filters: $filters, first: 0) {
                      total_count
                    }
                  }
                `
  },
  export: async function(query, variables, filename=null, total_count=null) {
    //total_count is only used to estimate the progress bar
    fetch(apiUrl+'/graphql/download',{
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': await getCsrfToken()
      },
      credentials: 'include',
      body: JSON.stringify({
        query: query,
        variables: variables,
        filename: filename,
        total_count: total_count
      })
    })
  }
}

const defaultCreateRow = function (definition, extraDefinitions) {
  const input = {}
  for (const fieldName in definition.fields) {
    if (definition.fields[fieldName] !== undefined && !definition.fields[fieldName].primary_key && definition.fields[fieldName].editable) {

      if(fieldName == 'field_data_row'){
        input[fieldName] = {id: null, field_data_value_set : []}
      } else if (definition.fields[fieldName].has_one !== undefined) {
        if (extraDefinitions && extraDefinitions[fieldName] !== undefined) {
          input[fieldName] = {}
        } else {
          input[fieldName] = { id: definition.fields[fieldName].default }
        }
      } else {
        input[fieldName] = definition.fields[fieldName].default ? definition.fields[fieldName].default : definition.fields[fieldName].type == "BooleanField" ? false : null
      }
    }
  }
  return input
}

/*const resetRow = function(row, emptyRow){
  for (const key in row){
    if(key in emptyRow){
      row[key] = emptyRow[key]
    } else{
      delete row[key]
    }
  }
}*/

const useSmartForm = function(apollo, feedback = {}) {

  const ctx = getCurrentInstance().ctx
  const router = useRouter()
  const route = useRoute()
  const { successHandler} = useFeedback()

  const { t } = useI18n()


  const { setFieldValue, handleSubmit, resetForm, errors, meta, values} = useForm()

  const saveRow = handleSubmit(function(){
    const input = api.cleanInput(apollo.definition, apollo.row, apollo.extraDefinitions)

    if(apollo.definition.fields['field_data_row'] !== undefined){
      delete input.input.field_data_row
    }

    if (apollo.isNew) {
      apollo.actions.createRow(input, {context: {hasUpload: hasFileInput(input)}})
    } else {
      apollo.actions.updateRow({id: apollo.id, ...input}, {context: {hasUpload: hasFileInput(input)}})
    }
  })

  const createFeedback = feedback.createFeedback ? feedback.createFeedback : (data) => {

    successHandler(t('ui.success_create'), ctx)

    try{
      const result = data.data[apollo.model + '_create'][apollo.model]
      const params = {}

      params[apollo.model + '_id'] = result.id
      params[apollo.model + '_description'] = result.node_description

      const viewRoute = { name: route.name.replace(/.add$/, '.view'), params }

      router.replace(viewRoute)
    }catch(err){
      window.dispatchEvent( new Event('go-back-in-breadcrumb'))
    }
  }

  const updateFeedback = feedback.updateFeedback ? feedback.updateFeedback : () =>{
    successHandler(t('ui.success_update'), ctx)
    window.dispatchEvent( new Event('go-back-in-breadcrumb'))
  }

  apollo.events.onCreateDone(createFeedback)

  apollo.events.onUpdateDone(updateFeedback)



  const goBack = feedback.goBack ? feedback.goBack : () => window.dispatchEvent( new Event('go-back-in-breadcrumb'))

  const title = t(apollo.isNew ? 'ui.add_model' : 'ui.edit_model', {model: t(['models', apollo.definition.model, 'name'].join('.'))})

  return {setFieldValue, saveRow, errors, meta, resetForm, successHandler, handleSubmit, createFeedback, updateFeedback, values, title, goBack}
}

const useApolloRow = function(model, extras, belongsTo, refreshQueries = []){

  const definitions = inject('definitions')

  const id = ref(getRouteId(model))
  const belongsToId = belongsTo ? getRouteId(belongsTo) : undefined
  const definition = definitions[model]
  const isNew = computed(function(){
    return id.value === undefined || id.value === null
  })

  const extraDefinitions = {}

  if(extras) {
    extras.forEach((item) => {
      if(item.includes('__')){
        const itemParts = item.split('__')

        if(itemParts.length > 2){
          throw 'Infinite extraDefinitions is not supported' //@todo
        }

        extraDefinitions[itemParts[0]].extraDefinitions = {}
        extraDefinitions[itemParts[0]].extraDefinitions[itemParts[1]] = definitions[itemParts[1]] //@todo infinite nest
      } else{
        extraDefinitions[item] = definitions[item]
      }
    })
  }

  const { result, loading, refetch } = useQuery(
    api.get(definitions[model], extraDefinitions), () => ({ id: id.value }), () => ({ enabled: !isNew.value, fetchPolicy: 'no-cache'})
  )

  const emptyRow = {...defaultCreateRow(definitions[model], extraDefinitions)}
  if(belongsTo){
    emptyRow[belongsTo] = {id: belongsToId}
  }

  const row = useResult(
    result,
    reactive(emptyRow),
    data => data[model]
  )

  const refresh = []
  refresh.push('get_'+model+'_set', 'get_'+model)

  if(Array.isArray(extras)){
    extras.forEach(function(item){
      refresh.push('get_'+item+'_set', 'get_'+item)
    })
  }

  refreshQueries.forEach(function(item){
    refresh.push(item)
  })

  const { mutate: createRow, onError: onCreateError, onDone: onCreateDone, loading: loadingCreate } = useMutation(api.create(definition), { refetchQueries : refresh})
  const { mutate: updateRow, onError: onUpdateError, onDone: onUpdateDone, loading: loadingUpdate } = useMutation(api.update(definition), { refetchQueries : refresh})
  const { mutate: deleteRow, onError: onDeleteError, onDone: onDeleteDone, loading: loadingDelete } = useMutation(api.delete(definition), { refetchQueries : refresh})

  const loadingMutation = computed(function(){
    return (loadingCreate.value || loadingUpdate.value || loadingDelete.value)
  })

  return {
    model,
    id,
    belongsTo,
    belongsToId,
    definition,
    extraDefinitions,
    isNew,
    loading,
    loadingMutation,
    row,
    emptyRow,
    actions: {
      refetch,
      createRow,
      updateRow,
      deleteRow
    },
    events: {
      onCreateDone,
      onDeleteDone,
      onUpdateDone,
      onCreateError,
      onDeleteError,
      onUpdateError
    }
  }
}

const getRouteId = function ( model) {
  const route = useRoute()
  return route && route.params && route.params[model + '_id'] ? route.params[model + '_id'] : null
}

const resolveDjangoPath = (object, path, defaultValue) => path .split('__') .reduce((o, p) => o ? o[p] : defaultValue, object)

const graphQlFilter = function(){
  const { t } = useI18n()
  return [
    {type: '__iexact', desc: t('ui.iexact')},
    {type: '__contains', desc: t('ui.contains')},
    {type: '__icontains', desc: t('ui.icontains')},
    {type: '__startswith', desc: t('ui.startswith')},
    {type: '__istartswith', desc: t('ui.istartswith')},
    {type: '__endswith', desc: t('ui.endswith')},
    {type: '__iendswith', desc: t('ui.iendswith')},
    {type: '__range', desc: t('ui.range')},
    {type: '__gt', desc: t('ui.greather_than')},
    {type: '__gte', desc: t('ui.greather_than_or_equal')},
    {type: '__lt', desc: t('ui.lower_than')},
    {type: '__lte', desc: t('ui.lower_than_or_equal')}
  ]
}

const formatPhone = function(value){

  value = (value ? value : '')

  let output = ''
  let length = value.length
  let international = false

  if(value.includes('+')){
    length--
    international = true
  }

  if(international){
    output = value
  }else{
    switch(length){
    case 7:
      output = value.substring(0, 3) + '-' + value.substring(3, 7)
      break
    case 10:
      output = '('+value.substr(0, 3)+') ' + value.substring(3, 6) + '-' + value.substring(6, 10)
      break
    case 11:
      output = value.substring(0, 1) + ' ('+value.substring(1, 4)+') ' + value.substring(4, 7) + '-' + value.substring(7, 11)
      break
    default:
      output = value
      break
    }
  }

  return output
}

const formatCurrency = function(value, decimals = 2){
  // @todo: handle locales (http://numeraljs.com/#locales) ie: numeral.locale('fr');
  const numeral = inject('numeral')
  return numeral(value).format('$0.'+'0'.repeat(decimals))
}

const formatNumber = function(value, decimals = 0){
  const numeral = inject('numeral')
  return numeral(value).format('0.'+'0'.repeat(decimals))
}

const formatPercentage = function(value, decimals = 0){
  const numeral = inject('numeral')
  return numeral(value/100).format('0.'+'0'.repeat(decimals)+'%')
}

export default
{
  api,
  apiUrl,
  useApolloRow,
  useSmartForm,
  defaultCreateRow,
  graphQlFilter,
  getRouteId,
  toCamel,
  resolveDjangoPath,
  formatPhone,
  formatCurrency,
  formatNumber,
  formatPercentage,
  createDebounce
}
