_ = require 'lodash'
debug = require('debug')('nanocyte-configuration-generator')
NodeUuid = require 'node-uuid'
ChannelConfig = require './channel-config'

DEFAULT_REGISTRY_URL = 'https://s3-us-west-2.amazonaws.com/nanocyte-registry/latest/registry.json'
METRICS_DEVICE_ID = 'f952aacb-5156-4072-bcae-f830334376b1'

VIRTUAL_NODES =
  'engine-input':
    config: {}
    data: {}
  'engine-output':
    config: {}
    data: {}
  'engine-data':
    config: {}
    data: {}
  'engine-debug':
    config: {}
    data: {}
  'engine-pulse':
    config: {}
    data: {}
  'router':
    config: {}
    data: {}
  'engine-start':
    config: {}
    data: {}
  'engine-stop':
    config: {}
    data: {}
  'subscribe-devices':
    config: {}
    data: {}

class ConfigurationGenerator
  constructor: (options, dependencies={}) ->
    {@registryUrl, @meshbluJSON, @metricsDeviceId} = options
    @registryUrl ?= DEFAULT_REGISTRY_URL
    @metricsDeviceId ?= METRICS_DEVICE_ID

    {@request, @channelConfig} = dependencies
    @request ?= require 'request'
    @channelConfig ?= new ChannelConfig
      accessKeyId:     options.accessKeyId
      secretAccessKey: options.secretAccessKey

  configure: (options, callback=->) =>
    {flowData, flowToken, deploymentUuid} = options

    debug 'configuring flow...', flowData

    debug 'fetching registry'
    @channelConfig.fetch (error) =>
      return callback error if error?

      @_getNodeRegistry (error, nodeRegistry) =>
        return callback error if error?
        debug 'fetched registry', nodeRegistry

        flowMetricNode =
          id: @_generateFlowMetricId()
          category: 'flow-metrics'
          flowUuid: flowData.flowId
          deviceId: @metricsDeviceId
          deploymentUuid: deploymentUuid

        flowData.nodes ?= []
        flowData.nodes.push flowMetricNode

        flowNodes = _.indexBy flowData.nodes, 'id'

        flowConfig = _.mapValues flowNodes, (nodeConfig) =>
          nodeConfig.nanocyte ?= {}
          nodeConfig.nanocyte.nonce = @_generateNonce()

          config: nodeConfig
          data: {}

        flowConfig = _.assign flowConfig, _.cloneDeep(VIRTUAL_NODES)
        instanceMap = @_generateInstances flowData.links, flowConfig, nodeRegistry

        _.each instanceMap, (instanceConfig, instanceId) =>
          {config,data} = flowConfig[instanceConfig.nodeUuid]

          config = @_legacyConversion _.cloneDeep config # prevent accidental mutation
          config.templateOriginalMessage = instanceConfig.templateOriginalMessage
          getSetConfig = @_mutilateGetSetNodes uuid: flowData.flowId, token: flowToken, config

          channelApiMatch = @channelConfig.get config.type
          defaultConfig = {}
          defaultConfig.channelApiMatch = channelApiMatch if channelApiMatch?
          config = _.defaultsDeep defaultConfig, config, getSetConfig

          flowConfig[instanceId] = {config: config, data: data}

        links = @_buildLinks(flowData.links, instanceMap)
        flowConfig.router.config = links

        flowConfig['engine-data'].config  = @_buildNodeMap instanceMap
        flowConfig['engine-pulse'].config = @_buildNodeMap instanceMap
        flowConfig['engine-debug'].config = @_buildNodeMap instanceMap
        flowConfig['engine-input'].config = @_buildMeshblutoNodeMap flowConfig, instanceMap
        flowConfig['engine-output'].config = _.extend {}, @meshbluJSON, uuid: flowData.flowId, token: flowToken
        flowConfig['subscribe-devices'].config = @_getSubscribeDevices flowNodes

        flowStopConfig = _.cloneDeep flowConfig

        engineStopLinks = flowConfig['router']['config']['engine-stop']?.linkedTo
        engineStopLinks ?= []

        stopRouterConfig = _.pick flowConfig['router']['config'], 'engine-stop', 'engine-output', engineStopLinks...
        flowStopConfig['router']['config'] = stopRouterConfig

        callback null, flowConfig, flowStopConfig

  _buildNodeMap: (flowNodeMap) =>
    _.mapValues flowNodeMap, (flowNode) =>
      nodeId: flowNode.nodeUuid

  _buildMeshblutoNodeMap: (flowConfig, instanceMap) =>
    inputInstances = _.where instanceMap, linkedToInput: true

    nodeMap = {}
    _.each inputInstances, (instance) =>
      nodeConfig = flowConfig[instance.nodeUuid]
      nodeMap[nodeConfig.config.uuid] ?= []

      alias = nodeConfig.config.alias
      aNodeMap = nodeId: instance.nodeUuid
      aNodeMap.alias = alias if alias?

      nodeMap[nodeConfig.config.uuid].push aNodeMap

    return nodeMap

  _generateInstances: (links, flowNodes, nodeRegistry) =>
    instanceMap = {}
    _.each flowNodes, (nodeConfig, nodeUuid) =>
      config = nodeConfig.config ? {}
      nanocyteConfig = config.nanocyte ? {}

      type = config.category
      type = config.type.replace('operation:', '') if type == 'operation'
      nodeFromRegistry = nodeRegistry[type] ? {}

      composedOf = nodeFromRegistry.composedOf ? {}

      linkedToData = _.detect composedOf, (value, key) =>
        value.linkedToData == true

      transactionGroupId = @_generateTransactionGroupId() if linkedToData?

      _.each composedOf, (template, templateId) =>
        instanceId = @_generateInstanceId()
        composedConfig = _.cloneDeep template
        composedConfig.nodeUuid = nodeUuid
        composedConfig.templateId = templateId
        composedConfig.debug = config.debug
        composedConfig.transactionGroupId = transactionGroupId if linkedToData?

        instanceMap[instanceId] = composedConfig

    return instanceMap

  _getNodeRegistry: (callback) =>
    @request.get @registryUrl, json: true, (error, response, nodeRegistry) =>
      callback error, nodeRegistry

  _getSubscribeDevices: (flowConfig) =>
    devices = _.where flowConfig, category: 'device'
    return broadcast: _.pluck devices, 'uuid'

  _buildLinks: (links, instanceMap) =>
    debug 'building links with', links
    result = {}
    _.each instanceMap, (config, instanceId) =>
      nodeLinks = _.filter links, from: config.nodeUuid
      templateLinks = config.linkedTo
      linkedTo = []

      if config.linkedToInput
        result[config.nodeUuid] ?=
          type: 'engine-input'
          linkedTo: []
        result[config.nodeUuid].linkedTo.push instanceId

      if config.linkedFromStart
        result['engine-start'] ?=
          type: 'engine-start'
          linkedTo: []
        result['engine-start'].linkedTo.push instanceId

      if config.linkedFromStop
        result['engine-stop'] ?=
          type: 'engine-stop'
          linkedTo: []
        result['engine-stop'].linkedTo.push instanceId

      if config.linkedToNext
        linkUuids = _.pluck nodeLinks, 'to'
        _.each instanceMap, (data, key) =>
          if _.contains linkUuids, data.nodeUuid
            linkedTo.push key if data.linkedToPrev

      _.each config.linkedTo, (templateLinkId) =>
        _.each instanceMap, (data, key) =>
          if data.nodeUuid == config.nodeUuid && data.templateId == templateLinkId
            linkedTo.push key

      linkedTo.push 'engine-output' if config.linkedToOutput
      linkedTo.push 'engine-pulse' if config.linkedToNext || config.linkedToPulse || config.linkedToOutput
      linkedTo.push 'engine-data' if config.linkedToData
      linkedTo.push 'engine-debug' if config.debug

      result[instanceId] =
        type: config.type
        linkedTo: linkedTo
        linkedToNext: config.linkedToNext

      result[instanceId].transactionGroupId = config.transactionGroupId if config.transactionGroupId?

    result['engine-output'] =
      type: 'engine-output'
      linkedTo: []
    result['engine-debug'] =
      type: 'engine-debug'
      linkedTo: []
    result['engine-pulse'] =
      type: 'engine-pulse'
      linkedTo: []
    result['engine-data'] =
      type: 'engine-data'
      linkedTo: []

    debug 'router config is', result

    return result

  _legacyConversion: (config) =>
    if config.type == 'operation:debounce'
      config.timeout = config.interval
      delete config.interval
    if config.type == 'operation:throttle'
      config.repeat = config.interval
      delete config.interval
    if config.type == 'operation:delay'
      config.fireOnce = true
      config.noUnsubscribe = true

    return config

  _mutilateGetSetNodes: (options, template) =>
    return {} unless template.type == 'operation:get-key' || template.type == 'operation:set-key'

    {uuid, token} = options

    bearerToken = new Buffer("#{uuid}:#{token}").toString('base64')

    {host,protocol,port} = @meshbluJSON
    host ?= 'meshblu.octoblu.com:443'
    if host == 'meshblu-messages.octoblu.com:443'
      host = 'meshblu.octoblu.com:443'
    port ?= 443
    protocol ?= 'http'
    protocol = 'https' if parseInt(port) == 443

    config =
      bodyEncoding: 'json'
      url: "#{protocol}://#{host}/v2/devices/#{uuid}"
      method: 'GET'
      headerKeys: [
        'Content-Type'
        'Authorization'
      ]
      headerValues: [
        'application/json'
        "Bearer #{bearerToken}"
      ]

    if template.type == 'operation:set-key'
      config.method = 'PATCH'
      config.bodyKeys =  [ 'data.{{msg.key}}' ]
      config.bodyValues = [ '{{msg.value}}' ]

    return config

  _generateFlowMetricId: =>
    NodeUuid.v4()

  _generateInstanceId: =>
    NodeUuid.v4()

  _generateNonce: =>
    NodeUuid.v4()

  _generateTransactionGroupId: =>
    NodeUuid.v4()

module.exports = ConfigurationGenerator
