const fs = require("fs")
const path = require("path")
const hapi = require("../hapi");
const readline = require("readline")
const standardEntity = require("../standardEntity");
const resourceEntity = require("../resourceEntity");
const config = require("../config")
const chalk = require("chalk")
const cliSpinners = require("cli-spinners")
const ora = require("ora")
const resourcesFactory = require('./resources')
const semver = require('semver')
const {zip, unzip} = require('./zip')

const ENTITY = "block"

const getUikitIds = (host, location, [uikitPalette, uikitTypography, uikitButton]) => {
  const {execSync} = require('child_process')
  const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
  const branchParts = branch.split('/')
  if (branchParts[0] === 'd' && (!uikitPalette || !uikitTypography || !uikitButton)) {
    execSync(`git checkout uikit/${branchParts[1]}/dev`)
    const {read: readUikitConfig} = config.host('uikit');
    const uikitConfig = readUikitConfig(host, location)
    if (uikitConfig) {
      !uikitPalette && (uikitPalette = uikitConfig.palette.id)
      !uikitTypography && (uikitTypography = uikitConfig.typography.id)
      !uikitButton && (uikitButton = uikitConfig.button.id)
    }
    execSync(`git checkout ${branch}`)
  }
  return [uikitPalette, uikitTypography, uikitButton]
}

// (() => {
//   const Module = require("module");
//   const ex = Module._nodeModulePaths
//   const node_modules = path.resolve("node_modules")
//   Module._nodeModulePaths = function () {
//     const result = ex.apply(Module, arguments)
//     return result.concat(result.indexOf(node_modules) === -1 ? node_modules : [])
//   }
// })();

const {read, write, unlink} = config.host(ENTITY);

// const getContent = opt => opt[0] == "@" ? fs.readFileSync(opt.slice(1), "utf8") : opt;

const build = async (api, {location = process.cwd(), id} ) => {
  const buildBlock = require("./build")
  const config = read(api.host, location)
  let view
  try {
    const {urlLoader, replaceResources} = await resourcesFactory(api, config, location, {blockId: id})
    view = await buildBlock(location, {urlLoader});
    view = replaceResources(view)
    return view
  } catch(error) {
    console.log(error)
    throw error
  }
}

const blockUpdate = async (api, {id, data, fromcurrent}) => {
  const {data: [obj]} = await api.block.list({query: {filter: {_id: id}, select: "__v"}});
  if(!obj) throw "Not Found";
  data = data && JSON.parse(data)
  if (fromcurrent) {
    data.view = await build(api, {id})
  }
  return await api.block.update({data, id, __v: obj.__v});
}

const commit = async (api, {categories, roles, name, wireframe, tags, website, uikitPalette, uikitTypography, uikitButton, location = process.cwd(), editorVersion}) => {
  const buildBlock = require("./build")
  const config = read(api.host, location)
  if(!config) throw "config is not found";
  const {id} = config;
  let view
  // const js = fs.readFileSync(path.resolve(location, entrypoint)).toString("utf8");
  // const css = style && fs.readFileSync(path.resolve(location, style)).toString("utf8") || "";
  const packageJson = require(path.resolve(location, 'package.json'))
  const {version} = packageJson

  !editorVersion && ({editorVersion} = packageJson)

  try {
    const {urlLoader, replaceResources} = await resourcesFactory(api, config, location)
    view = await buildBlock(location, {urlLoader});
    view = replaceResources(view)
  } catch(error) {
    if(error.errors && error.errors.length)
      process.stdout.write(error.errors.join("\n")+"\n")
    else throw error
    return
  }

  const data = {view};
  const prev = await api.block.get({id})
  if(!prev) throw "Block is not found"
  const __v = prev.data.__v

  let major, minor, patch, dbVersion
  if (prev.data.version) {
    major = prev.data.version.major
    minor = prev.data.version.minor
    patch = prev.data.version.patch
    dbVersion = `${major}.${minor}.${patch}`
  } else {
    dbVersion = version
  }
  let newVersion = semver.inc(version, 'patch')
  if(semver.lt(newVersion, dbVersion)) {
    newVersion = semver.inc(dbVersion, 'patch')
  }

  data.version = {}
  data.version.major = semver.major(newVersion)
  data.version.minor = semver.minor(newVersion)
  data.version.patch = semver.patch(newVersion)

  name && (data.name = name)
  wireframe != null && (data.wireframe = wireframe)
  categories && (data.categories = categories)
  roles && (data.roles = roles)
  tags && (data.tags = tags)
  website && (data.website = website)
  uikitPalette && (data.uikitPalette = uikitPalette)
  uikitTypography && (data.uikitTypography = uikitTypography)
  uikitButton && (data.uikitButton = uikitButton)
  editorVersion && (data.editorVersion = editorVersion)

  const response = await api.block.update({id, __v, data})
  packageJson.version = newVersion
  fs.writeFileSync(path.resolve(location, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n', 'utf8')
  return response.data
}

const init = async (api, {
  name, categories, roles, entrypoint, style, tags, wireframe, website,
  uikitPalette, uikitTypography, uikitButton, location = process.cwd(), wjson, editorVersion,
}) => {
  const buildBlock = require("./build")
  const config = read(api.host, location)
  if (config) throw "exists";
  if (wjson) {
    if(!fs.existsSync("init.json")) throw("Couldn\"t find init.json")
    const initData = JSON.parse(fs.readFileSync("init.json").toString("utf8"))
    // entrypoint = initData.entrypoint
    // style = initData.style
    categories = [initData.category]
    name = initData.name
  }
  // entrypoint = entrypoint.replace(/(^[^./])/, "./$1")
  // style = style.replace(/(^[^./])/, "./$1")
  let view;
  // const js = fs.readFileSync(path.resolve(location, entrypoint)).toString("utf8");
  // const css = (style && fs.readFileSync(path.resolve(location, style)).toString("utf8") || "");
  try {
    view = await buildBlock(location);
  } catch(error) {
    if(error.errors && error.errors.length)
      process.stdout.write(error.errors.join("\n")+"\n")
    else throw error
    return
  }

  [uikitPalette, uikitTypography, uikitButton] = getUikitIds(api.host, location, [uikitPalette, uikitTypography, uikitButton])

  const packageJson = require(path.resolve(location, 'package.json'))

  !editorVersion && ({editorVersion} = packageJson)

  const addData = {name, view, categories, tags, wireframe}
  roles && (addData.roles = roles)
  website && (addData.website = website);
  uikitPalette && (addData.uikitPalette = uikitPalette);
  uikitTypography && (addData.uikitTypography = uikitTypography);
  uikitButton && (addData.uikitButton = uikitButton);
  editorVersion && (addData.editorVersion = editorVersion);
  const {data} = await api.block.add({data: addData})
  const toSave = {id: data._id, entrypoint, style};
  write(api.host, toSave, location)
  await commit(api, {location})
  return data
}

const purge = async (api, {location = ""}) => {
  const config = read(api.host, location)
  if(!config) throw "config is not found";
  const {id} = config
  unlink(api.host, location)
  await api.block.unpublish({id})
  const response = await api.block.remove({id})
  return response.data
}

const doAction = action => async (api, {id, location = process.cwd()}) => {
  id = id || read(api.host, location).id;
  const {data} = await api.block[action]({id})
  return data
}

const publishAll = async (api, {location, blocks, action, path: innerPath = "", ...argv}) => {
  const isPurge = action === "purge"
  const actionsMap = {
    publish: ["commit", "publish"],
    reinit: ["purge", "init", "commit", "publish"],
    purge: ["purge"],
    init: ["init", "commit", "publish"],
    skip: [],
  }
  const handlersMap = {
    commit,
    init,
    purge,
    publish: doAction("publish")
  }
  const isValidAction = act => Object.keys(actionsMap).includes(act)

  if (action && !isValidAction(action)) throw (`${action} - is not valid action`)

  const blocksDir = path.resolve(location)
  if (fs.existsSync(blocksDir)) {
    const files = fs.readdirSync(location)
    console.log(chalk.red(`Starting magic on ${api.host}`))
    for (const file of files.filter(f => !blocks || blocks.includes(f))) {
      const componentPath = path.join(blocksDir, file, innerPath)
      const initFilePath = path.join(componentPath, "init.json")
      const configExists = fs.existsSync(path.join(componentPath, `${ENTITY}.${config.host2fname(api.host)}`))
      if (isPurge || fs.existsSync(initFilePath)) {
        let initData = {}
        try {
          initData = isPurge
            ? {}
            : JSON.parse(fs.readFileSync(initFilePath).toString("utf8"))
        } catch (error) {
          throw(`Bad json data in ${initFilePath}`)
        }
        if (!isPurge && ["name", "category", "entrypoint", "style"].some(key => !initData[key])) {
          throw (`${initFilePath} is not complete. {name, category, entrypoint, style} fields are required`)
        }
        const data = {
          ...argv,
          ...initData,
          categories: [initData.category],
          location: componentPath
        }
        let answer = null
        if (!action && configExists) {
          console.log(`\
Block "${chalk.underline.cyan(file)}" is allready exists. Possible actions:
    "skip" - proceed to next block
    "publish" - skip init but commit and publish
    "reinit" - purge then init, commit, publish
    "purge" - remove block from db and unlink config file or leave blank for "ski"
`)
          await new Promise((resolve) => {
            const rl = readline.createInterface({
              input: process.stdin,
              output: process.stdout
            })
            rl.question("\nWe should: ", answ => {
              answer = answ || "skip"
              resolve(rl.close())
            })
          })
          if (!isValidAction(answer)) throw ("U r dumb...")
        }
        let actionKey = action || answer || "init"
        actionKey === "publish" && !configExists && (actionKey = "init")
        const handlers = actionsMap[actionKey]
        handlers.length && console.log(`\nWorking on "${chalk.cyan(file)}"`)
        for (const handler of handlers) {
          if (handler !== "purge" || configExists) {
            const spinner = ora({text: chalk.yellow(handler), spinner: cliSpinners.monkey}).start()
            try {
              const {_id, removed, n} = await handlersMap[handler](api, data)
              let more = ""
              switch (handler) {
                case "init":
                  more = chalk.yellow(`id: ${_id}`)
                  break
                case "purge":
                  more = chalk.red(`${JSON.stringify(removed)}, n: ${n}`)
                  break
                default:
                  break
              }
              spinner.succeed(chalk.green(`${handler}ed ${more}`).replace(/eed\s/, "ed "))
            } catch (error) {
              spinner.fail(`${handler}: ${typeof error === "object" ? JSON.stringify(error) : error}`)
            }
          }
        }
      }
    }
    console.log(chalk.red("Magic finished!"))
  }
}

module.exports = yargs => yargs.command({
  command: ["block"],
  desc: "block support",
  builder: yargs => standardEntity("block", {update: false})(
    yargs.help("help").command({
      command: "init",
      desc: "init new block file",
      builder: yargs => yargs
        .option("name", {desc: "Block name", string: true})
        .option("wjson", {desc: "use init.json", alias: "j", boolean: true})
        .option("wireframe", {desc: "is wireframe", boolean: true, default: null})
        .option("categories", {alias: "c", desc: "Block categories", array: true})
        .option("roles", {alias: "r", desc: "Block roles", array: true})
        .option("tags", {desc: "Block tags", array: true})
        .option("entrypoint", {default: "index.js", desc: "block entry point", string: true})
        .option("style", {default: "style.css", desc: "block style", string: true})
        .option("website", {default: null, desc: "website", string: true})
        .option("uikitPalette", {default: null, desc: "uikitPalette", string: true})
        .option("uikitTypography", {default: null, desc: "uikitTypography", string: true})
        .option("uikitButton", {default: null, desc: "uikitButton", string: true})
        .option("editorVersion", {default: null, desc: "editorVersion", string: true})
        .check(argv => {
          console.log(argv)
          if (!argv.wjson && !argv.categories) {
            throw "name and at least one category should be passed"
          }
          return true
        }).usage(
          '$0 block init --name="My Block" -c header footer --entrypoint=./index.js --style=./style.css'
        ),
      handler: hapi(init),
    }).command({
      command: "commit",
      desc: "commit block changes",
      builder: yargs => yargs
        .option("content", {desc: "JSON --content=json", string: true})
        .option("name", {desc: "Name", string: true})
        .option("wireframe", {desc: "is wireframe", boolean: true, default: null})
        .option("categories", {desc: "Block categories", array: true})
        .option("roles", {desc: "Block roles", array: true})
        .option("tags", {desc: "Block tags", array: true})
        .option("website", {desc: "website", string: true})
        .option("uikitPalette", {desc: "uikitPalette", string: true})
        .option("uikitTypography", {desc: "uikitTypography", string: true})
        .option("uikitButton", {desc: "uikitButton", string: true})
        .option("editorVersion", {desc: "editorVersion", string: true}),
      handler: hapi(commit),
    }).command({
      command: "edit [id]",
      desc: "edit block",
      handler: hapi(require("./edit")),
    }).command({
      command: "build",
      desc: "build block view (internal)",
      handler: hapi(build),
    }).command({
      command: "update <id> <data>",
      aliases: ["up"],
      desc: "Update one by id",
      handler: hapi(blockUpdate),
      builder: yargs => yargs.option('fromcurrent', {boolean: true, desc: 'update block with current directory view(internal purpose)', default: false}),
    }).command({
      command: "purge",
      desc: "remove block from server and unlink block-file",
      handler: hapi(purge),
    }).command({
      command: "resource",
      desc: "Resource support",
      builder: resourceEntity("block"),
      handler() {yargs.showHelp()}
    }).command({
      command: ["picture", "pict"],
      desc: "Resource support for inited ",
      builder: resourceEntity("block", {ownerId: true}),
      handler() {yargs.showHelp()}
    }).command({
      command: "info",
      desc: "Block info",
      handler: hapi(doAction("get"))
    }).command({
      command: "publish [id]",
      desc: "Enable public usage",
      handler: hapi(doAction("publish"))
    }).command({
      command: "unpublish [id]",
      desc: "Disable public usage",
      handler: hapi(doAction("unpublish"))
    }).command({
      command: "publishAll <location> [options]",
      desc: "For each block in location do init, commit, publish actions",
      usage: "publishAll ../project/blocks -pt=component -a=publish -b=why cta about",
      handler: hapi(publishAll),
      builder: yargs => yargs
        .option("path", {desc: "Path to component inside block dir", string: true, alias: "pt"})
        .option("action", {
          desc: `\
publish|purge|reinit

Action to use on every block by default.

"publish" - init if not inited then commit and publish
"purge" - remove from db and unlink config file
"reinit" - purge then init, commit, publish
`,
          alias: "a",
          string: true
        })
        .option("blocks", {
          desc: "blocks... to pick, other blocks will be ignored (separate with space)",
          alias: "b",
          array: true,
        }),
    }).command({
      command: "zip [id]", desc: "zip block",
      builder: yargs => yargs.option("output", {alias: "o", desc: "output to file", string: true}),
      handler: hapi(zip)
    }).command({
      command: "unzip <filename>", desc: "unzip block.zip to new one",
      handler: hapi(unzip)
    }).command({
      command: "changeCategory <filterjson>", desc: "change categories",
      builder: yargs => yargs
        .option("remove", {alias: "r", desc: "categories to remove", array: true})
        .option("append", {alias: "a", desc: "categories to append", array: true}),
      handler: hapi((api, {filterjson, remove, append}) => api.block.changeCategory({
        query: {filter: JSON.parse(filterjson), remove, append}
      }))
    })
  ),
  handler() {yargs.showHelp()}
})
