const fs = require('fs-extra')
const path = require('path')
const log = require('../../util/log')
const manifestMBDBParse = require('../../util/manifest_mbdb_parse')
const plist = require('../../util/plist')
const Mode = require('stat-mode');

module.exports = {
  version: 4,
  name: 'backup.files',
  description: `Gets a backup's file list`,
  requiresBackup: true,

  // Run on a v3 lib / backup object.
  run (lib, { backup, extract, filter }) {
    return new Promise(async (resolve, reject) => {
      getManifest(backup)
        .then(files => {
          // Possibly extract objects.
          if (extract) {
            extractFiles(backup, extract, filter, files)
          }

          resolve(files)
        })
        .catch(reject)
    })
  },

  // Available fields.
  output: {
    id: el => el.fileID,
    domain: el => el.domain,
    path: el => el.filename,
    size: el => el.filelen || 0,
    mtime: el => el.mtime || 0,
    mode: el => new Mode(el).toString()
  }
}

/// Get the manifest for an sqlite database if available
function getSqliteFileManifest (backup) {
  return new Promise(async (resolve, reject) => {
    backup.openDatabase('Manifest.db', true)
      .then(db => {
        db.all('SELECT fileID, domain, relativePath as filename, file from FILES', async function (err, rows) {
          if (err) reject(err)

          // Extract binary plist metadata
          for (var row of rows) {
            let data = plist.parseBuffer(row.file)
            let metadata = data['$objects'][1];
            row.filelen = metadata.Size
            row.mode = metadata.Mode
            row.mtime = row.atime = metadata.LastModified
          }

          resolve(rows)
        })
      })
      .catch(reject)
  })
}

/// Get the manifest from the mbdb file
function getMBDBFileManifest (backup) {
  return new Promise((resolve, reject) => {
    let mbdbPath = backup.getFileName('Manifest.mbdb', true)
    manifestMBDBParse.process(mbdbPath, resolve, reject)
  })
}

/// Try to load both of the manifest files
function getManifest (backup) {
  return new Promise(async (resolve, reject) => {
    // Try the new sqlite file database.
    try {
      log.verbose('Trying sqlite manifest...')
      let item = await getSqliteFileManifest(backup)
      return resolve(item)
    } catch (e) {
      log.verbose('Trying sqlite manifest... [failed]', e)
    }

    // Try the mbdb file database
    try {
      log.verbose('Trying mbdb manifest...')
      let item = await getMBDBFileManifest(backup)
      return resolve(item)
    } catch (e) {
      log.verbose('Trying mbdb manifest...[failed]', e)
    }

    reject(new Error('Could not find a manifest.'))
  })
}

/// Filter exclusion check
function isIncludedByFilter (filter, item, filePath) {
  if (filter === 'all' || filter === undefined)
    return true;

  for (var f of Array.isArray(filter) ? filter : [filter]) {
    if (!isIncludedBySingleFilter(f, item, filePath))
      return false;
  }
  return true;
}

function isIncludedBySingleFilter (filter, item, filePath) {
  for (var x of [item.domain, item.filename, filePath]) {
    if (isIncludedBySingleFilterCheck(filter, x))
      return true;
  }
}

function isIncludedBySingleFilterCheck (filter, x) {
  if (filter instanceof RegExp)
    return x.search(filter) > -1
  else if (filter instanceof Function)
    return filter(x)
  else
    return x.indexOf(filter) > -1;
}

/// Extract files
/// - backup: the backup api object
/// - destination: file system location
/// - filter: contains check filter for files
/// - items: list of files.
function extractFiles (backup, destination, filter, items) {
  for (var item of items) {
    try {
      var domainPath = item.domain
      if (domainPath.match(/^AppDomain.*-/)) {
        // Extract sub-domain from app domain
        domainPath = domainPath.replace('-', path.sep)
      }

      domainPath = domainPath.replace('Domain', '')

      var filePath = path.join(domainPath, item.filename)

      // Skip items not included by the filter
      if (!isIncludedByFilter(filter, item, filePath)) {
        // Skip to the next iteration of the loop.
        log.action('skipped', filePath)
        continue
      }

      var stat = new Mode(item)

      if (stat.isSymbolicLink()) {
        log.warning('skipping symlink', filePath, 'to', item.linktarget)
        // FIXME: Restore symlinks
        continue
      }

      // Calculate the output path
      var outPath = path.join(destination, filePath)

      if (stat.isDirectory()) {
        log.action('mkdir', filePath)
        fs.ensureDirSync(outPath)
      } else if (stat.isFile()) {
        let sourceFile = backup.getFileName(item.fileID)

        // Only process files that exist.
        if (fs.existsSync(sourceFile)) {
          log.action('export', filePath)
          fs.copySync(sourceFile, outPath)
          fs.utimesSync(outPath, item.atime, item.mtime)
        } else {
          log.error('not found', sourceFile)
        }
      } else {
        throw new Error('unknown filetype')
      }

      // Save output info to the data item.
      item.output_dir = outPath

    } catch (e) {
      log.error(item.fileID, item.filename, e.toString())
    }
  }
}