const fs = require('fs-extra')
const path = require('path')
const yaml = require('js-yaml')
const clone = require('clone')
const stringifyJS = require('javascript-stringify').stringify
// Connectors
const cwd = require('./cwd')
const plugins = require('./plugins')
const folders = require('./folders')
const prompts = require('./prompts')
// Utils
const { get, set, unset, loadModule } = require('@vue/cli-shared-utils')
const { log } = require('../util/logger')
const extendJSConfig = require('@vue/cli/lib/util/extendJSConfig')

const fileTypes = ['js', 'json', 'yaml']
let current = {}

function list (context) {
  return plugins.getApi(cwd.get()).configurations
}

function findOne (id, context) {
  return list(context).find(
    c => c.id === id
  )
}

function findFile (fileDescriptor, context) {
  if (fileDescriptor.package) {
    const pkg = folders.readPackage(cwd.get(), context)
    const data = pkg[fileDescriptor.package]
    if (data) {
      return { type: 'package', path: path.join(cwd.get(), 'package.json') }
    }
  }

  for (const type of fileTypes) {
    const files = fileDescriptor[type]
    if (files) {
      for (const file of files) {
        const resolvedFile = path.resolve(cwd.get(), file)
        if (fs.existsSync(resolvedFile)) {
          return { type, path: resolvedFile }
        }
      }
    }
  }
}

function getDefaultFile (fileDescriptor, context) {
  const keys = Object.keys(fileDescriptor)
  if (keys.length) {
    for (const key of keys) {
      if (key !== 'package') {
        const file = fileDescriptor[key][0]
        return {
          type: key,
          path: path.resolve(cwd.get(), file)
        }
      }
    }
  }
}

function readFile (config, fileDescriptor, context) {
  const file = findFile(fileDescriptor, context)
  let fileData = {}
  if (file) {
    if (file.type === 'package') {
      const pkg = folders.readPackage(cwd.get(), context)
      fileData = pkg[fileDescriptor.package]
    } else if (file.type === 'js') {
      fileData = loadModule(file.path, cwd.get(), true)
    } else {
      const rawContent = fs.readFileSync(file.path, { encoding: 'utf8' })
      if (file.type === 'json') {
        fileData = JSON.parse(rawContent)
      } else if (file.type === 'yaml') {
        fileData = yaml.load(rawContent)
      }
    }
  }
  return {
    file,
    fileData
  }
}

function readData (config, context) {
  const data = {}
  config.foundFiles = {}
  if (!config.files) return data
  for (const fileId in config.files) {
    const fileDescriptor = config.files[fileId]
    const { file, fileData } = readFile(config, fileDescriptor, context)
    config.foundFiles[fileId] = file
    data[fileId] = fileData
  }
  return data
}

function writeFile (config, fileId, data, changedFields, context) {
  const fileDescriptor = config.files[fileId]
  let file = findFile(fileDescriptor, context)

  if (!file) {
    file = getDefaultFile(fileDescriptor, context)
  }

  if (!file) return

  log('Config write', config.id, data, changedFields, file.path)
  fs.ensureFileSync(file.path)
  let rawContent
  if (file.type === 'package') {
    const pkg = folders.readPackage(cwd.get(), context)
    pkg[fileDescriptor.package] = data
    rawContent = JSON.stringify(pkg, null, 2)
  } else {
    if (file.type === 'json') {
      rawContent = JSON.stringify(data, null, 2)
    } else if (file.type === 'yaml') {
      rawContent = yaml.dump(data)
    } else if (file.type === 'js') {
      const source = fs.readFileSync(file.path, { encoding: 'utf8' })
      if (!source.trim()) {
        rawContent = `module.exports = ${stringifyJS(data, null, 2)}`
      } else {
        const changedData = changedFields.reduce((obj, field) => {
          obj[field] = data[field]
          return obj
        }, {})
        rawContent = extendJSConfig(changedData, source)
      }
    }
  }
  fs.writeFileSync(file.path, rawContent, { encoding: 'utf8' })
}

function writeData ({ config, data, changedFields }, context) {
  for (const fileId in data) {
    writeFile(config, fileId, data[fileId], changedFields[fileId], context)
  }
}

async function getPromptTabs (id, context) {
  const config = findOne(id, context)
  if (config) {
    const data = readData(config, context)
    log('Config read', config.id, data)
    current = {
      config,
      data
    }

    // API
    const onReadData = await config.onRead({
      cwd: cwd.get(),
      data
    })

    let tabs = onReadData.tabs
    if (!tabs) {
      tabs = [
        {
          id: '__default',
          label: 'Default',
          prompts: onReadData.prompts
        }
      ]
    }
    await prompts.reset()
    for (const tab of tabs) {
      tab.prompts = tab.prompts.map(data => prompts.add({
        ...data,
        tabId: tab.id
      }))
    }
    if (onReadData.answers) {
      await prompts.setAnswers(onReadData.answers)
    }
    await prompts.start()

    plugins.callHook({
      id: 'configRead',
      args: [{
        config,
        data,
        onReadData,
        tabs,
        cwd: cwd.get()
      }],
      file: cwd.get()
    }, context)

    return tabs
  }
  return []
}

async function save (id, context) {
  const config = findOne(id, context)
  if (config) {
    if (current.config === config) {
      const answers = prompts.getAnswers()
      const data = clone(current.data)
      const changedFields = {}
      const getChangedFields = fileId => changedFields[fileId] || (changedFields[fileId] = [])

      // API
      await config.onWrite({
        prompts: prompts.list(),
        answers,
        data: current.data,
        files: config.foundFiles,
        cwd: cwd.get(),
        api: {
          assignData: (fileId, newData) => {
            getChangedFields(fileId).push(...Object.keys(newData))
            Object.assign(data[fileId], newData)
          },
          setData: (fileId, newData) => {
            Object.keys(newData).forEach(key => {
              let field = key
              const dotIndex = key.indexOf('.')
              if (dotIndex !== -1) {
                field = key.substr(0, dotIndex)
              }
              getChangedFields(fileId).push(field)

              const value = newData[key]
              if (typeof value === 'undefined') {
                unset(data[fileId], key)
              } else {
                set(data[fileId], key, value)
              }
            })
          },
          getAnswer: async (id, mapper) => {
            const prompt = prompts.findOne(id)
            if (prompt) {
              const defaultValue = await prompts.getDefaultValue(prompt)
              if (defaultValue !== prompt.rawValue) {
                let value = get(answers, prompt.id)
                if (mapper) {
                  value = mapper(value)
                }
                return value
              }
            }
          }
        }
      })

      writeData({ config, data, changedFields }, context)

      plugins.callHook({
        id: 'configWrite',
        args: [{
          config,
          data,
          changedFields,
          cwd: cwd.get()
        }],
        file: cwd.get()
      }, context)

      current = {}
    }
  }
  return config
}

function cancel (id, context) {
  const config = findOne(id, context)
  if (config) {
    current = {}
  }
  return config
}

module.exports = {
  list,
  findOne,
  getPromptTabs,
  save,
  cancel
}
