const path = require('path')
const {fork} = require('child_process')
const {generatePromise} = require('../utils')

const TUNNEL_STATUS = require('./tunnel-status')
const SEND_MESSAGE_HEARTBEAT_INTERVAL = 3000

// This line is for pkg
require('./convert-frpc-output-to-tunnel-status.js')

function createTunnelController({
  tunnelId,
  tunnelConfig,
  configFileRootPath,
  binPath,
  socks5Proxy,
  portRange,
  logFilePath,
  reconnectTimeout = 30000,
  frpcProcessWrapperPath = path.join(__dirname, 'frpc-process-wrapper.js'),
}) {
  return new FrpcTunnelController({
    tunnelId,
    tunnelConfig,
    configFileRootPath,
    binPath,
    socks5Proxy,
    portRange,
    logFilePath,
    reconnectTimeout,
    frpcProcessWrapperPath,
  })
}

class FrpcTunnelController {
  constructor({
    tunnelId,
    tunnelConfig,
    configFileRootPath,
    binPath,
    socks5Proxy,
    portRange,
    logFilePath,
    frpcProcessWrapperPath,
    reconnectTimeout,
  }) {
    this._tunnelId = tunnelId
    this._tunnelConfig = tunnelConfig
    this._configFileRootPath = configFileRootPath
    this._logFilePath = logFilePath
    this._binPath = binPath
    this._socks5Proxy = socks5Proxy
    this._portRange = portRange
    this._onStatusChangedCallbacks = []
    this._status = TUNNEL_STATUS.OFF
    this._frpcProcessWrapperPath = frpcProcessWrapperPath
    this._reconnectTimeout = reconnectTimeout
    this._reconnectTimeoutId = undefined
  }
  get status() {
    return this._status
  }
  set status(newStatus) {
    if (this._status === newStatus) {
      return
    }

    this._status = newStatus
    this._onStatusChangedCallbacks.forEach((cb) => cb(this._tunnelId, newStatus))
  }
  get tunnelId() {
    return this._tunnelId
  }

  async start() {
    this.status = TUNNEL_STATUS.INIT

    const [promise, resolveFn, rejectFn] = generatePromise()

    const frpcProcess = fork(this._frpcProcessWrapperPath, {
      stdio: 'inherit',
      detached: false,
      windowsHide: true,
      env: {
        tunnelId: this._tunnelId,
        stringifyConfig: JSON.stringify(this._tunnelConfig),
        socks5ProxyPort: this._socks5Proxy && this._socks5Proxy.address().port,
        stringifyPortRange: JSON.stringify(this._portRange),
        configFileRootPath: this._configFileRootPath,
        logFilePath: this._logFilePath,
        binPath: this._binPath,
        heartbeatTimeout: SEND_MESSAGE_HEARTBEAT_INTERVAL * 7,
      },
    })
    this._frpcProcess = frpcProcess

    const resolveAfterConnectingSucceededOrFailed = ({status}) => {
      if (status === TUNNEL_STATUS.STOPPED) {
        this.status = TUNNEL_STATUS.ERROR
        rejectFn(`Frpc was stopped. look at ${this._logFilePath} to get more information`)
      }

      if (status === TUNNEL_STATUS.INIT_ERROR || status === TUNNEL_STATUS.INIT_TIMEOUT_ERROR) {
        this.status = status
        const message = status === TUNNEL_STATUS.INIT_ERROR ? 'Start error.' : 'Timeout error.'
        rejectFn(`${message} look at ${this._logFilePath} to get more information`)
        this.stop()
      }

      if (status === TUNNEL_STATUS.RUNNING) {
        this.status = TUNNEL_STATUS.RUNNING
        resolveFn()
      }
    }

    frpcProcess.once('message', resolveAfterConnectingSucceededOrFailed)

    await promise

    frpcProcess.on('message', ({status}) => {
      if (status === TUNNEL_STATUS.RUNNING) {
        this._reconnectTimeoutId && clearTimeout(this._reconnectTimeoutId)
        this._reconnectTimeoutId = undefined
      }

      if (!this._reconnectTimeoutId && status === TUNNEL_STATUS.RECONNECT) {
        this._reconnectTimeoutId = setTimeout(() => this.stop(), this._reconnectTimeout)
      }

      this.status = status
    })

    this._heartBitIntervalId = setInterval(() => {
      !frpcProcess.killed && frpcProcess.send({status: 'ok'})
    }, SEND_MESSAGE_HEARTBEAT_INTERVAL)

    frpcProcess.on('error', (err) => {
      console.log(err)
      clearInterval(this._heartBitIntervalId)
      this.status = TUNNEL_STATUS.ERROR
    })

    return promise
  }

  stop() {
    const [promise, resolveFn, _rejectFn] = generatePromise()

    if (this._heartBitIntervalId) {
      clearInterval(this._heartBitIntervalId)
      this._heartBitIntervalId = undefined
    }

    if (this._reconnectTimeoutId) {
      clearTimeout(this._reconnectTimeoutId)
      this._reconnectTimeoutId = undefined
    }

    if (this.status === TUNNEL_STATUS.STOPPED || this._frpcProcess.killed) {
      return
    }

    this.status = TUNNEL_STATUS.STOPPING

    if (this._frpcProcess.exitCode === null) {
      this._frpcProcess.once('exit', () => {
        // TODO: change the flow only this class set the status to TUNNEL_STATUS.STOPPED
        this.status = TUNNEL_STATUS.STOPPED
        resolveFn()
      })

      this._frpcProcess.kill('SIGTERM')
    } else {
      this.status = TUNNEL_STATUS.STOPPED
      resolveFn()
    }

    return promise
  }

  onStatusChanged(cb) {
    this._onStatusChangedCallbacks.push(cb)
  }
}

module.exports = createTunnelController
