/**
 * Command line script - cryptography operations (mostly for UA)
 * Usage (shut down server before usage):
 *  ubcli crypt --help
 *
 * @author pavel.mash
 * @module crypt
 * @memberOf module:@unitybase/ubcli
 */
const base = require('@unitybase/base')
const path = require('path')
const fs = require('fs')
const options = base.options
const argv = base.argv

let iitCrypto
let iitSettings

module.exports = function crypt () {
  if (options.switchIndex('?') !== -1 || options.switchIndex('help') !== -1 ||
    options.switchIndex('-help') !== -1) {
    showUsage()
    return
  }

  const serverConfig = argv.getServerConfiguration()
  try {
    iitCrypto = require('@ub-d/iit-crypto')
  } catch (e) {
    console.error('to use ubcli crypt module @ub-d/iit-crypto must be added into application')
    throw e
  }
  iitSettings = serverConfig.security.dstu && serverConfig.security.dstu.iit
  let iitSettingsPath = '/var/opt/unitybase/shared'
  if (iitSettings && iitSettings.librarySettings) {
    iitSettingsPath = iitSettings.librarySettings
  }
  iitCrypto.initForCommandLineUsage(iitSettingsPath) // NOTE - this required only for command line script. Inside server library init automatically
  const [, , , command, arg1, arg2] = process.argv
  const COMMANDS = {
    enumDev,
    sign,
    signAsic,
    verify,
    hash,
    split,
    combine,
    certParse
  }
  if (!COMMANDS[command]) {
    if (command) console.error(`Unknown command ${command}`)
    showUsage()
    return
  }
  COMMANDS[command](arg1, arg2)
}
module.exports.shortDoc = "Cryptography operations. Run 'ubcli crypt --help' for usage"

/**
 * Display usage
 */
function showUsage () {
  console.log(`
Cryptography operations, expects '@ub-d/iit-crypto' package to be installed

ubcli crypt [command] [args]

Commands:
  sign    fileName4Sign signFormat [-k privateKeyPath] [-p privateKeyPwd | -i] [-o signatureFn] [-kn nameOfAdditionalKey]
              Create detached signature for file. Can be combined to container using "ubcli crypto combine"
                - if "-kn nameOfAdditionalKey" specified - use additional key (one of dstu.iit.additionalKeys) to sign 
                - if "-k privateKeyPath" is not specified - use key from "ubConfig.security.dstu.iit.keyPath"
                - if "-p privateKeyPwd" is not specified - use password from "ubConfig.security.dstu.iit.password",
                - "-i" is specified - ask for password form stdin
                - "signFormat" can be one of "CAdES", "XAdES"
                - "-o signatureFn" is a result file name, if not specified - output result to fileName4Sign folder in fileName4Sign.[p7s|xml]
  signAsic  fileNames4Sign signFormat [-k privateKeyPath] [-p privateKeyPwd | -i] [-o signatureFn] [-kn nameOfAdditionalKey]
                Create ASiC-E-signFormat (ASiC-E-CAdES or ASiC-E-XAdES) container for file(s) (; separated)
                - if "-kn nameOfAdditionalKey" specified - use additional key (one of dstu.iit.additionalKeys) to sign
                - if "-k privateKeyPath" is not specified - use key from "ubConfig.security.dstu.iit.keyPath"
                - if "-p privateKeyPwd" is not specified - use password from "ubConfig.security.dstu.iit.password",
                - "-i" is specified - ask for password form stdin
                - "signFormat" can be one of "CAdES", "XAdES"
                - "-o signatureFn" is a result file name, if not specified - output result to first fileName4Sign folder in firstFileName4Sign.asice              
  verify  signFn [dataFn]
              Verify signature. If "dataFn" is not specified consider "signFn" is container with signature(s) and data 
  hash    fileName [algorithm] [-b] [-cert /part/to/signing.cer]
              Calculate hash of file. Default algorithm is GOST (GOST-34311).
                - possible algorithm values are "GOST","MD5","SHA1","SHA256","SHA384","SHA512","SHA3_256","SHA3_512"
                - default output id base64 encoded hash value; if "-b" specified - hexadecimal
                - if '-cert /part/to/signing.cer' is passed - hashing parameters will be obtained from certificate 
  split   containerFn [destFolder]
              Split container into signatures and optional data
  combine dataFn signatureFn [-ss secondarySignatureFn] [-forceAsicS] [-d destFolder] 
              Combine data with signature (and optionally 2'd signature)
                - type of result container is auto-detected, but can be forced to create ASiC-S (if possible)
                - second signature can be specified in -ss
                - if destination is not specified - will write result to current folder          
  enumDev     Enumerate available devices for private key operations
  certParse pathToCertOrFolder
              Parse certificate (or all certificates if path is folder) and output parsed JSON into stdout           
`)
}

/**
 * Display JSON with available key medias and devices
 */
function enumDev () {
  console.log('Available key medias: ')
  console.log(JSON.stringify(iitCrypto.getAllKeyMedia(), null, ' '))
}

/**
 *  Create detached signature for file. [-k privateKeyPath] [-p privateKeyPwd | -i] [-o signatureFn] [-kn additionalKeyName]
 *   - if "-k privateKeyPath" is not specified - use key from "ubConfig.security.dstu.iit.keyPath"
 *   - if "-p privateKeyPwd" is not specified - use password from "ubConfig.security.dstu.iit.password",
 *   - "-i" is specified - ask for password form stdin
 *   - "signFormat" can be one of "CAdES", "XAdES"
 *   - "-o signatureFn" is a result file name, if  not specified - output result to stdin
 *
 * @param {string} fileName4Sign
 * @param {string} signFormat one of "CAdES", "XAdES"
 */
function sign (fileName4Sign, signFormat) {
  if (!signFormat || signFormat.startsWith('-')) signFormat = 'CAdES'

  const keyName = options.switchValue('kn') || ''
  if (keyName) {
    const additionalKeysCfg = iitSettings.additionalKeys || []
    iitCrypto.initAdditionalKeys(additionalKeysCfg) // for cmd line init additional keys manually
  } else {
    let keyPath = options.switchValue('k')
    if (!keyPath) keyPath = iitSettings && iitSettings.keyPath
    if (!keyPath) throw new Error("Private key is not specified, either use '-kn KeyName' to specify additional key name fro config, or '-k keyPath' or define 'dstu.iit.keyPath' in ubConfig")

    let keyPwd
    if (options.switchIndex('i') !== -1) {
      console.write('Private key password:')
      keyPwd = console.readLn()
    } else {
      keyPwd = options.switchValue('p')
      if (!keyPwd) keyPwd = iitSettings && iitSettings.password
      if (!keyPwd) throw new Error('Private key password is not specified as -p parameter and not found in ubConfig. Use -i to ask for password from stdin')
    }
    iitCrypto.readPkFromFile(keyPath, keyPwd)
  }

  const fullFn = buildFullPath(fileName4Sign)
  let res
  if (signFormat === 'CAdES') {
    res = iitCrypto.sign(fullFn, { keyName })
  } else if (signFormat === 'XAdES') {
    throw new Error('not implemented XAdES sign')
  } else {
    throw new Error('Invalid sign format. Possible values are CAdES or XAdES')
  }
  let outFn = options.switchValue('o')
  if (outFn) {
    outFn = buildFullPath(outFn)
  } else {
    outFn = fullFn + iitCrypto.CONST.FILE_EXT[signFormat]
  }
  fs.writeFileSync(outFn, res)
  console.log('Signature is written to: ', outFn)
}

/**
 * Create ASiC-E-signFormat (ASiC-E-CAdES or ASiC-E-XAdES) container for file(s) (; separated).
 *   - if "-k privateKeyPath" is not specified - use key from "ubConfig.security.dstu.iit.keyPath"
 *   - if "-p privateKeyPwd" is not specified - use password from "ubConfig.security.dstu.iit.password",
 *   - "-i" is specified - ask for password form stdin
 *   - "signFormat" can be one of "CAdES", "XAdES"
 *   - "-o signatureFn" is a result file name, if not specified - output result to first fileName4Sign folder in firstFileName4Sign.asice
 *
 * @param {string} fileName4Sign
 * @param {string} signFormat one of "CAdES", "XAdES"
 */
function signAsic (fileName4Sign, signFormat) {
  if (!signFormat || signFormat.startsWith('-')) signFormat = 'CAdES'

  const keyName = options.switchValue('kn') || ''
  if (keyName) {
    const additionalKeysCfg = iitSettings.additionalKeys || []
    iitCrypto.initAdditionalKeys(additionalKeysCfg) // for cmd line init additional keys manually
  } else {
    let keyPath = options.switchValue('k')
    if (!keyPath) keyPath = iitSettings && iitSettings.keyPath
    if (!keyPath) throw new Error('Private key path is not specified as -k parameter and not found in ubConfig')

    let keyPwd
    if (options.switchIndex('i') !== -1) {
      console.write('Private key password:')
      keyPwd = console.readLn()
    } else {
      keyPwd = options.switchValue('p')
      if (!keyPwd) keyPwd = iitSettings && iitSettings.password
      if (!keyPwd) throw new Error('Private key password is not specified as -p parameter and not found in ubConfig. Use -i to ask for password from stdin')
    }
    iitCrypto.readPkFromFile(keyPath, keyPwd)
  }

  const files = fileName4Sign.split(';').map(fn => {
    return {
      dataOrDataPath: buildFullPath(fn)
    }
  })
  let outFn = options.switchValue('o')
  if (outFn) {
    outFn = buildFullPath(outFn)
  } else {
    outFn = files[0].dataOrDataPath + iitCrypto.CONST.FILE_EXT.ASiCE
  }
  const res = iitCrypto.signAsASiC(files, outFn, { keyName }) // result is output file name
  console.log('Signature is written to: ', res)
}

/**
 * Split any container (with one data file) into data file and signatures
 * @param {string} containerFn Container file name
 * @param {string }destFolder Destination folder for extraction
 */
function split (containerFn, destFolder) {
  if (!containerFn || !destFolder) {
    console.error('usage: ubcli split containerFn destFolder')
    return
  }
  const buf = fileAsBuf(containerFn)
  const res = iitCrypto.splitAnyContainer(buf)
  if (!res) {
    console.error("Can't split")
    return
  }
  // write data file
  const signsCnt = res.signatures.length
  console.info(`Container '${containerFn}' is ${res.containerType} with ${signsCnt} '${res.containerSignFormat}' signature ${signsCnt > 1 ? '(s)' : ''}`)
  const fullDest = ensueFolder(destFolder)

  // write signature(s)
  const dataFN = path.join(fullDest, res.dataFileName || 'data.bin')
  fs.writeFileSync(dataFN, res.data)
  let ext = iitCrypto.CONST.FILE_EXT[res.containerSignFormat]
  if ((res.containerType === iitCrypto.CONST.C_TYPES.ASiCE) && (res.containerSignFormat === iitCrypto.CONST.SIGN_FORMAT.CAdES)) {
    console.warn('Signature from ASiC-E CAdES extracted as UB specific zip file without data - this is not a signature from IIT POV')
    ext = '.zip'
  }
  res.signatures.forEach((s, i) => {
    const sNum = (i + 1).toString().padStart(3, '0')
    const fn = path.join(fullDest, `signature${sNum}${ext}`)
    fs.writeFileSync(fn, s)
  })
  console.log(`Data and signatures are written to ${fullDest}`)
}

/**
 * Combine data and signature(s) into one container
 * @param {string} dataFn
 * @param {string} signatureFn
 */
function combine (dataFn, signatureFn) {
  const signatures = []
  signatures.push(fileAsBuf(signatureFn))
  const secFn = options.switchValue('ss')
  if (secFn) {
    signatures.push(fileAsBuf(secFn))
  }
  const forceASiCS = options.switchIndex('forceAsicS') !== -1
  const destFolder = options.switchValue('d') || './'
  const { container, preferredResultName, containerType, containerSignFormat } = iitCrypto.combineAnyContainer({
    dataOrDataPath: buildFullPath(dataFn),
    signatures,
    forceASiCS
  })
  const fullDest = ensueFolder(destFolder)
  const fn = path.join(fullDest, preferredResultName)
  fs.writeFileSync(fn, container)
  console.log(`Container of type ${containerType} with ${containerSignFormat} signature(s) is written to ${fn}`)
}

/**
 *
 * @param {string} dataFn
 * @param {string} [algo]
 */
function hash (dataFn, algo) {
  if (!algo || algo.startsWith('-')) algo = 'GOST'
  const fullFn = buildFullPath(dataFn)
  const hexa = options.switchIndex('b') !== -1
  let res
  if (algo === 'GOST') {
    let hashBin
    const certPath = options.switchValue('cert')
    if (certPath) {
      const fullCertPath = buildFullPath(certPath)
      const certDataBin = fs.readFileSync(fullCertPath)
      hashBin = iitCrypto.ctxHash(fullFn, certDataBin)
    } else {
      hashBin = iitCrypto.ctxHash(fullFn)
    }
    res = Buffer.from(hashBin).toString(hexa ? 'hex' : 'base64')
  } else if (['MD5', 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'SHA3_256', 'SHA3_512'].includes(algo)) {
    // eslint-disable-next-line no-undef
    const hashHexa = nhashFile(fullFn, algo)
    res = hexa ? hashHexa : Buffer.from(hashHexa, 'hex').toString('base64')
  } else {
    throw new Error(`Invalid hash algorithm ${algo}`)
  }
  console.log(res)
}

/**
 * Verify signature
 * @param {string} signFn
 * @param {string} [dataFn]
 */
function verify (signFn, dataFn) {
  const fullSignFn = buildFullPath(signFn)
  const fullDataFn = dataFn ? buildFullPath(dataFn) : null
  const signBuf = fs.readFileSync(fullSignFn, { encoding: 'bin' })
  const res = iitCrypto.verify(signBuf, fullDataFn)
  console.log(JSON.stringify(res, null, ' '))
}

/**
 * Parse certificate(s)
 * @param {string} certOrFolderPath
 */
function certParse (certOrFolderPath) {
  const certOrFolderFullPath = buildFullPath(certOrFolderPath)
  const stat = fs.statSync(certOrFolderFullPath)
  if (!stat.isFile()) { // enum all *.cer in folder
    const files = fs.readdirSync(certOrFolderFullPath)
    files.forEach(f => {
      // if (f.endsWith('.cer'))
      console.log('\n=====', f, '=====')
      const certFn = path.join(certOrFolderFullPath, f)
      try {
        const certBuf = fs.readFileSync(certFn, { encoding: 'bin' })
        console.log(JSON.stringify(iitCrypto.parseCertificate(certBuf), null, ' '))
      } catch (e) {
        console.error(`File: ${f}, Error: ${e.message}`)
      }
    })
  } else {
    const certBuf = fs.readFileSync(certOrFolderFullPath, { encoding: 'bin' })
    console.log(JSON.stringify(iitCrypto.parseCertificate(certBuf), null, ' '))
  }
}

/**
 * For relative path transform it to absolute relative to cwd
 * @param {string} fn
 * @returns {string}
 */
function buildFullPath (fn) {
  return path.isAbsolute(fn)
    ? fn
    : path.join(process.cwd(), fn)
}
/**
 * return file as buffer or throw
 * @private
 * @param {string} fn
 * @returns {Buffer}
 */
function fileAsBuf (fn) {
  const fullFn = buildFullPath(fn)
  if (!fs.existsSync(fullFn)) {
    throw new Error(`Specified file '${fullFn}' dose not exists`)
  }
  return fs.readFileSync(fullFn, { encoding: 'bin' })
}

/**
 * Create absolute folder path and force to create it
 * @param {string} folderPath
 * @returns {string} absolute folder path
 */
function ensueFolder (folderPath) {
  const fullDest = path.isAbsolute(folderPath)
    ? folderPath
    : path.join(process.cwd(), folderPath)
  if (!fs.existsSync(fullDest)) {
    console.log(`Creating ${fullDest} folder`)
    fs.mkdirSync(fullDest)
  }
  return fullDest
}