const _ = require('lodash')
const fse = require('fs-extra')
const os = require('os')
const del = require('del')
const path = require('path')
const cp = require('child_process')
const chalk = require('chalk')
const Promise = require('bluebird')
const pluralize = require('pluralize')
const execa = require('execa')
const electron = require('@packages/electron')
const debug = require('debug')('cypress:binary')
const R = require('ramda')
const la = require('lazy-ass')
const check = require('check-more-types')

const meta = require('./meta')
const smoke = require('./smoke')
const packages = require('./util/packages')
const xvfb = require('../../cli/lib/exec/xvfb')
const { transformRequires } = require('./util/transform-requires')
const { testStaticAssets } = require('./util/testStaticAssets')
const performanceTracking = require('../../packages/server/test/support/helpers/performance.js')

const rootPackage = require('@packages/root')

const fs = Promise.promisifyAll(fse)

const logger = function (msg, platform) {
  const time = new Date()
  const timeStamp = time.toLocaleTimeString()

  return console.log(timeStamp, chalk.yellow(msg), chalk.blue(platform))
}

const logBuiltAllPackages = () => {
  return console.log('built all packages')
}

// can pass options to better control the build
// for example
//   skipClean - do not delete "dist" folder before build
const buildCypressApp = function (platform, version, options = {}) {
  la(check.unemptyString(version), 'missing version to build', version)

  const distDir = _.partial(meta.distDir, platform)
  const buildDir = _.partial(meta.buildDir, platform)
  const buildAppDir = _.partial(meta.buildAppDir, platform)

  const log = _.partialRight(logger, platform)

  const testVersion = (folderNameFn) => {
    return (function () {
      log('#testVersion')
      const dir = folderNameFn()

      la(check.unemptyString(dir), 'missing folder for platform', platform)
      console.log('testing dist package version')
      console.log('by calling: node index.js --version')
      console.log('in the folder %s', dir)

      return execa('node', ['index.js', '--version'], {
        cwd: dir,
      }).then((result) => {
        la(check.unemptyString(result.stdout),
          'missing output when getting built version', result)

        console.log('app in %s', dir)
        console.log('built app version', result.stdout)
        la(result.stdout === version, 'different version reported',
          result.stdout, 'from input version to build', version)

        return console.log('✅ using node --version works')
      })
    })
  }

  const testBuiltStaticAssets = function () {
    log('#testBuiltStaticAssets')

    return testStaticAssets(distDir())
  }

  const checkPlatform = function () {
    log('#checkPlatform')
    if (platform === os.platform()) {
      return
    }

    return Promise.reject(new Error('Build platform mismatch'))
  }

  const cleanupPlatform = function () {
    log('#cleanupPlatform')

    if (options.skipClean) {
      log('skipClean')

      return
    }

    const cleanup = function () {
      const dir = distDir()

      la(check.unemptyString(dir), 'empty dist dir', dir, 'for platform', platform)

      return fs.removeAsync(distDir())
    }

    return cleanup()
    .catch(cleanup)
  }

  const buildPackages = function () {
    log('#buildPackages')

    return packages.runAllBuild()
    // Promise.resolve()
    .then(R.tap(logBuiltAllPackages))
  }

  const copyPackages = function () {
    log('#copyPackages')

    return packages.copyAllToDist(distDir())
  }

  const replaceLocalNpmVersions = function () {
    log('#replaceLocalNpmVersions')

    return packages.replaceLocalNpmVersions(distDir())
  }

  const npmInstallPackages = function () {
    log('#npmInstallPackages')

    const pathToPackages = distDir('packages', '*')

    return packages.npmInstallAll(pathToPackages)
  }

  const cleanLocalNpmPackages = function () {
    log('#cleanLocalNpmPackages')

    return fs.removeAsync(distDir('npm'))
  }

  /**
   * Creates the package.json file that sits in the root of the output app
   */
  const createRootPackage = function () {
    log(`#createRootPackage ${platform} ${version}`)

    const electronVersion = electron.getElectronVersion()

    la(electronVersion, 'missing Electron version', electronVersion)

    return electron.getElectronNodeVersion()
    .then((electronNodeVersion) => {
      la(electronNodeVersion, 'missing Electron Node version', electronNodeVersion)

      const json = {
        name: 'cypress',
        productName: 'Cypress',
        description: rootPackage.description,
        version, // Cypress version
        electronVersion,
        electronNodeVersion,
        main: 'index.js',
        scripts: {},
        env: 'production',
      }

      const outputFilename = distDir('package.json')

      debug('writing to %s json %o', outputFilename, json)

      return fs.outputJsonAsync(outputFilename, json)
      .then(() => {
        const str = `\
process.env.CYPRESS_INTERNAL_ENV = process.env.CYPRESS_INTERNAL_ENV || 'production'
require('./packages/server')\
`

        return fs.outputFileAsync(distDir('index.js'), str)
      })
    })
  }

  const removeTypeScript = function () {
    // remove the .ts files in our packages
    log('#removeTypeScript')

    return del([
      // include ts files of packages
      distDir('**', '*.ts'),

      // except those in node_modules
      `!${distDir('**', 'node_modules', '**', '*.ts')}`,
    ])
    .then((paths) => {
      console.log(
        'deleted %d TS %s',
        paths.length,
        pluralize('file', paths.length),
      )

      return console.log(paths)
    })
  }

  const cleanJs = function () {
    log('#cleanJs')

    return packages.runAllCleanJs()
  }

  const transformSymlinkRequires = function () {
    log('#transformSymlinkRequires')

    return transformRequires(distDir())
    .then((replaceCount) => {
      return la(replaceCount > 5, 'expected to replace more than 5 symlink requires, but only replaced', replaceCount)
    })
  }

  // we also don't need ".bin" links inside Electron application
  // thus we can go through dist/packages/*/node_modules and remove all ".bin" folders
  const removeBinFolders = function () {
    log('#removeBinFolders')

    const searchMask = distDir('packages', '*', 'node_modules', '.bin')

    console.log('searching for', searchMask)

    return del([searchMask])
    .then((paths) => {
      console.log(
        'deleted %d .bin %s',
        paths.length,
        pluralize('folder', paths.length),
      )

      return console.log(paths)
    })
  }

  const removeCyFolders = function () {
    log('#removeCyFolders')

    const searchMask = distDir('packages', 'server', '.cy')

    console.log('searching', searchMask)

    return del([searchMask])
    .then((paths) => {
      console.log(
        'deleted %d .cy %s',
        paths.length,
        pluralize('file', paths.length),
      )

      return console.log(paths)
    })
  }

  const getIconFilename = function (platform) {
    const filenames = {
      darwin: 'cypress.icns',
      win32: 'cypress.ico',
      linux: 'icon_512x512.png',
    }
    const iconFilename = electron.icons().getPathToIcon(filenames[platform])

    console.log(`For platform ${platform} using icon ${iconFilename}`)

    return iconFilename
  }

  const removeDevElectronApp = function () {
    log('#removeDevElectronApp')
    // when we copy packages/electron, we get the "dist" folder with
    // empty Electron app, symlinked to our server folder
    // in production build, we do not need this link, and it
    // would not work anyway with code signing

    // hint: you can see all symlinks in the build folder
    // using "find build/darwin/Cypress.app/ -type l -ls"
    console.log('platform', platform)
    const electronDistFolder = distDir('packages', 'electron', 'dist')

    la(check.unemptyString(electronDistFolder),
      'empty electron dist folder for platform', platform)

    console.log(`Removing unnecessary folder '${electronDistFolder}'`)

    return fs.removeAsync(electronDistFolder) // .catch(_.noop) why are we ignoring an error here?!
  }

  const electronPackAndSign = function () {
    log('#electronPackAndSign')

    // See the internal wiki document "Signing Test Runner on MacOS"
    // to learn how to get the right Mac certificate for signing and notarizing
    // the built Test Runner application

    const appFolder = distDir()
    const outputFolder = meta.buildRootDir(platform)
    const electronVersion = electron.getElectronVersion()

    la(check.unemptyString(electronVersion), 'missing Electron version to pack', electronVersion)
    const iconFilename = getIconFilename(platform)

    console.log(`output folder: ${outputFolder}`)

    const args = [
      '--publish=never',
      `--c.electronVersion=${electronVersion}`,
      `--c.directories.app=${appFolder}`,
      `--c.directories.output=${outputFolder}`,
      `--c.icon=${iconFilename}`,
      // for now we cannot pack source files in asar file
      // because electron-builder does not copy nested folders
      // from packages/*/node_modules
      // see https://github.com/electron-userland/electron-builder/issues/3185
      // so we will copy those folders later ourselves
      '--c.asar=false',
    ]
    const opts = {
      stdio: 'inherit',
    }

    console.log('electron-builder arguments:')
    console.log(args.join(' '))

    return execa('electron-builder', args, opts)
  }

  const lsDistFolder = function () {
    log('#lsDistFolder')
    const buildFolder = buildDir()

    console.log('in build folder %s', buildFolder)

    return execa('ls', ['-la', buildFolder])
    .then(R.prop('stdout'))
    .then(console.log)
  }

  const runSmokeTests = function () {
    log('#runSmokeTests')

    const run = function () {
      // make sure to use a longer timeout - on Mac the first
      // launch of a built application invokes gatekeeper check
      // which takes a couple of seconds
      const executablePath = meta.buildAppExecutable(platform)

      return smoke.test(executablePath)
    }

    if (xvfb.isNeeded()) {
      return xvfb.start()
      .then(run)
      .finally(xvfb.stop)
    }

    return run()
  }

  const verifyAppCanOpen = function () {
    if (platform !== 'darwin') {
      return Promise.resolve()
    }

    const appFolder = meta.zipDir(platform)

    log(`#verifyAppCanOpen ${appFolder}`)

    return new Promise((resolve, reject) => {
      const args = ['-a', '-vvvv', appFolder]

      debug(`cmd: spctl ${args.join(' ')}`)
      const sp = cp.spawn('spctl', args, { stdio: 'inherit' })

      return sp.on('exit', (code) => {
        if (code === 0) {
          return resolve()
        }

        return reject(new Error('Verifying App via GateKeeper failed'))
      })
    })
  }

  const printPackageSizes = function () {
    const appFolder = meta.buildAppDir(platform, 'packages')

    log(`#printPackageSizes ${appFolder}`)

    if (platform === 'win32') {
      return Promise.resolve()
    }

    // "du" - disk usage utility
    // -d -1 depth of 1
    // -h human readable sizes (K and M)
    const args = ['-d', '1', appFolder]

    const parseDiskUsage = function (result) {
      const lines = result.stdout.split(os.EOL)
      // will store {package name: package size}
      const data = {}

      lines.forEach((line) => {
        const parts = line.split('\t')
        const packageSize = parseFloat(parts[0])
        const folder = parts[1]

        const packageName = path.basename(folder)

        if (packageName === 'packages') {
          return // root "packages" information
        }

        data[packageName] = packageSize
      })

      return data
    }

    const printDiskUsage = function (sizes) {
      const bySize = R.sortBy(R.prop('1'))

      return console.log(bySize(R.toPairs(sizes)))
    }

    return execa('du', args)
    .then(parseDiskUsage)
    .then(R.tap(printDiskUsage))
    .then((sizes) => {
      return performanceTracking.track('test runner size', sizes)
    })
  }

  return Promise.resolve()
  .then(checkPlatform)
  .then(cleanupPlatform)
  .then(buildPackages)
  .then(copyPackages)
  .then(replaceLocalNpmVersions)
  .then(npmInstallPackages)
  .then(cleanLocalNpmPackages)
  .then(createRootPackage)
  .then(removeTypeScript)
  .then(cleanJs)
  .then(transformSymlinkRequires)
  .then(testVersion(distDir))
  .then(testBuiltStaticAssets)
  .then(removeBinFolders)
  .then(removeCyFolders)
  .then(removeDevElectronApp)
  .then(electronPackAndSign)
  .then(lsDistFolder)
  .then(testVersion(buildAppDir))
  .then(runSmokeTests)
  .then(verifyAppCanOpen)
  .then(printPackageSizes)
  .return({
    buildDir: buildDir(),
  })
}

module.exports = buildCypressApp
