const Promise = require('bluebird')
const os = require('os')
const execa = require('execa')
const path = require('path')
const la = require('lazy-ass')
const fs = require('fs')
const { filesize } = require('filesize')

// prints disk usage numbers using "du" utility
// available on Linux and Mac
const printFileSizes = function (folder) {
  console.log(`File sizes in ${folder}`)
  const paths = path.join(folder, '*')
  const options = {
    stdio: 'inherit',
    shell: true,
  }

  return execa(`du -hs ${paths}`, options)
}

// resolves with zipped filename
const macZip = (src, dest) => {
  return printFileSizes(src)
  .then(() => {
    if (os.platform() !== 'darwin') {
      throw new Error('Can only zip on Mac platform')
    }

    // Ditto (Mac) options
    // http://www.unix.com/man-page/OSX/1/ditto/
    // -c create archive
    // -k set archive format to PKZip
    // --sequesterRsrc When creating a PKZip archive, preserve resource
    //   forks and HFS meta-data in the subdirectory __MACOSX
    // --keepParent when zipping folder "foo", makes the folder
    //   the top level in the archive
    //   foo.zip
    //     foo/
    //        ...
    const zip = `ditto -c -k --sequesterRsrc --keepParent ${src} ${dest}`
    const options = {
      stdio: 'inherit',
      shell: true,
    }

    console.log(zip)

    const onZipFinished = () => {
      return console.log('✅ ditto finished')
    }

    const onError = function (err) {
      console.error(`⛔️ could not zip ${src} into ${dest}`)
      console.error(err.message)
      throw err
    }

    return execa(zip, options)
    .then(onZipFinished)
    .then(() => dest)
    .catch(onError)
  })
}

const megaBytes = (bytes) => {
  return 1024 * 1024 * bytes
}

const checkZipSize = function (zipPath) {
  const stats = fs.statSync(zipPath)
  const zipSize = filesize(stats.size, { round: 0 })

  console.log(`zip file size ${zipSize}`)
  // Before you modify these max sizes, check and see what you did that might have
  // done to increase the size of the binary, and if you do need to change it,
  // call it out in the PR description / comments
  const MAX_ALLOWED_SIZE_MB = os.platform() === 'win32' ? 295 : 256
  const MAX_ZIP_FILE_SIZE = megaBytes(MAX_ALLOWED_SIZE_MB)

  if (stats.size > MAX_ZIP_FILE_SIZE) {
    throw new Error(`Zip file is too large: ${zipSize} (${stats.size} bytes) exceeds ${MAX_ZIP_FILE_SIZE} bytes`)
  }
}

const linuxZipAction = function (parentFolder, dest, relativeSource) {
  console.log(`zipping ${parentFolder}`)
  const cmd = `cd ${parentFolder} && zip -r9 ${dest} ${relativeSource}`

  console.log(`linux zip: ${cmd}`)

  const onZipFinished = () => {
    return console.log('✅ zip finished')
  }

  const onError = function (err) {
    console.error(`⛔️ could not zip ${relativeSource} in folder ${parentFolder}`)
    console.error(`to produce ${dest}`)
    console.error(err.message)
    throw err
  }

  return execa(cmd, { shell: true })
  .then(onZipFinished)
  .then(() => dest)
  .then((val) => {
    checkZipSize(val)

    return val
  })
  .catch(onError)
}

// src is built folder with packed Cypress application
// like /root/app/build/linux-unpacked or build/win-unpacked
// and we want to always have /root/app/build/Cypress
const renameFolder = function (src) {
  const parentFolder = path.dirname(src)
  const folderName = path.basename(src)

  if (folderName === 'Cypress') {
    console.log('nothing to rename, folder "%s" ends with Cypress', src)

    return Promise.resolve(src)
  }

  const renamed = path.join(parentFolder, 'Cypress')

  console.log(`renaming ${src} to ${renamed}`)

  return fs.promises.rename(src, renamed)
  .then(() => renamed)
}

// resolves with zipped filename
const linuxZip = function (src, dest) {
  // in Linux switch to the folder containing source folder
  la(path.isAbsolute(src), 'source path should be absolute', src)
  la(path.isAbsolute(dest), 'destination path should be absolute', dest)

  // on Linux, make sure the folder name is "Cypress" first
  return renameFolder(src)
  .then((renamedSource) => {
    return printFileSizes(renamedSource)
    .then(() => renamedSource)
  }).then((renamedSource) => {
    console.log(`will zip folder ${renamedSource}`)
    const parentFolder = path.dirname(renamedSource)
    const relativeSource = path.basename(renamedSource)

    return linuxZipAction(parentFolder, dest, relativeSource)
  })
}

// resolves with zipped filename
const windowsZipAction = function (src, dest) {
  // use 7Zip to zip
  // http://www.7-zip.org/
  // zips entire source directory including top level folder name
  //   Cypress/
  //     foo.txt
  // creates cypress.zip for example
  // unzip cypress.zip to get back the folder
  //   Cypress/
  //     foo.txt
  const cmd = `7z a ${dest} ${src}`

  console.log(`windows zip: ${cmd}`)

  const onZipFinished = () => {
    return console.log('✅ 7z finished')
  }

  const onError = function (err) {
    console.error(`⛔️ could not zip ${src} into ${dest}`)
    console.error(err.message)
    throw err
  }

  return execa(cmd, { shell: true })
  .then(onZipFinished)
  .then(() => dest)
  .then((val) => {
    checkZipSize(val)

    return val
  })
  .catch(onError)
}

const windowsZip = (src, dest) => {
  return renameFolder(src)
  .then((renamedSource) => {
    return windowsZipAction(renamedSource, dest)
  })
}

const zippers = {
  linux: linuxZip,
  darwin: macZip,
  win32: windowsZip,
}

module.exports = {
  // zip Cypress folder to create destination zip file
  // uses tool depending on the platform
  ditto (src, dest) {
    const platform = os.platform()

    console.log('#zip', platform)
    console.log('Zipping %s into %s', src, dest)

    const zipper = zippers[platform]

    if (!zipper) {
      throw new Error(`Missing zip function for platform ${platform}`)
    }

    return zipper(src, dest)
  },

  checkZipSize,
}
