import { setTimeout } from 'timers/promises'
import { Low, JSONFile, Memory } from 'lowdb'
import fs from 'fs'
import { defaults, isObject } from './defaults.mjs'

export default function GetDB(config, log, orgFilename = 'db.json') {
  let adapter = new Memory()
  let fullpath = 'in-memory'
  if (orgFilename) {
    fullpath = orgFilename
    adapter = new JSONFile(fullpath)
  }
  const db = new Low(adapter)

  db.id = 'id'
  db.filename = fullpath
  db.config = config

  db.createId = function(collection) {
    if (collection.length) {
      return (collection[collection.length - 1].id || 0) + 1
    }
    return 1
  }

  db.get = function(collection, id, returnIndex = false) {
    let col = db.getCollection(collection)
    for (let i = col.length - 1; i >= 0; i--) {
      if (col[i][db.id] === id) {
        if (returnIndex) return i
        return col[i]
      }
    }
    return null
  }

  db.getCollection = function(collection) {
    if (typeof(collection) === 'string') {
      return db.data[collection]
    }
    return collection
  }

  db.upsert = function(collection, item) {
    let col = db.getCollection(collection)
    if (item[db.id]) {
      let i = db.get(col, item[db.id], true)
      if (i !== null) {
        col[i] = item
        return
      }
    } else {
      item[db.id] = db.createId(col)
    }
    col.push(item)
  }

  db.upsertFirst = function(collection, item) {
    let col = db.getCollection(collection)
    if (item[db.id]) {
      let i = db.get(col, item[db.id], true)
      if (i !== null) {
        col[i] = item
        return
      }
    } else {
      item[db.id] = db.createId(col)
    }
    col.splice(0, 0, item)
  }

  db.remove = function(collection, itemOrId) {
    let col = db.getCollection(collection)
    let id = itemOrId
    if (typeof(id) === 'object') {
      id = id[db.id]
    }
    for (let i = col.length - 1; i >= 0; i--) {
      if (col[i][db.id] === id) {
        col.splice(i, 1)
        return true
      }
    }
    return false
  }
  
  db.addApplication = function(name) {
    db.data.core[name] ||= {}
    defaults(db.data.core[name], {
      active: '',
      latestInstalled: '',
      updater: '',
      versions: [],
    })
  }

  db.log = log
  db._write = db.write.bind(db)
  db.write = function() {
    return this._write()
      // Do couple of retries. Sometimes it fails randomly doing atomic writes.
      .catch(() => { return setTimeout(20).then(() => { return this._write() }) })
      .catch(() => { return setTimeout(50).then(() => { return this._write() }) })
      .catch(() => { return setTimeout(100).then(() => { return this._write() }) })
      .catch((err) => {
        try {
          db.writeSync()
        } catch {
          this.log.error(err, 'Error saving to db')
        }
      })
  }

  db.writeSync = function() {
    try {
      fs.writeFileSync(db.filename, JSON.stringify(db.data))
    } catch(err) {
      db.log.error(err, `Error during writeSync to ${db.filename}`)
    }
  }

  return db.read()
    .then(function() {
      if (!isObject(db.data)) {
        if (fullpath !== 'in-memory') {
          db.log.warn(`File ${fullpath} was empty or not a json object, clearing it.`)
        }
        db.data = {}
      }
      defaults(db.data, { core: { version: 1 } })
    
      return db.write()
    })
    .then(
      function() { return db },
      function(err) {
        let wrapped = new Error(`Error writing to ${fullpath}: ${err.message} (${err.code})`)
        wrapped.code = err.code
        throw wrapped
      }
    )
}
