import findUp from 'findup-sync'
import get from 'lodash/get'
import mapKeys from 'lodash/mapKeys'
import defaults from 'lodash/defaults'
import isFunction from 'lodash/isFunction'
import compact from 'lodash/compact'
import uniq from 'lodash/uniq'
import isError from 'lodash/isError'
import { promisify } from 'bluebird'
import { join, basename } from 'path'
import { mixinPropertyUtils } from 'skypager-util'
import config from './config/index'
import filesystem from './file-systems/node'
import Helper from './helper'
import Project from './project'
import Cache from './cache'
import minimist from 'minimist'

const InstanceIdMap = {}
const ProjectsCache = {}
const { keys, getOwnPropertyDescriptors } = Object

const ARGV = minimist(process.argv)

export class Portfolio {
  static get Project() {
    return Project
  }

  static get Helper() {
    return Helper
  }

  get promisify() {
    return promisify
  }

  constructor (name = 'Skypager', options = {}) {
    if (typeof name === 'object') {
      options = name
      name = options.name || this.constructor.name
    }

    mixinPropertyUtils(this)

    this.hide('configuration', {})

    this.instanceId = `${name}-${ Math.floor(Date.now() / 1000)}`

    this.hide('cache', new Cache())
    this.hide('__project_cache', ProjectsCache[this.instanceId] = {})
    this.hide('moduleId', get(module, 'id', __filename))

    filesystem({
      host: this
    })

    if (!this.dirname) {
      this.dirname = join(__dirname, '..')
    }
  }

  get helpers() {
    return Helper.registry
  }

  get Project() {
    return this.get('options.Project', Project)
  }

  get Helper() {
    return this.get('options.Helper', Helper)
  }
  
  // TODO
  // I want to try and use the trick i learned from terse-webpack which uses
  // a registry of features and reducers to expose a composable / chainable
  // configuration functions that can be used to create presets as project templates
  configure (scope = 'portfolio', options = {}) {
    const current = this.configuration[scope]

    if (current) {
      return config({
        ...options,
        history: current.history,
      })
    }

    return this.configuration[scope] = config({
      ...options,
      history: [{scope}]
    })
  }

  createProject(cwd, options = {}) {
    if (typeof cwd === 'undefined') {
      throw new Error('Must pass a Skypager a directory to load')
    }

    defaults(options, {
      manifestFilename: 'package.json',
      manifestPath: join(cwd, 'package.json'),
      type: 'default',
      membershipKey: 'skypager',
    })

    if (!this.fsx.existsSync(options.manifestPath)) {
      throw('Can not load a Skypager project without a manifest file')
    }

    const manifest = this.fsx.readJsonSync(options.manifestPath)
    const manifestOptions = get(manifest, options.membershipKey, {})
    const ProjectType = this.Project 

    return ProjectType.load(cwd, {
      manifest,
      ...manifestOptions,
      ...options,
    }, {
      portfolio: this,
      framework: this.framework,
    })
  }

  /**
   * Load a project from a folder, checking the cache first to see if it exists.
   */
  load (cwd, options = {}, context = {}) {
    if (typeof cwd === 'undefined') {
      throw new Error('Must pass a Skypager a directory to load')
    }

    const instanceId = get(this.__project_cache, cwd)

    if (!options.fresh && instanceId && InstanceIdMap[instanceId]) {
      return InstanceIdMap[instanceId]
    }

    const project = this.createProject(cwd, options, {
      portfolio: this,
      framework: this.framework,
      ...context,
    })

    filesystem({
      host: project
    })

    this.__project_cache[cwd] = project.id

    Helper.attachAll(project)

    const passedThrough = InstanceIdMap[project.id] = this.projectDidLoad 
      ? passThroughLocalConfig(this.projectDidLoad(project))
      : passThroughLocalConfig(project) 

    return passedThrough
  }

  get framework() {
    return this.constructor
  }

  get allHelpers() {
    return Helper.allHelpers
  }
  
  get allProjects() {
    return this.projectCacheKeys
      .map(i => this.__project_cache[i])
      .map(instanceId => InstanceIdMap[instanceId])
  }

  get projectInstanceIds() {
    return keys(InstanceIdMap)
  }

  get projects() {
    return mapKeys(this.__project_cache, (project) => project.name)
  }

  get projectCacheKeys() {
    return keys(getOwnPropertyDescriptors(this.__project_cache))
  }

  get projectRoots() {
    return this.allProjects.map(
      p => p.cwd
    )
  }

  get requireCacheEntries () {
    const moduleIds = keys(require.cache)

    return this.projectRoots.map(rootPath => (
      moduleIds.filter(moduleId => moduleId.startsWith(rootPath))
    ))
  }

  get userHome() {
    return (process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME']) || process.cwd()
  }

  get isDevelopingLocally() {
    return this.dirname === process.cwd()
  }

  get isNodeModule() {
    return this.parentFolderName === 'node_modules'
  }

  get localPackage() {
    return findUp('package.json', {
      cwd: process.cwd()
    })
  }

  get parentPackage() {
    return this.parentPackagePath && this.fsx.readJsonSync(this.parentPackagePath)
  }

  get parentPackagePath() {
    return findUp('package.json', {
      cwd: this.parentFolder
    })
  }

  get parentFolder () {
    return join(this.dirname, '..')
  }

  get parentFolderName() {
    return basename(this.parentFolder)
  }

  get grandParentFolder() {
    return join(this.parentFolder, '..')
  }

  clearProjectCache() {
    this.requireCacheEntries.forEach(entry => delete(require.cache[entry]))

    this.projectCacheKeys.forEach(cacheKey => {
      const instanceId = this.projects[cacheKey]
      const target = InstanceIdMap[instanceId]

      if (target) {
        delete(InstanceIdMap[instanceId])
      }

      delete(this.projects[cacheKey])
    })
  }

  get instanceIdMap() {
    return InstanceIdMap
  }

  findProjectByInstanceId(instanceId) {
    return InstanceIdMap[instanceId]
  }

}

export default Portfolio

const FailureLog = (moduleId, stage, error) =>
`
  There was an error loading the project at:

    ${moduleId}

  It occured:

    ${ stage }
  
  The Error Message was:

    ${ error.message }

  The Stacktrace:

    ${ error.stack }
`

const passThroughLocalConfig = (project) => {
  const main = project.skypagerMain

  let localConfig = project.existsSync(project.resolve(main))
    ? attemptNormalRequire(project.resolve(main))
    : (project) => project

  if (isError(localConfig)) {
    console.log(FailureLog(main, 'While requiring project config file', localConfig))

    if(ARGV.fail || process.env.FAIL_FAST) {
      process.exit(1)      
    }    
  } else if (isFunction(localConfig.default)) {
    localConfig = localConfig.default    
  }

  if (isFunction(localConfig)) {
    try {
      localConfig(project)
    } catch(error) {
      console.log(FailureLog(main, 'While running config function', error))

      if(ARGV.fail || process.env.FAIL_FAST) {
        process.exit(1)      
      }    
    }
  }

  try {
    project.attachProjectTypes()
  } catch(error) {
    project.error('Error attaching project types', error)
  }

  try {
    project.enableFeatures()
  } catch(error) {
    project.error('Error enabling features', error)
  }

  return project
}

const attemptNormalRequire = (moduleId, retryAgain = true) => {
  try {
    return __non_webpack_require__(moduleId)
  } catch (error) {
    if (retryAgain && error.message.match(/Unexpected token/i)) {
      require('babel-register')
      return attemptNormalRequire(moduleId, false)
    }

    return error
  }
}
