import os from 'os'
import path from 'path'
import fs from 'fs/promises'
import { spawn, execSync } from 'child_process'
import { fileURLToPath, pathToFileURL } from 'url'

export default class Util {
  static combineStack(err, appendErr) {
    err.stack = err.stack + '\nFrom:\n' + appendErr.stack.split('\n').slice(1).join('\n') + '\n'
    return err
  }

  constructor(root_import_meta_url) {
    this._root_import_meta_url = root_import_meta_url
  }

  getPathFromRoot(add) {
    const __dirname = path.dirname(fileURLToPath(this._root_import_meta_url));
    return path.join(__dirname,'./', add)
  }

  getUrlFromRoot(add) {
    return path.join(this._root_import_meta_url,'../', add)
  }

  getExtension(filename) {
    let extension = path.extname(filename)
    if (filename.indexOf('.tar.') > 0) {
      return '.tar' + extension
    }
    return extension
  }

  getAppNames(config) {
    const validLevels = [
      'fatal',
      'error',
      'warn',
      'info',
      'debug',
      'trace',
    ]
    let out = []
    let keys = Object.keys(config)
    for (let key of keys) {
      if (typeof(config[key]) !== 'object' || config[key] == null)
        continue
      if (typeof(config[key].port) !== 'number' || !config[key].port)
        continue
      if (typeof(config[key].provider) !== 'string' || !config[key].provider)
        continue
      if (config[key].https != null && typeof(config[key].https) !== 'boolean')
        continue
      if (config[key].updateEvery != null && (typeof(config[key].updateEvery) !== 'number' || config[key].updateEvery < 0))
        continue
      if (config[key].startWaitUntilFail != null && (typeof(config[key].startWaitUntilFail) !== 'number' || config[key].startWaitUntilFail < 10))
        continue
      if (config[key].heartbeatTimeout != null && (typeof(config[key].heartbeatTimeout) !== 'number' || config[key].heartbeatTimeout < 10))
        continue
      if (config[key].heartbeatAttempts != null && (typeof(config[key].heartbeatAttempts) !== 'number' || config[key].heartbeatAttempts < 1))
        continue
      if (config[key].heartbeatAttemptsWait != null && (typeof(config[key].heartbeatAttemptsWait) !== 'number' || config[key].heartbeatAttemptsWait < 10))
        continue
      if (config[key].heartbeatPath != null && (typeof(config[key].heartbeatPath) !== 'string' || config[key].heartbeatPath[0] !== '/'))
        continue
      if (config[key].log != null) {
        if (!Array.isArray(config[key].log))
          continue
        let valid = true
        for (let stream of config[key].log) {
          if (!stream || typeof(stream) !== 'object' || Array.isArray(stream)) {
            valid = false
            break
          }
          if (typeof(stream.level) !== 'string' || !stream.level || !validLevels.includes(stream.level)) {
            valid = false
            break
          }
          if ((typeof(stream.path) !== 'string' || !stream.path) && stream.stream !== 'process.stdout') {
            valid = false
            break
          }
        }
        if (!valid)
          continue
      }
      if (config[key].cluster != null && (typeof(config[key].cluster) !== 'number' || config[key].cluster > 100 || config[key].cluster < 0))
        continue
      if (config[key].clusterWaitOnCrash != null && (typeof(config[key].clusterWaitOnCrash) !== 'number' || config[key].clusterWaitOnCrash < 10))
        continue

      out.push(key)
    }
    return out
  }

  get7zipExecutable() {
    const util = new Util(import.meta.url)
    if (process.platform === 'win32') {
      return util.getPathFromRoot('../bin/7zdec.exe')
    }
    return util.getPathFromRoot('../bin/7zdec')
  }

  getNpmExecutable() {
    if (process.platform === 'win32') {
      return 'npm.cmd'
    }
    return 'npm'
  }

  verifyConfig(config) {
    if (!config.name) throw new Error('name is missing in config.json')
    if (this.getAppNames(config).length === 0) throw new Error('no application was found in config')
    if (config.debugPort != null && (typeof(config.debugPort) !== 'number' || !config.debugPort)) throw new Error('debugPort in config not a valid number')
  }
  
  extractFile(file, stream = function() {}) {
    let program = this.get7zipExecutable()
    let args = ['x', file]
    if (file.indexOf('.tar.') > 0) {
      program = 'tar'
      args = ['xvf', file]
    }
    return this.runCommand(program, args, path.dirname(file), stream)
  }

  runCommand(command, options = [], folder = null, stream = function() {}) {
    
    let baseError = new Error('')

    return new Promise(function(res, rej) {
      let fullcommand = path.join(folder ? folder : '', command)
      if (command.indexOf('/') >= 0 || command.indexOf('\\') >= 0) {
        fullcommand = command
      }
      stream(`[Command] ${fullcommand} ${options.join(' ')}\n`)
      let processor = spawn(command, options, {
        shell: true,
        cwd: folder,
      })
      let timeOuter = setInterval(function() {
        try {
          processor.stdin.write('n\n')
        } catch {}
      }, 250)
      processor.stdout.on('data', function(data) {
        stream(data.toString().replace(/\r\n/g, '\n'))
      })
      processor.stderr.on('data', function(data) {
        stream(data.toString().replace(/\r\n/g, '\n'))
      })
      processor.stdin.on('error', function() {
        clearInterval(timeOuter)
      })
      processor.on('error', function(err) {
        clearInterval(timeOuter)
        baseError.message = err.message
        rej(baseError)
      })
      processor.on('exit', function (code) {
        clearInterval(timeOuter)
        if (code !== 0) {
          baseError.message = 'Program returned error code: ' + code
          return rej(baseError)
        }
        res(code)
      })
    })
  }

  runCommandBackground(command, options = [], folder = null, stream = function() {}) {
    let fullcommand = path.join(folder ? folder : '', command)
    if (command.indexOf('/') >= 0 || command.indexOf('\\') >= 0) {
      fullcommand = command
    }
    stream(`[Command] ${fullcommand} ${options.join(' ')}\n`)
    let processor = spawn(command, options, {
      shell: true,
      cwd: folder,
    })
    let timeOuter = setInterval(function() {
      try {
        processor.stdin.write('n\n')
      } catch {}
    }, 250)
    processor.stdout.on('data', function(data) {
      stream(data.toString().replace(/\r\n/g, '\n'))
    })
    processor.stderr.on('data', function(data) {
      stream(data.toString().replace(/\r\n/g, '\n'))
    })
    processor.stdin.on('error', function() {
      clearInterval(timeOuter)
    })
    processor.on('error', function(err) {
      clearInterval(timeOuter)
    })
    processor.on('exit', function (code) {
      clearInterval(timeOuter)
    })

    processor._kill = processor.kill
    processor.kill = function() {
      if(os.platform() === 'win32'){
        try {
          execSync('taskkill /pid ' + processor.pid + ' /T /F')
        } catch {}
      }else{
        processor._kill();  
      }
    }
    return processor
  }
}
