import { defaults, debounce, getPath, setPath, pushPath } from './utils.js'
import { readFile, readFileSync } from './readFile.js'
import { writeFile, removeFile } from './writeFile.js'

const getDefaults = () => {
  return {
    loadPath: '/locales/{{lng}}/{{ns}}.json',
    addPath: '/locales/{{lng}}/{{ns}}.missing.json',
    ident: 2,
    parse: JSON.parse,
    stringify: JSON.stringify
    // expirationTime: 60 * 60 * 1000
  }
}

class Backend {
  constructor (services, options = {}, allOptions = {}) {
    this.services = services
    this.options = options
    this.allOptions = allOptions
    this.type = 'backend'
    this.init(services, options, allOptions)
  }

  init (services, options = {}, allOptions = {}) {
    this.services = services
    this.options = defaults(options, this.options || {}, getDefaults())
    this.allOptions = allOptions
    this.queuedWrites = {}
    this.debouncedWrite = debounce(this.write, 250)
  }

  read (language, namespace, callback) {
    let loadPath = this.options.loadPath
    if (typeof this.options.loadPath === 'function') {
      loadPath = this.options.loadPath(language, namespace)
    }
    const filename = this.services.interpolator.interpolate(loadPath, { lng: language, ns: namespace })
    if (this.allOptions.initAsync === false || this.allOptions.initImmediate === false) {
      try {
        const { data, stat } = readFileSync(filename, this.options)
        const timestamp = stat && stat.mtime && stat.mtime.getTime()
        if (this.options.expirationTime && timestamp && timestamp + this.options.expirationTime < Date.now()) {
          this.removeFile(language, namespace)
          return callback(new Error('File expired!'), false) // no retry
        }
        callback(null, data, timestamp)
      } catch (err) {
        callback(err, false) // no retry
      }
      return
    }
    readFile(filename, this.options)
      .then(({ data, stat }) => {
        const timestamp = stat && stat.mtime && stat.mtime.getTime()
        if (this.options.expirationTime && timestamp && timestamp + this.options.expirationTime < Date.now()) {
          this.removeFile(language, namespace)
          return callback(new Error('File expired!'), false) // no retry
        }
        callback(null, data, timestamp)
      })
      .catch((err) => callback(err, false)) // no retry
  }

  create (languages, namespace, key, fallbackValue, callback) {
    if (typeof callback !== 'function') callback = () => {}
    if (typeof languages === 'string') languages = [languages]

    let todo = languages.length
    const done = () => {
      if (!--todo) callback()
    }

    languages.forEach((lng) => {
      // eslint-disable-next-line no-useless-call
      this.queue.call(this, lng, namespace, key, fallbackValue, done)
    })
  }

  // this way i18next-fs-backend can be used as cache layer in combination with i18next-chained-backend
  save (language, namespace, data, callback) {
    if (!callback) callback = () => {}

    const keys = Object.keys(data)
    let todo = keys.length
    const done = () => {
      if (!--todo) callback()
    }

    keys.forEach((key) => {
      // eslint-disable-next-line no-useless-call
      this.queue.call(this, language, namespace, key, data[key], done)
    })
  }

  removeFile (language, namespace) {
    let addPath = this.options.addPath
    if (typeof this.options.addPath === 'function') {
      addPath = this.options.addPath(language, namespace)
    }
    const filename = this.services.interpolator.interpolate(addPath, { lng: language, ns: namespace })
    removeFile(filename, this.options)
      .then(() => {})
      .catch(() => {})
  }

  write () {
    for (const lng in this.queuedWrites) {
      const namespaces = this.queuedWrites[lng]
      if (lng !== 'locks') {
        for (const ns in namespaces) {
          this.writeFile(lng, ns)
        }
      }
    }
  }

  writeFile (lng, namespace) {
    const lock = getPath(this.queuedWrites, ['locks', lng, namespace])
    if (lock) return

    let addPath = this.options.addPath
    if (typeof this.options.addPath === 'function') {
      addPath = this.options.addPath(lng, namespace)
    }

    const filename = this.services.interpolator.interpolate(addPath, { lng, ns: namespace })

    const missings = getPath(this.queuedWrites, [lng, namespace])
    setPath(this.queuedWrites, [lng, namespace], [])

    if (missings.length) {
      // lock
      setPath(this.queuedWrites, ['locks', lng, namespace], true)

      const proceed = ({ data }) => {
        missings.forEach((missing) => {
          const path = this.allOptions.keySeparator === false ? [missing.key] : (missing.key.split(this.allOptions.keySeparator || '.'))
          try {
            setPath(data, path, missing.fallbackValue)
          } catch (e) {
            if (path.length < 2 || !e.message || (e.message.indexOf('Cannot create property') < 0)) throw e
            setPath(data, [missing.key], missing.fallbackValue)
          }
        })

        const proceedWrite = () => {
          // unlock
          setPath(this.queuedWrites, ['locks', lng, namespace], false)
          missings.forEach((missing) => {
            if (missing.callback) missing.callback()
          })
          // rerun
          this.debouncedWrite()
        }
        writeFile(filename, data, this.options)
          .then(proceedWrite)
          .catch(proceedWrite)
      }
      readFile(filename, this.options).then(proceed).catch(() => proceed({ data: {} }))
    }
  }

  queue (lng, namespace, key, fallbackValue, callback) {
    pushPath(this.queuedWrites, [lng, namespace], { key, fallbackValue: fallbackValue || '', callback })
    this.debouncedWrite()
  }
}

Backend.type = 'backend'

export default Backend
