// store the cwd
const cwd = process.cwd()

const path = require('path')
const _ = require('lodash')
const os = require('os')
const simpleGit = require('simple-git')
const chalk = require('chalk')
const Promise = require('bluebird')
const minimist = require('minimist')
const la = require('lazy-ass')
const check = require('check-more-types')
const debug = require('debug')('cypress:binary')
const rp = require('@cypress/request-promise')

const zip = require('./zip')
const ask = require('./ask')
const meta = require('./meta')
const build = require('./build')
const upload = require('./upload')
const questionsRemain = require('./util/questions-remain')
const uploadUtils = require('./util/upload')
const { uploadArtifactToS3 } = require('./upload-build-artifact')
const { moveBinaries } = require('./move-binaries')
const { exec } = require('child_process')
const xvfb = require('../../cli/lib/exec/xvfb').default
const smoke = require('./smoke')
const { needsSandbox } = require('../../cli/lib/tasks/verify')
const execa = require('execa')

const log = function (msg) {
  const time = new Date()
  const timeStamp = time.toLocaleTimeString()

  console.log(timeStamp, chalk.yellow(msg), chalk.blue(meta.PLATFORM))
}

const success = (str) => {
  return console.log(chalk.bgGreen(` ${chalk.black(str)} `))
}

const fail = (str) => {
  return console.log(chalk.bgRed(` ${chalk.black(str)} `))
}

const zippedFilename = () => upload.zipName

// goes through the list of properties and asks relevant question
// resolves with all relevant options set
// if the property already exists, skips the question
const askMissingOptions = function (properties = []) {
  const questions = {
    platform: ask.whichPlatform,
    version: ask.deployNewVersion,
    // note: zip file might not be absolute
    zip: ask.whichZipFile,
    commit: ask.toCommit,
  }
  const pickedQuestions = _.pick(questions, properties)

  return questionsRemain(pickedQuestions)
}

async function testExecutableVersion (buildAppExecutable, version) {
  log('#testVersion')

  console.log('testing built app executable version')
  console.log(`by calling: ${buildAppExecutable} --version`)

  const args = ['--version']

  if (needsSandbox()) {
    args.push('--no-sandbox')
  }

  const result = await execa(buildAppExecutable, args)

  la(result.stdout, 'missing output when getting built version', result)

  console.log('built app version', result.stdout)
  la(
    result.stdout.trim() === version.trim(),
    `different version reported: '${result.stdout.trim()}' from input version to build '${version.trim()}'`,
  )

  console.log('✅ using --version on the Cypress binary works')
}

// hack for @packages/server modifying cwd
process.chdir(cwd)

const commitVersion = function (version) {
  const msg = `release ${version} [skip ci]`

  return simpleGit.commit(msg, {
    '--allow-empty': null,
  })
}

const deploy = {
  meta,

  parseOptions (argv) {
    const opts = minimist(argv, {
      alias: {
        zip: ['zipFile', 'zip-file', 'filename'],
      },
    })

    if (opts['skip-tests']) {
      opts.runTests = false
    }

    if (!opts.platform) opts.platform = os.platform()

    debug('parsed command line options')
    debug(opts)

    return opts
  },

  release () {
    // read off the argv
    const options = this.parseOptions(process.argv)

    const release = ({ version, commit }) => {
      return upload.s3Manifest(version)
      .then(() => {
        if (commit) {
          return commitVersion(version)
        }
      }).then(() => {
        return success('Release Complete')
      }).catch((err) => {
        fail('Release Failed')
        throw err
      })
      .then(() => {
        return this.checkManifest({ version })
      })
      .then(() => {
        return this.checkDownloads({ version })
      })
    }

    return askMissingOptions(['version'])(options)
    .then(release)
  },

  checkManifest ({ version }) {
    const checkManifest = () => {
      const url = `https://download.cypress.io/desktop.json`

      process.stdout.write(`Checking for ${chalk.yellow(version)} in the manifest at ${chalk.cyan(url)} ... `)

      return rp.get(url)
      .then((res) => {
        const manifest = JSON.parse(res)

        return manifest
      })
    }

    return checkManifest().then((manifest) => {
      const versionMatches = manifest.version === version

      process.stdout.write(`${versionMatches ? '✅' : '❌'}\n`)

      if (!versionMatches) {
        console.log(chalk.red(`\nFound ${manifest.version} in the manifest, but ${version} was requested.`))

        process.exit(1)
      }
    })
  },

  checkDownloads ({ version }) {
    const systems = [
      { platform: 'linux', arch: 'x64' },
      { platform: 'linux', arch: 'arm64' },
      { platform: 'darwin', arch: 'x64' },
      { platform: 'darwin', arch: 'arm64' },
      { platform: 'win32', arch: 'x64' },
    ]

    const urlExists = (url) => {
      return rp.head(url)
      .then(() => true)
      .catch(() => false)
    }

    const checkSystem = ({ platform, arch }) => {
      const url = `https://download.cypress.io/desktop/${version}?platform=${platform}&arch=${arch}`
      const system = `${platform}-${arch}`

      process.stdout.write(`Checking for ${chalk.yellow(system)} at ${chalk.cyan(url)} ... `)

      return urlExists(url)
      .then((exists) => {
        const result = exists ? '✅' : '❌'

        process.stdout.write(`${result}\n`)

        return { exists, platform, arch, url }
      })
    }

    const allEnsured = (results) => {
      return !results.filter(({ exists }) => !exists).length
    }

    return Promise.mapSeries(systems, checkSystem)
    .then((results) => {
      if (allEnsured(results)) return results

      console.log(chalk.red(`\nCould not ensure v${version} of the Cypress binary is available for the following systems:`))

      return results
    })
    .map((result) => {
      const { exists, platform, arch, url } = result

      if (exists) return result

      console.log(`
  ${chalk.yellow('Platform')}: ${platform}
  ${chalk.yellow('Arch')}: ${arch}
  ${chalk.yellow('URL')}: ${url}`)

      return result
    })
    .then((results) => {
      if (allEnsured(results)) return

      const purgeCommand = `yarn binary-purge --version ${version}`
      const ensureCommand = `yarn binary-ensure --version ${version}`

      console.log(`\nPurge the cloudflare cache with ${chalk.yellow(purgeCommand)} and check again with ${chalk.yellow(ensureCommand)}\n`)

      process.exit(1)
    })
  },

  ensure () {
    const options = this.parseOptions(process.argv)

    return questionsRemain({ version: ask.getEnsureVersion })(options)
    .then(this.checkDownloads)
  },

  build (options) {
    console.log('#build')
    if (options == null) {
      options = this.parseOptions(process.argv)
    }

    debug('parsed build options %o', options)

    return askMissingOptions(['version', 'platform'])(options)
    .then(() => {
      console.log('building binary: platform %s version %s', options.platform, options.version)

      return build.buildCypressApp(options)
    })
  },

  package (options) {
    console.log('#package')
    if (options == null) {
      options = this.parseOptions(process.argv)
    }

    debug('parsed build options %o', options)

    return askMissingOptions(['version', 'platform'])(options)
    .then(() => {
      console.log('packaging binary: platform %s version %s', options.platform, options.version)

      return build.packageElectronApp(options)
    })
  },

  async smoke (options) {
    console.log('#smoke')

    if (options == null) {
      options = this.parseOptions(process.argv)
    }

    debug('parsed build options %o', options)

    await askMissingOptions(['version'])(options)

    // runSmokeTests
    let usingXvfb = xvfb.isNeeded()

    try {
      if (usingXvfb) {
        await xvfb.start()
      }

      log(`#testExecutableVersion ${meta.buildAppExecutable()}`)
      await testExecutableVersion(meta.buildAppExecutable(), options.version)

      const executablePath = meta.buildAppExecutable()

      await smoke.test(executablePath, meta.buildAppDir())
    } finally {
      if (usingXvfb) {
        await xvfb.stop()
      }
    }
  },

  zip (options) {
    console.log('#zip')
    if (!options) {
      options = this.parseOptions(process.argv)
    }

    return askMissingOptions(['platform'])(options)
    .then((options) => {
      const zipDir = meta.zipDir(options.platform)

      console.log('directory to zip %s', zipDir)
      options.zip = path.resolve(zippedFilename(options.platform))

      return zip.ditto(zipDir, options.zip)
    })
  },

  // upload Cypress binary or NPM Package zip file under unique hash
  'upload-build-artifact' (args = process.argv) {
    console.log('#uploadBuildArtifact')

    return uploadArtifactToS3(args)
  },

  // uploads a single built Cypress binary ZIP file
  // usually a binary is built on CI and is uploaded
  upload (options) {
    console.log('#upload')

    if (!options) {
      options = this.parseOptions(process.argv)
    }

    return askMissingOptions(['version', 'platform', 'zip'])(options)
    .then((options) => {
      la(check.unemptyString(options.zip),
        'missing zipped filename', options)

      options.zip = path.resolve(options.zip)

      return options
    }).then((options) => {
      console.log('Need to upload file %s', options.zip)
      console.log('for platform %s version %s',
        options.platform, options.version)

      const uploadPath = upload.getFullUploadPath({
        version: options.version,
        platform: options.platform,
        name: upload.zipName,
      })

      return upload.toS3({
        file: options.zip,
        uploadPath,
      }).then(() => {
        return uploadUtils.purgeDesktopAppFromCache({
          version: options.version,
          platform: options.platform,
          zipName: options.zip,
        })
      })
    })
  },

  'move-binaries' (args = process.argv) {
    console.log('#moveBinaries')

    return moveBinaries(args)
  },

  // purge all URLs from Cloudflare cache from file
  'purge-urls' (args = process.argv) {
    console.log('#purge-urls')

    const options = minimist(args, {
      string: 'filePath',
      alias: {
        filePath: 'f',
      },
    })

    la(check.unemptyString(options.filePath), 'missing file path to url list', options)

    return uploadUtils.purgeUrlsFromCloudflareCache(options.filePath)
  },

  // purge all platforms of a desktop app for specific version
  'purge-version' (args = process.argv) {
    console.log('#purge-version')
    const options = minimist(args, {
      string: 'version',
      alias: {
        version: 'v',
      },
    })

    la(check.unemptyString(options.version), 'missing app version to purge', options)

    return uploadUtils.purgeDesktopAppAllPlatforms(options.version, upload.zipName)
  },

  // goes through the entire pipeline:
  //   - build
  //   - zip
  //   - upload
  deploy () {
    const options = this.parseOptions(process.argv)

    return askMissingOptions(['version', 'platform'])(options)
    .then((options) => {
      return this.build(options)
      .then(() => {
        return this.zip(options)
      })
      // assumes options.zip contains the zipped filename
      .then(() => {
        return this.upload(options)
      })
    })
  },

  async checkIfBinaryExistsOnCdn (args = process.argv) {
    console.log('#checkIfBinaryExistsOnCdn')

    const url = await uploadArtifactToS3([...args, '--dry-run', 'true'])

    console.log(`Checking if ${url} exists...`)

    const binaryExists = await rp.head(url)
    .then(() => true)
    .catch(() => false)

    if (binaryExists) {
      console.log('A binary was already built for this operating system and commit hash. Skipping binary build process...')
      exec('circleci-agent step halt', (_, __, stdout) => {
        console.log(stdout)
      })

      return
    }

    console.log('Binary does not yet exist. Continuing to build binary...')

    return binaryExists
  },
}

module.exports = _.bindAll(deploy, _.functions(deploy))
