import _ from 'lodash'
// https://github.com/nickyout/fast-stable-stringify/issues/8#issuecomment-329455969
// import stringify from 'fast-stable-stringify'

import { TMP_ID_PREFIX } from '../constant'
import { load } from '../collection/load'
import { dispatchMutations } from '../store'
import { prepareFindData } from './findInMemory'

export const isPreloadSkip = (self, option) => !option.serverPreload && self.store && self.store.vanCtx.duringServerPreload

export function defaultGetQueryString(query, option, coll) {
  if (Array.isArray(query)) {
    query = { [coll.idField]: { $in: query } }
  }
  const opt = { ..._.omitBy(option, (v, k) => k[0] === '_'), query }
  const sortedKeys = _.keys(opt).sort()
  return _.map(sortedKeys, k => `${k}=${JSON.stringify(opt[k])}`).join('&')
}

// @auto-fold here
function markPromise(self, key, promise, overwrite) {
  const { _fetchingPromises } = self
  if (!overwrite) {
    const oldPromise = _fetchingPromises[key]
    if (oldPromise) return oldPromise
  }

  promise
    .then(ret => {
      if (_fetchingPromises[key] === promise) {
        delete _fetchingPromises[key]
        self.addMutation(null) // force render to update isFetching
      }
      return ret
    })
    .catch(err => {
      if (_fetchingPromises[key] === promise) {
        delete _fetchingPromises[key]
        self.addMutation(null) // force render to update isFetching
      }
      return Promise.reject(err)
    })
  _fetchingPromises[key] = promise
  return promise
}

const isTmpId = (id, tmpIdPrefix = TMP_ID_PREFIX) => !id || _.startsWith(id, tmpIdPrefix)
const sortUniqFilter = (ids, tmpIdPrefix) => _.filter(_.sortedUniq(ids.sort()), id => !isTmpId(id, tmpIdPrefix))
// @auto-fold here
function prepareFetchQuery(query, idField, tmpIdPrefix = TMP_ID_PREFIX) {
  if (Array.isArray(query)) {
    const ids = sortUniqFilter(query, tmpIdPrefix)
    if (ids.length === 0) {
      return false
    }
    return ids
  }

  const fetchQuery = { ...query }
  const entries = Object.entries(query)
  for (let i = 0, ii = entries.length; i < ii; i++) {
    const [key, matcher] = entries[i]
    if (matcher) {
      if (typeof matcher === 'string' && isTmpId(matcher, tmpIdPrefix)) {
        return false
      } else if (matcher.$in) {
        const $in = sortUniqFilter(matcher.$in, tmpIdPrefix)
        if ($in.length === 0) {
          return false
        }
        fetchQuery[key] = { $in }
      }
    } else if (key === idField) {
      // idField is falsy
      return false
    }
  }
  return fetchQuery
}

export function findRemote(coll, query, option) {
  const notForce = !option.force

  prepareFindData(coll, query, option)
  if (notForce && option._allIdsHit) return false

  const fetchQuery = prepareFetchQuery(query, coll.idField)
  if (notForce && fetchQuery === false) return false

  const queryString = (coll.getQueryString || defaultGetQueryString)(fetchQuery, option, coll)
  if (notForce && queryString === false) return false
  option.queryString = queryString

  if (notForce) {
    const { fetchAts } = coll.getState()
    const now = Date.now()
    // collection.fetchMaxAge: 1, // in seconds; null, 0 or -1 means no maxAge
    const { fetchMaxAge } = coll
    if (fetchMaxAge > 0 ? fetchAts[queryString] > now - fetchMaxAge : fetchAts[queryString]) {
      return false
    }
    fetchAts[queryString] = now
  }

  // want to return fetching promise for findAsync
  const { onFetch } = coll
  const p = Promise.resolve(onFetch(fetchQuery, option, coll)).then(res => {
    load(coll, res)
    // flush dispatch mutates after load()
    dispatchMutations(coll.store)
    return res
  })
  return markPromise(coll, queryString, p)
}

@@ -826,16 +826,23 @@
 kPromise
+Finally
 (self, k
@@ -856,19 +856,8 @@
 mise
-, overwrite
 ) %7B%0A
@@ -903,176 +903,240 @@
 if (
-!overwrite) %7B%0A    const oldPromise = _fetchingPromises%5Bkey%5D%0A    if (oldPromise) return oldPromise%0A  %7D%0A%0A  promise%0A    .then(ret =%3E %7B%0A      if (_fetchingPromises%5Bkey%5D ===
+_fetchingPromises%5Bkey%5D === promise) %7B%0A    delete _fetchingPromises%5Bkey%5D%0A    if (Object.keys(_fetchingPromises).length === 0) %7B%0A      self.addMutation(%7B $merge: %7B fetchingAt: undefined %7D %7D)%0A    %7D%0A  %7D%0A%7D%0Afunction markPromise(self, key,
  pro
@@ -1141,36 +1141,31 @@
 romise) %7B%0A  
-      delete
+const %7B
  _fetchingPr
@@ -1174,170 +1174,193 @@
 ises
-%5Bkey%5D%0A        self.addMutation(null) // force render to update isFetching%0A      %7D%0A      return ret%0A    %7D)%0A    .catch(err =%3E %7B%0A      if (_fetchingPromises%5Bkey%5D ===
+ %7D = self%0A  // if (!overwrite) %7B%0A  const oldPromise = _fetchingPromises%5Bkey%5D%0A  if (oldPromise) return oldPromise%0A  // %7D%0A%0A  promise%0A    .then(ret =%3E %7B%0A      markPromiseFinally(self, key,
  pro
@@ -1368,124 +1368,97 @@
 ise)
- %7B%0A 
+%0A
       
- delete _fetchingPromises%5Bkey%5D
+return ret
 %0A    
+%7D)%0A
     
-self.addMutation(null) // force render to update isFetching%0A      %7D
+.catch(err =%3E %7B%0A      markPromiseFinally(self, key, promise)
 %0A   
@@ -1529,16 +1529,75 @@
 promise%0A
+  self.addMutation(%7B $merge: %7B fetchingAt: Date.now() %7D %7D)%0A
   return

add fetchingAt state