#!/usr/bin/env node

/* eslint-disable no-console */

const exists = require('fs').existsSync
const Metalsmith = require('..')
const program = require('commander')
const { resolve, isAbsolute, dirname } = require('path')
const { isString, isObject } = require('../lib/helpers')

const color = {
  error: '\x1b[31m',
  warn: '\x1b[33m',
  info: '\x1b[36m',
  success: '\x1b[32m',
  log: '\x1b[0m'
}

/**
 * Usage.
 */

program
  .version(require('../package.json').version)
  .option('-c, --config <path>', 'configuration file location', 'metalsmith.json')

/**
 * Examples.
 */

program.on('--help', function () {
  console.log('  Examples:')
  console.log()
  console.log('    # build from metalsmith.json:')
  console.log('    $ metalsmith')
  console.log()
  console.log('    # build from lib/config.json:')
  console.log('    $ metalsmith --config lib/config.json')
  console.log()
})

/**
 * Parse.
 */

program.parse(process.argv)

/**
 * Config.
 */

const dir = process.cwd()
const config = program.config
const path = isAbsolute(config) ? config : resolve(dir, config)

// Important addition of 2.5.x. Given local plugins with a relative path are written with __dirname in mind,
// having a config-relative dir path makes sure the CLI runs properly
// when the command is executed from a subfolder or outside of the ms directory
const confRelativeDir = dirname(path)
if (!exists(path)) fatal(`could not find a ${config} configuration file.`)

let json
try {
  // requiring json is incompatible with ESM, however given the metalsmith CLI is not meant to be "imported" or used in an ESM flow,
  // it is ok to keep it here for now
  json = require(path)
} catch (e) {
  fatal(`it seems like ${config} is malformed.`)
}

/**
 * Metalsmith.
 */

const metalsmith = new Metalsmith(confRelativeDir)
if (json.source) metalsmith.source(json.source)
if (json.destination) metalsmith.destination(json.destination)
if (json.concurrency) metalsmith.concurrency(json.concurrency)
if (json.metadata) metalsmith.metadata(json.metadata)
if (json.clean != null) metalsmith.clean(json.clean)
if (json.frontmatter != null) metalsmith.frontmatter(json.frontmatter)
if (json.ignore != null) metalsmith.ignore(json.ignore)
if (isObject(json.env)) metalsmith.env(expandEnvVars(json.env, process.env))

// set a flag plugins can check to target CLI-specific behavior
metalsmith.env('CLI', true)

/**
 * Plugins.
 */

normalize(json.plugins).forEach(function (plugin) {
  for (const name in plugin) {
    const opts = plugin[name]
    let mod

    try {
      const local = resolve(confRelativeDir, name)
      const npm = resolve(confRelativeDir, 'node_modules', name)

      if (exists(local) || exists(`${local}.js`)) {
        mod = require(local)
      } else if (exists(npm)) {
        mod = require(npm)
      } else {
        mod = require(name)
      }
    } catch (e) {
      fatal(`failed to require plugin "${name}".`)
    }

    try {
      metalsmith.use(mod(opts))
    } catch (e) {
      fatal(`error using plugin "${name}"...`, `${e.message}\n\n${e.stack}`)
    }
  }
})

/**
 * Build.
 */

metalsmith.build(function (err) {
  if (err) fatal(err.message, err.stack)
  log('success', `successfully built to ${metalsmith.destination()}`)
})

/**
 * Log an error and then exit the process.
 *
 * @param {String} msg
 * @param {String} [stack]  Optional stack trace to print.
 */

function fatal(msg, stack) {
  log('error', msg)
  if (stack) {
    log('error', stack)
  }
  // eslint-disable-next-line no-process-exit
  process.exit(1)
}

function log(type, msg) {
  if (!msg) {
    msg = type
  }
  const fn = console[type] || console.log
  let args = [`Metalsmith · ${msg}`,'\x1b[0m']
  if (color[type]) args = [color[type], ...args]
  fn(...args)
}

/**
 * Normalize an `obj` of plugins.
 *
 * @param {Array or Object} obj
 * @return {Array}
 */

function normalize(obj) {
  if (obj instanceof Array) return obj
  const ret = []

  for (const key in obj) {
    const plugin = {}
    plugin[key] = obj[key]
    ret.push(plugin)
  }

  return ret
}

/**
 * Expand env var values in env with values in expansionSource
 * @param {Object} env 
 * @param {Object} expansionSource 
 * @returns {Object}
 */
function expandEnvVars(env, expansionSource) {
  Object.entries(env).forEach(([name, value]) => {
    if (isString(value) && value.startsWith('$')) {
      env[name] = expansionSource[value.slice(1)]
    }
  }, env)
  return env
}