'use strict'
const os = require('os')
const path = require('path')
const crypto = require('crypto')
const {promises: fs, existsSync, statSync} = require('fs')
const nodeCleanup = require('node-cleanup')

const {
  getExpectedBinHash,
  getBinHash,
  getSupportedPlatformAndArchList,
} = require('@applitools/eg-frpc')

const {getCacheDirectoryPath, FrpcDownloadError} = require('../src/utils')

const {
  createDirectoryIfDoesntExist,
  createLogger,
  deleteOldFiles,
  extraceDataFromRequest,
  findFreePort,
  initSocks5ProxyServer,
} = require('../src/utils')
const {downloadFrpc, getFrpcDownloadLink, getManualFrpcInstallingErrorMessage} = require('./frpc')

const createApp = require('../src/execution-grid-tunnel')

let heartbeatIntervalId, deleteOldFilesInterval, cleanupFunction

const HOUR_IN_MILLISECONDS = 60 * 60 * 1000
const DAY_IN_MILLISECONDS = 24 * HOUR_IN_MILLISECONDS

const FILES_ROOT_DIRECTORY = path.resolve(os.tmpdir(), `eg-applitools`)
const serviceLoggerDirectory = path.resolve(FILES_ROOT_DIRECTORY, 'execution-grid-tunnel-logs')
createDirectoryIfDoesntExist(FILES_ROOT_DIRECTORY)

let serviceLogger

const PLATFORM = os.platform()
const ARCH = os.arch()

const CACHE_DIRECTORY_PATH =
  process.env.APPLITOOLS_EG_FRPC_CACHE_DIRECTORY ||
  path.join(getCacheDirectoryPath(os.platform()), 'eg-applitools')

let isPrepareEnvironmentRunning = false

async function prepareEnvironment(args) {
  if (isPrepareEnvironmentRunning) {
    if (args['logger'])
      args['logger'].info({action: 'prepare-environment-called-while-already-running'})
    if (serviceLogger)
      serviceLogger.info({action: 'prepare-environment-called-while-already-running'})
    throw Error('PREPARE_ENV_ALREADY_RUNNING')
  }
  isPrepareEnvironmentRunning = true
  return prepareEnvironmentDo({...args}).finally(() => {
    isPrepareEnvironmentRunning = false
  })
}

async function prepareEnvironmentDo({
  shouldInstallFrpc = true,
  customTestCacheDirectoryPath,
  egTunnelManagerUrl = process.env.APPLITOOLS_EG_TUNNEL_MANAGER_URL ||
    'https://exec-wus.applitools.com',
  logger,
  mode = process.env.APPLITOOLS_EG_TUNNEL_MODE_ENV, // Set it to 'development' only in tests
} = {}) {
  if (logger) {
    if (!logger.info || !logger.warn || !logger.error) {
      throw new Error(
        'Logger should support the next functions: logger.info, logger.warn, logger.log, logger.error',
      )
    }
    if (serviceLogger && serviceLogger !== logger) {
      serviceLogger.info({action: 'replace-logger-on-prepare-environment'})
    }
    serviceLogger = logger
  } else {
    serviceLogger = createLogger({
      mode,
      dirname: serviceLoggerDirectory,
      level: process.env.APPLITOOLS_EG_TUNNEL_LOG_LEVEL || 'error',
    })
  }
  const platform = PLATFORM,
    arch = ARCH
  const cacheDirectoryPath = customTestCacheDirectoryPath || CACHE_DIRECTORY_PATH
  const fileSuffix = PLATFORM === 'win32' ? '.exe' : ''
  const frpcPath = path.resolve(cacheDirectoryPath, `frpc${fileSuffix}`)

  serviceLogger.info({
    action: 'start-prepare-environment',
    success: true,
    platform,
    arch,
    cacheDirectoryPath,
  })

  await createDirectoryIfDoesntExist(CACHE_DIRECTORY_PATH)

  const isPlatformAndArchSupported = getSupportedPlatformAndArchList().some(
    (item) => item.platform === platform && item.arch === arch,
  )

  if (!isPlatformAndArchSupported) {
    serviceLogger.info({
      action: 'unsupported-environment',
      success: false,
      platform,
      arch,
    })
    throw `execution-grid-tunnel doesn't support ${PLATFORM} with ${ARCH} architecture yet`
  }

  serviceLogger.info({
    action: 'start-frpc-verification',
  })

  const expectedHash = getExpectedBinHash({platform, arch})

  let isFrpcExists = existsSync(frpcPath)
  const binHash = isFrpcExists ? await getBinHash(frpcPath) : 'none'
  let isExpectedFrpc = isFrpcExists && binHash === expectedHash

  if (!isExpectedFrpc && !shouldInstallFrpc) {
    const url = getFrpcDownloadLink({platform, arch, egTunnelManagerUrl})
    const message = getManualFrpcInstallingErrorMessage({
      platform,
      arch,
      cacheDirectoryPath,
      frpcPath,
      shouldUpdate: isFrpcExists,
      egTunnelManagerUrl,
    })
    serviceLogger.info({
      action: 'verify-frpc-failed',
      reason: `user specified that frpc shouldn't be installed and the existing frpc is not the expected one`,
      isFrpcExists: isExpectedFrpc,
      isExpectedFrpc,
      expectedHash,
      actualHash: binHash,
      clientMessage: message,
      url: url || 'none',
    })
    throw new FrpcDownloadError(url, CACHE_DIRECTORY_PATH, frpcPath, message)
  }

  if (!isExpectedFrpc) {
    await downloadFrpc({
      platform,
      arch,
      cacheDirectoryPath,
      frpcPath,
      shouldUpdate: isFrpcExists,
      egTunnelManagerUrl,
      logger: serviceLogger,
    })
    serviceLogger.info({
      action: 'finish-frpc-verification',
      didDownloadFrpc: true,
      oldHash: binHash,
      newHash: expectedHash,
    })
  } else {
    serviceLogger.info({
      action: 'finish-frpc-verification',
      didDownloadFrpc: false,
      binHash,
    })
  }

  return {frpcPath}
}

async function startEgTunnelService({
  port = process.env.APPLITOOLS_EG_TUNNEL_PORT,
  host = process.env.APPLITOOLS_EG_TUNNEL_HOST || 'localhost',
  egTunnelManagerUrl = process.env.APPLITOOLS_EG_TUNNEL_MANAGER_URL ||
    'https://exec-wus.applitools.com',
  heartbeatInterval = process.env.APPLITOOLS_EG_TUNNEL_HEARTBEAT_INRERVAL || 3000,
  portRange = {
    min: parseInt(process.env.APPLITOOLS_EG_TUNNEL_MIN_PORT_RANGE) || 40000,
    max: parseInt(process.env.APPLITOOLS_EG_TUNNEL_MAX_PORT_RANGE) || 50000,
  },
  tunnelConfigFileDirectory = process.env.APPLITOOLS_EG_TUNNEL_CONFIG_FILE_DIRECTORY ||
    path.join(FILES_ROOT_DIRECTORY, 'tunnels/configs'),
  tunnelLogFileDirectory = process.env.APPLITOOLS_EG_TUNNEL_LOG_FILES_DIRECTORY ||
    path.join(FILES_ROOT_DIRECTORY, 'tunnels/logs'),

  configFileMaxAge = parseInt(process.env.APPLITOOLS_EG_TUNNEL_CONFIG_FILE_MAX_AGE) ||
    DAY_IN_MILLISECONDS,
  tunnelLogFileMaxAge = parseInt(process.env.APPLITOOLS_EG_TUNNEL_LOG_FILE_MAX_AGE) ||
    DAY_IN_MILLISECONDS,
  egTunnelServiceLogFileMaxAge = 7 * DAY_IN_MILLISECONDS,
  logger,
  sendTunnelHeartBeatAlways = false, // Set it to true only in tests. By default, you shouldn't send heartbeat if there is no tunnel
  shouldInstallFrpc = true,
  customTestCacheDirectoryPath,
  mode = process.env.APPLITOOLS_EG_TUNNEL_MODE_ENV, // Set it to 'development' only in tests
} = {}) {
  if (logger) {
    if (!logger.info || !logger.warn || !logger.error) {
      throw new Error(
        'Logger should support the next functions: logger.info, logger.warn, logger.log, logger.error',
      )
    }
    if (serviceLogger && serviceLogger !== logger) {
      serviceLogger.info({action: 'replace-logger-on-start-eg-tunnel'})
    }
    serviceLogger = logger
  } else {
    serviceLogger = createLogger({
      mode,
      dirname: serviceLoggerDirectory,
      level: process.env.APPLITOOLS_EG_TUNNEL_LOG_LEVEL || 'error',
    })
  }

  const {frpcPath} = await prepareEnvironment({
    shouldInstallFrpc,
    customTestCacheDirectoryPath,
    logger: serviceLogger,
  })

  createDirectoryIfDoesntExist(serviceLoggerDirectory)
  createDirectoryIfDoesntExist(tunnelConfigFileDirectory)
  createDirectoryIfDoesntExist(tunnelLogFileDirectory)

  // init socks5 proxy server
  const socks5ProxyPort = await findFreePort(portRange.min, portRange.max)
  const socks5Proxy = await initSocks5ProxyServer({
    port: socks5ProxyPort,
    logger: serviceLogger,
  })
  const socks5ProxyServer = socks5Proxy.server

  serviceLogger.info({
    action: 'init-socks5-proxy-server',
    success: true,
    port: socks5ProxyServer.address().port,
  })

  const app = createApp({
    egTunnelManagerUrl,
    tunnelConfigFileDirectory,
    logFileDirectory: tunnelLogFileDirectory,
    runTunnelBinPath: frpcPath,
    socks5Proxies: [socks5ProxyServer],
    portRange,
    logger: serviceLogger,
  })

  // remove old config and log files
  const deleteAllOldFiles = () => {
    const items = [
      {
        directoryPath: tunnelConfigFileDirectory,
        maximunFileAge: configFileMaxAge,
      },
      {
        directoryPath: tunnelLogFileDirectory,
        maximunFileAge: tunnelLogFileMaxAge,
      },
      {
        directoryPath: serviceLoggerDirectory,
        maximunFileAge: egTunnelServiceLogFileMaxAge,
      },
    ]

    items.forEach(({directoryPath, maximunFileAge}) => {
      deleteOldFiles({directoryPath, maximunFileAge}).catch((error) => {
        serviceLogger.error({
          action: 'delete-old-files',
          success: false,
          directoryPath,
          maximunFileAge,
          error,
        })
      })
    })
  }

  deleteAllOldFiles()
  deleteOldFilesInterval = setInterval(deleteAllOldFiles, HOUR_IN_MILLISECONDS)

  heartbeatIntervalId = setInterval(() => {
    const tunnelIds = app.tunnelProcessManager.getActiveTunnels()

    if (!tunnelIds.length && !sendTunnelHeartBeatAlways) return

    extraceDataFromRequest(app.egTunnelManager.sendHeartBeat(tunnelIds)).catch(
      ({error, statusCode}) => {
        serviceLogger.error({
          action: 'send-heartbeat',
          success: false,
          error,
          statusCode,
        })
      },
    )
  }, heartbeatInterval)

  await app.ready()

  if (port === undefined) {
    port = 0
  }

  // const address = await app.listen({port, host})
  const address = await app.listen({port, host})

  if (port === 0) {
    port = app.server.address().port
  }
  console.log(
    `execution-grid-tunnel service is available on http://localhost:${app.server.address().port}`,
  )

  serviceLogger.info({
    action: 'listen-app',
    address,
    success: true,
    address: `http://localhost:${port}`,
  })

  cleanupFunction = async (_exitCode, _signal) => {
    clearInterval(heartbeatIntervalId)
    clearInterval(deleteOldFilesInterval)

    // Stop all tunnels in cleanup
    app.tunnelProcessManager.stopAll().catch(console.log)
    app.close()
    socks5Proxy.close()
    !logger && serviceLogger.close()
  }

  return {cleanupFunction, port}
}

// Run the service automatically only when file is called directly (`npm start` or `node run-execution-grid-tunnel-server`)
// It allows to test the main function
if (require.main === module) {
  startEgTunnelService().catch(async (err) => {
    try {
      heartbeatIntervalId && clearInterval(heartbeatIntervalId)
      deleteOldFilesInterval && clearInterval(deleteOldFilesInterval)
      console.error(`Webserver crashed: ${err.stack || err.toString()}`)
      serviceLogger.error(`Webserver crashed: ${err.stack || err.toString()}`)
    } finally {
      process.exit(1)
    }
  })

  nodeCleanup(cleanupFunction)
}

module.exports = {startEgTunnelService, prepareEnvironment}
