/**
 * Command line module for creating folder with all static assets (models, modules) available for client using
 * `clientRequire` and `models` endpoints. Such folder can be served by nginx as a static folder.
 * This greatly reduce a UnityBase application logs size and speed up static files loading (about 1ms per file)
 * Command line usage:

 ubcli linkStatic -?

 * @author pavel.mash 2019-11-15
 * @module wwwStaticFolder
 * @memberOf module:@unitybase/ubcli
 */
const fs = require('fs')
const argv = require('@unitybase/base').argv
const options = require('@unitybase/base').options
const path = require('path')

const DEBUG = false
const WINDOWS = (process.platform === 'win32')
const COMMENT = WINDOWS ? 'REM' : '#'

module.exports = linkStatic
module.exports.shortDoc = 'Create directory with static assets'

function linkStatic (cfg) {
  console.time('Generate static content directory')
  if (!cfg) {
    const opts = options.describe('linkStatic',
      `Create folder with a static assets, which can be used by nginx
as drop-in replacement to /clientRequire and /models endpoints`,
      'ubcli'
    )
      .add({
        short: 't',
        long: 'target',
        param: 'target',
        defaultValue: '*',
        help: 'Target folder. Default is "inetPub" value from config'
      })
      .add({
        short: 'run',
        long: 'run',
        defaultValue: false,
        help: 'Execute a bash/cmd script after creation'
      })
    cfg = opts.parseVerbose({}, true)
    if (!cfg) return
  }
  const cfgFN = argv.getConfigFileName()
  const ubCfg = argv.getServerConfiguration()
  let target = cfg.target || ubCfg.httpServer.inetPub

  if (!target || (typeof target !== 'string')) {
    throw new Error('Target folder is not specified. Either set a "http.inetPub" value in config or pass switch --target path/to/target/folder')
  }
  if (!path.isAbsolute(target)) target = path.join(process.cwd(), target)

  const CLIENT_REQUIRE_TARGET_ALIAS = WINDOWS ? '%CRT%' : '$CRT'
  const NODE_MODULES_SOURCES_ALIAS = WINDOWS ? '%NMS%' : '$NMS'

  const domainModels = ubCfg.application.domain.models
  const realCfgPath = path.dirname(fs.realpathSync(cfgFN)) // config can be a symlink from /opt/unitybase/products
  const modulesPath = path.join(realCfgPath, 'node_modules')
  if (!fs.existsSync(modulesPath)) {
    throw new Error(`node_modules folder not found in the folder with app config. Expected "${modulesPath}". May be you miss "npm i" command?`)
  }
  const tm = fs.readdirSync(modulesPath)
  const commands = []
  const clientRequireTarget = path.join(target, 'clientRequire')
  commands.push({
    type: 'comment',
    text: 'Modules for /clientRequire endpoint replacement'
  })

  for (const m of tm) {
    if (m.startsWith('.')) continue
    if (m.startsWith('@')) { // namespace
      const ttm = fs.readdirSync(path.join(modulesPath, m))
      commands.push({
        type: 'mkdir',
        to: path.join(CLIENT_REQUIRE_TARGET_ALIAS, m)
      })
      const L = commands.length
      for (const sm of ttm) {
        if (!sm.startsWith('.')) {
          tryAddModule(modulesPath, NODE_MODULES_SOURCES_ALIAS, path.join(m, sm), commands, CLIENT_REQUIRE_TARGET_ALIAS)
        }
      }
      if (commands.length === L) {
        // no modules added - remove ns folder creation
        commands.pop()
      }
    } else {
      tryAddModule(modulesPath, NODE_MODULES_SOURCES_ALIAS, m, commands, CLIENT_REQUIRE_TARGET_ALIAS)
    }
  }

  // process models. In case model is already sym-linked for clientRequire - use a related link
  // TODO - This allow to copy full folder to remote fs
  // const modelsTarget = path.join(clientRequireTarget, 'models')
  DEBUG && console.log(domainModels)
  commands.push({
    type: 'comment',
    text: 'Models for /model endpoint replacement'
  })

  for (const m of domainModels) {
    const packageJsonFn = path.join(m.realPath, 'package.json')
    const packageJSON = require(packageJsonFn)
    if (!packageJSON.config || !packageJSON.config.ubmodel || !packageJSON.config.ubmodel.name) {
      throw new Error(`package.json config for model ${m.name} should contains a section "config": {"ubmodel": {"name":...}`)
    }

    m.realPublicPath = packageJSON.config.ubmodel.isPublic
      ? m.realPath
      : path.join(m.realPath, 'public')
    m.packageJSON = packageJSON

    let rpp = m.realPublicPath
    if (rpp.endsWith('/') || rpp.endsWith('\\')) rpp = rpp.slice(0, -1)
    if (!fs.existsSync(rpp)) { // no public folder
      DEBUG && console.info(`Skip model ${m.Name} - no public folder ${rpp}`)
      continue
    }
    const moduleLink = commands.find(c => c.from === rpp)
    if (moduleLink) {
      commands.push({
        from: moduleLink.to,
        to: path.join(CLIENT_REQUIRE_TARGET_ALIAS, 'models', m.packageJSON.config.ubmodel.name),
        type: 'folder'
      })
    } else {
      commands.push({
        from: rpp,
        to: path.join(CLIENT_REQUIRE_TARGET_ALIAS, 'models', m.packageJSON.config.ubmodel.name),
        type: 'folder'
      })
    }
  }

  let script

  if (WINDOWS) {
    script = [
      '@ECHO OFF',
      `SET CRT=${clientRequireTarget}`,
      `SET NMS=${modulesPath}`,
      'RMDIR %CRT% /s /q', // prevent recursive symlinks
      'MKDIR %CRT%\\models'
    ]
  } else {
    script = [
      'err() { echo "err"; exit $?; }',
      `CRT=${clientRequireTarget}`,
      `NMS=${modulesPath}`,
      'rm -rf $CRT', // prevent recursive symlinks
      'mkdir -p $CRT',
      'mkdir -p $CRT/models'
    ]
  }

  for (const cmd of commands) {
    if (WINDOWS) {
      if (cmd.type === 'comment') {
        script.push(`REM ${cmd.text}`)
      } else if (cmd.type === 'mkdir') {
        script.push(`MKDIR ${cmd.to}`)
      } else if (cmd.type === 'folder') {
        script.push(`MKLINK /J /D ${cmd.to} ${cmd.from} || goto err`)
      } else if (cmd.type === 'file') {
        script.push(`if not exist ${cmd.to} MKLINK /H ${cmd.to} ${cmd.from} || goto err`)
      }
    } else {
      if (cmd.type === 'comment') {
        script.push(`# ${cmd.text}`)
      } else if (cmd.type === 'mkdir') {
        script.push(`mkdir -p ${cmd.to} || err`)
      } else if (cmd.type === 'folder') {
        script.push(`ln -s ${cmd.from} ${cmd.to} || err`)
      } else if (cmd.type === 'file') {
        script.push(`ln -s -f ${cmd.from} ${cmd.to} || err`)
      }
    }
  }

  const favIconTarget = path.join(target, 'favicon.ico')
  let favIconSrc
  const m = domainModels.find(m => m.name === 'cust')
  // either use existed cust model favicon or copy one from inetpub or UB model into cust (first run)
  if (m) {
    const custFIPath = path.join(m.realPublicPath, 'favicon.ico')
    if (fs.existsSync(custFIPath)) { // favicon in cust model exists - use it
      favIconSrc = custFIPath
      script.push(`${COMMENT} use favicon from ${m.name}`)
    } if (fs.existsSync(favIconTarget)) { // no favicon in cust model, but exists in intepub - copy it into cust
      script.push(`${COMMENT} copy favicon from app inetpub into ${m.name}`)
      if (WINDOWS) {
        script.push(`copy ${favIconTarget} ${custFIPath}`)
        script.push(`del ${favIconTarget}`)
      } else {
        script.push(`cp ${favIconTarget} ${custFIPath}`)
        script.push(`rm ${favIconTarget}`)
      }
      favIconSrc = custFIPath
    } else { // neither in cust nor in inetpub - use default from UB model
      const ubModel = domainModels.find(m => m.name === 'UB')
      if (ubModel) {
        const ubFavIcon = path.join(ubModel.realPublicPath, 'img', 'UBLogo16.ico')
        script.push(`${COMMENT} no favicon.ico found in cust model - use default favicon`)
        if (WINDOWS) {
          script.push(`copy ${ubFavIcon} ${custFIPath}`)
        } else {
          script.push(`cp ${ubFavIcon} ${custFIPath}`)
        }
        favIconSrc = custFIPath
      }
    }
  }
  if (favIconSrc) {
    if (WINDOWS) {
      script.push(`MKLINK /H ${favIconTarget} ${favIconSrc}`)
    } else {
      script.push(`ln -sf ${favIconSrc} ${favIconTarget}`)
    }
  }

  script.push(`${COMMENT} update modification time for files in modules updated by npm`)
  if (WINDOWS) {
    script.push(`
if exist %NMS%\\@unitybase\\ub\\node_modules\\@unitybase (
  echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  echo Updating date for files in node_modules folder is skipped because symbolic links is detected between packages
  echo If you are on the development environment this is OK, if on PRODUCTION - REMOVE SYMBOLIC LINKS AND RERUN THIS SCRIPT
  echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
) else (
  forfiles /P %NMS% /S /D -01/01/1986 /C "cmd /c Copy /B @path+,,"
)  
goto :eof
:err
 EXIT 1
:eof
`)
  } else {
    script.push('find -L $CRT -type f -not -path "*/node_modules/*" -not -newermt \'1986-01-01\' -exec touch -m {} +')
  }

  // win
  // forfiles /P .\node_modules /S /D -01.01.1986 /C "cmd /c Copy /B @path+,,"
  const resFn = path.join(process.cwd(), `.linkStatic.${WINDOWS ? 'cmd' : 'sh'}`)
  fs.writeFileSync(resFn, script.join('\n'))
  console.log(`

${WINDOWS ? 'CMD' : 'Bash'} script ${resFn} is created
  Review a script, take care about target folder and package list.
  In case some package should not be exposed to client add a section
        "config": {"ubmodel": {} } into corresponding package.json.
  Use a command:
    ${WINDOWS ? '.\\.linkStatic.cmd' : 'chmod +x ./.linkStatic.sh && ./.linkStatic.sh'}
  to link a static`)

  // let pjsPath = path.join(cfgPath, 'package.json')
  // if (!fs.existsSync(pjsPath)) {
  //   throw new Error(`package.json not found in the folder with app config. Expected path "${pjsPath}"`)
  // }
  // let appPackage = require(pjsPath)
  // console.log(domainModels)
  // console.log(appPackage)

  /*
  How to prevent server-side logic to be exposed for client

  First let's explain what packages are exposed:
    - packages without `config.ubmodel` section and packaged with `config.ubmodel.isPublic: true` inside package.json
      are exposed as is (sym-linked into ${httpServer.inetPub}/clientRequire)
    - for packages with `config.ubmodel && !config.ubmodel.isPublic` only `public` folder content and package.json itself
      is sym-linked into ${httpServer.inetPub}/clientRequire. All other model folders are hidden from client

  So, to hide all package files from client add a "config" : {"ubmodel": {} } section into package.json

   */
}

/**
 * Check module should be exposed and if yes, add command to "to" array
 * @param {string} modulesPath node_modules root
 * @param {string} MPT alias for modulesPath
 * @param {string} module Name of module to check
 * @param {array<object>} commands
 * @param {string} target Target folder
 */
function tryAddModule (modulesPath, MPT, module, commands, target) {
  const pPath = path.join(modulesPath, module, 'package.json')
  if (!fs.existsSync(pPath)) return
  const p = require(pPath)

  const hasUbModel = p.config && p.config.ubmodel
  if (!hasUbModel || (hasUbModel && p.config.ubmodel.isPublic)) { // packages without `config.ubmodel` and public packages
    if (!hasUbModel) {
      DEBUG && console.info(`Add common module "${module}"`)
    } else {
      DEBUG && console.info(`Add public model  "${module}"`)
    }
    commands.push({
      type: 'folder',
      from: path.join(MPT, module),
      to: path.join(target, module)
    })
    if (!hasUbModel) { // add link to module entry point to use in `index .entryPoint.js` nginx directive
      const pkgEntryPoint = path.join(modulesPath, module, p.main || 'index.js')
      // Check only files. Entry point can be a folder as in https://github.com/tarruda/has:  "main": "./src"
      // In such cases second call to linkStatic creates a File system loop
      // In any case such modules can't be requires by systemjs? so better to exclude it at all
      if (fs.isFile(pkgEntryPoint)) {
        commands.push({
          type: 'file',
          from: path.join(MPT, module, p.main || 'index.js'),
          to: path.join(target, module, '.entryPoint.js')
        })
      } else {
        DEBUG && console.warn(`Entry point ${pkgEntryPoint} not exists (or points to a folder). Skip linking of .entryPoint.js`)
      }
    }
  } else { // packages with `public` folder
    const pubPath = path.join(modulesPath, module, 'public')
    if (!fs.existsSync(pubPath)) {
      DEBUG && console.log(`Skip server-side "${module}"`)
      return
    }
    DEBUG && console.info(`Add public part  "${module}/public"`)
    commands.push({
      type: 'mkdir',
      to: path.join(target, module)
    })
    commands.push({
      type: 'folder',
      from: path.join(MPT, module, 'public'),
      to: path.join(target, module, 'public')
    })
    commands.push({
      from: path.join(MPT, module, 'package.json'),
      to: path.join(target, module, 'package.json'),
      type: 'file'
    })
  }
}