modules/app/files.js

const fs = require('fs-extra')
const path = require('path')
const dirTree = require('directory-tree')

const storeDir = path.join(process.env.HOME, '.traktify')

const defConfigDir = path.join(__dirname, '../../def_config.json')
const defConfigRaw = fs.readFileSync(defConfigDir, 'utf8')


/**
 * Configuration settings for the app.
 * @typedef Config
 * @property {Object} client
 * @property {Object} client.settings
 * @property {Object} client.rpc
 * @property {Object} user
 * @property {Object} user.trakt
 * @property {Object|false} user.trakt.auth
 * @property {Boolean} user.trakt.status
 */


/**
 * Paths of system files that are saved outside the app.
 * @typedef SystemPaths
 * @property {String} cache
 * @property {String} log
 * @property {String} config
 */

/** @type {SystemPaths} */
const PATHS = {
   cache: path.join(storeDir, '.cache'),
   log: path.join(storeDir, '.log'),
   config: path.join(storeDir, 'config.json')
}


/**
 * Initializes a directory tree that is used for system files.
 * It detects existing files and does not overwrite them to keep custom settings.
 * @returns {Promise.<SystemPaths>} Resolves paths of system files
 */
function initFileStructure() {
   fs.ensureDirSync(storeDir)

   const storeDirStructure = dirTree(storeDir)

   const defaults = {
      '.cache': {},
      '.log': 'TRAKTIFY LOG\n\n',
      'config.json': defConfigRaw
   }

   return new Promise((resolve, reject) => {
      // add missing files
      let firstChildren = storeDirStructure.children.map(c => c.name)
      let desired = ['.cache', '.log', 'config.json']
      // TODO: This will only work if the default file structure does not consist of multiple directory levels. Create helper function to make this possible if a more structured default tree is required.

      desired.map(d => {
         if(!firstChildren.includes(d)) return d
      }).filter(n => n).forEach(m => {
         if(typeof defaults[m] == 'object') {
            // in this case, the missing element was a directory
            fs.ensureDirSync(path.join(storeDir, m))
         } else {
            fs.outputFileSync(path.join(storeDir, m), defaults[m])
         }
      })

      // fix config file if necessary
      const configDir = path.join(storeDir, 'config.json')
      const configRaw = fs.readFileSync(configDir, 'utf8')

      try {
         const config = JSON.parse(configRaw)

         let missing = !Object.keys(config).includes('client')
            || !Object.keys(config).includes('user')
   
         if(missing) {
            throw 'config file is missing'
         }
      } catch (err) {
         console.error(err)
         fs.outputFileSync(configDir, defaults['config.json'])
      }

      resolve({
         cache: path.join(storeDir, '.cache'),
         log: path.join(storeDir, '.log'),
         config: configDir
      })
   })
}


/**
 * Keys, secrets and tokens used for API requests.
 * @typedef APIKeys
 * @property {String} trakt_id
 * @property {String} trakt_secret
 * @property {String} fanart_key
 */

/**
 * Searches for existing keyfiles and returns the contents.
 * If a secret version exists and is complete, it will be used over dev keys.
 * Useful to automate keys in different environment setups.
 * @returns {APIKeys} Object with API keys
 */
function getAPIKeys() {
   const onDev = process.env.NODE_ENV !== 'production'
   const appPath = process.env.APP_PATH
   const secretPath = path.join(appPath, 'keys.secret.json')
   const devPath = path.join(appPath, 'keys.dev.json')

   const requiredKeys = ['trakt_id', 'trakt_secret', 'fanart_key']

   let hasSecret = fs.existsSync(secretPath)
   
   if(hasSecret) {
      /** @type {APIKeys} */
      let secretKeys = fs.readJSONSync(secretPath)
      let fulfills = requiredKeys.map(k => Object.keys(secretKeys).includes(k))
      
      if(!fulfills.includes(false)) {
         return secretKeys
      } else if(!onDEV) {
         throw new Error('keyfile is missing keys')
      }
   }

   let hasDev = fs.existsSync(devPath)

   if(onDev && hasDev) {
      /** @type {APIKeys} */
      let devKeys = fs.readJSONSync(devPath)
      let fulfills = requiredKeys.map(k => Object.keys(devKeys).includes(k))
      
      if(!fulfills.includes(false)) {
         return devKeys
      } else {
         throw new Error('no usable keyfile found')
      }
   }
}


/**
 * Writes changes to the configuration file.
 * @param {Config} updates Config object with possibly unsaved changes
 */
function saveConfig(updates) {
   return fs.writeFileSync(PATHS.config, JSON.stringify(updates))
}


/**
 * Reads configuration file from disk.
 * @returns {Config} Configuration settings
 */
function readConfig() {
   if(!fs.existsSync(PATHS.config)) {
      return null
   }

   return JSON.parse(fs.readFileSync(PATHS.config, 'utf8'))
}


/**
 * Overwrites the configuration file with defaults and returns the data.
 * Removes authenticated users.
 * @returns {Config} The default configuration
 */
function resetConfig() {
   fs.outputFileSync(PATHS.config, defConfigRaw)
   return JSON.parse(defConfigRaw)
}


/**
 * Deletes all cache files at once.
 */
function removeCacheFiles() {
   try {
      fs.emptyDirSync(PATHS.cache)
   } catch (err) {
      console.error(err)
   }
}


module.exports = {
   PATHS,
   initFileStructure, getAPIKeys,
   saveConfig, readConfig, resetConfig,
   removeCacheFiles
}