
module CORL
module Plugin
class Network < Nucleon.plugin_class(:nucleon, :base)

  init_plugin_collection(:add_node, :batch)

  #-----------------------------------------------------------------------------
  # Cloud plugin interface

  def normalize(reload)
    super

    logger.info("Initializing network: reloading? #{reload}")
    myself.config = CORL.configuration(Config.new(myself._export, {}, true, false).import({ :autosave => false, :create => false, :new => true })) unless reload

    config.delete(:directory) # TODO: Figure out what to do with this??

    unless reload
      @build = Build.new

      config_identities_glob  = File.join('config', 'identities', '*')
      vagrant_identities_path = File.join('config', 'identities', 'vagrant')

      ignore([
        'build',
        config_identities_glob,
        config_identities_glob + File::SEPARATOR,
        "!#{vagrant_identities_path}"
      ])
    end
  end

  #---

  def remove_plugin
    CORL.remove_plugin(config)

    each_plugin do |type, provider, name, plugin|
      CORL.remove_plugin(plugin)
    end
  end

  #-----------------------------------------------------------------------------
  # Checks

  def has_nodes?(provider = nil)
    node_config(provider).export.empty? ? false : true
  end

  #-----------------------------------------------------------------------------
  # Property accessors / modifiers

  plugin_collection :CORL, :node
  plugin_collection :CORL, :builder, :single_instance => true
  plugin_collection :CORL, :provisioner

  #---

  def hiera_var
    @hiera
  end

  def hiera_var=hiera
    @hiera = hiera
  end

  #---

  def hiera_override_dir
    File.join(directory, 'config')
  end

  #---

  def home
    extension_set(:home, ( ENV['USER'] == 'root' ? '/root' : ENV['HOME'] ))
  end

  #---

  def directory
    config.directory
  end

  #---

  def key_directory
    File.join(directory, 'keys')
  end

  #---

  def build_directory
    File.join(directory, 'build')
  end

  #---

  def key_cache_directory
    File.join(build_directory, 'keys')
  end

  #---

  def config_directory
    File.join(directory, 'config')
  end

  #---

  def cache
    config.cache
  end

  #---

  def ignore(files)
    config.ignore(files)
  end

  #---

  def remote(name)
    config.remote(name)
  end

  def set_remote(name, location)
    config.set_remote(name, location)
  end

  #---

  def node_password=password
    @node_password = password
  end

  def node_password
    @node_password
  end

  #---

  def node_groups
    groups = {}

    each_node_config do |provider, name, info|
      search_node(provider, name, :settings, [], :array).each do |group|
        group = group.to_sym
        groups[group] = [] unless groups.has_key?(group)
        groups[group] << { :provider => provider, :name => name }
      end
    end
    groups
  end

  #---

  def node_info(references, default_provider = nil)
    groups    = node_groups
    node_info = {}

    default_provider = Manager.connection.type_default(:node) if default_provider.nil?

    references.each do |reference|
      info = Plugin::Node.translate_reference(reference)
      info = { :provider => default_provider, :name => reference } unless info
      name = info[:name].to_sym

      # Check for group membership
      if groups.has_key?(name)
        groups[name].each do |member_info|
          provider = member_info[:provider].to_sym

          node_info[provider] = [] unless node_info.has_key?(provider)
          node_info[provider] << member_info[:name]
        end
      else
        # Not a group
        provider = info[:provider].to_sym

        if node_config.export.has_key?(provider)
          node_found = false

          each_node_config(provider) do |node_provider, node_name, node|
            if node_name == name
              node_info[node_provider] = [] unless node_info.has_key?(node_provider)
              node_info[node_provider] << node_name
              node_found = true
              break
            end
          end

          unless node_found
            # TODO:  Error or something?
          end
        end
      end
    end
    node_info
  end

  #---

  def node_lookup(public_ip, hostname, require_new = false)
    matches = {}

    each_node_config do |provider, name, info|
      # TODO: Abstract this out... (extension hook)
      if provider == :vagrant && fact(:vagrant_exists)
        matches[provider] = name if name.to_s == hostname.to_s
      else
        matches[provider] = name if info[:public_ip] == public_ip && name.to_s == hostname.to_s
      end
    end

    unless matches.empty?
      if matches.has_key?(:local) && matches.keys.length > 1
        matches.delete(:local) # No need for local if there is an actual provider
      end
      provider = matches.keys[0]
      return node(provider, matches[provider], require_new)
    end
    nil
  end

  #---

  def local_node(require_new = false)
    hostname   = lookup(:fqdn)
    ip_address = CORL.public_ip
    local_node = node_lookup(ip_address, hostname, require_new)

    if local_node.nil?
      name       = Util::Data.ensure_value(hostname, ip_address)
      local_node = CORL.node(name, extended_config(:local_node).import({ :meta => { :parent => myself }}), :local)
    else
      local_node.network = myself
      local_node.normalize(true)
      local_node.localize
    end

    local_node.password = node_password
    local_node
  end

  #---

  def nodes_by_reference(references, default_provider = nil, require_new = false)
    nodes = []

    node_info(references, default_provider).each do |provider, names|
      names.each do |name|
        nodes << node(provider, name, require_new)
      end
    end
    nodes
  end

  #---

  def test_node(provider, options = {})
    config = Config.ensure(options).import({ :meta => { :parent => myself } })
    CORL.node(:test, config.export, provider)
  end

  #---

  def build
    @build
  end

  #---

  def create_builder(name, provider, options = {})
    CORL.create_plugin(:CORL, :builder, provider, extended_config(name, options).import({ :meta => { :parent => myself }}))
  end

  #---

  def identity_builder(options = {})
    create_builder(:network_identity_builder, :identity, options)
  end

  def package_builder(name, options = {})
    create_builder(:network_package_builder, :package, options)
  end

  def project_builder(name, options = {})
    create_builder(:network_project_builder, :project, options)
  end

  #-----------------------------------------------------------------------------
  # Operations

  def load(options = {})
    config.load(options)
  end

  #---

  def save(options = {})
    config.save(options)
  end

  #---

  def attach_files(type, name, files, options = {})
    attach_config  = Config.ensure(options).import({ :type => :file })
    included_files = []
    files          = [ files ] unless files.is_a?(Array)

    files.each do |file|
      if file
        attached_file = config.attach(type, name, file, attach_config)
        included_files << attached_file unless attached_file.nil?
      end
    end
    included_files
  end

  #---

  def attach_data(type, name, data, options = {})
    attach_config = Config.ensure(options).import({ :type => :source })
    attached_data = nil

    if data.is_a?(String)
      attached_data = config.attach(type, name, data, attach_config)
    end
    attached_data
  end

  #---

  def delete_attachments(ids, options = {})
    config.delete_attachments(ids, options)
  end

  #---

  def add_node(provider, name, options = {})
    config = Config.ensure(options)

    keypair = config.get(:keypair, nil)
    return false unless keypair && keypair.is_a?(Util::SSH::Keypair)

    remote_name = config.delete(:remote, :edit)

    node_config = extended_config(:network_new_node, Config.new(Util::Data.clean({
      :settings     => [ "server" ] | array(config.delete(:groups, [])),
      :region       => config.delete(:region, nil),
      :machine_type => config.delete(:machine_type, nil),
      :public_ip    => config.delete(:public_ip, nil),
      :image        => config.delete(:image, nil),
      :user         => config.delete(:user, :root),
      :hostname     => name
    })))

    # Set node data
    node        = set_node(provider, name, node_config.export)
    hook_config = { :node => node, :remote => remote_name, :config => config }
    success     = true

    yield(:preprocess, hook_config) if block_given?

    if ! node.local? && node.attach_keys(keypair) && extension_check(:add_node, hook_config)
      node.keypair = keypair

      # Create new node / machine
      success = node.create do |op, data|
        block_given? ? yield("create_#{op}".to_sym, data) : data
      end

      remote_name = nil if remote_name && ! remote(remote_name)

      if success && node.save({ :remote => remote_name, :message => "Created machine #{name} on #{provider}" })
        success = init_node(node, config.defaults({ :bootstrap => true, :seed => true })) do |op, data|
          block_given? ? yield(op, data) : data
        end
      end
    end
    success
  end

  #---

  def init_node(node, options = {})
    config  = Config.ensure(options)
    success = true

    bootstrap_requested = config.has_key?(:bootstrap)
    bootstrap           = config.delete(:bootstrap, false)

    seed_requested      = config.has_key?(:seed)
    seed                = config.delete(:seed, false)
    seed_project        = config.get(:project_reference, nil)

    if seed_project.nil?
      project_provider = myself.config.project.plugin_provider
      project_remote   = myself.config.remote(:edit)
      seed_project     = "#{project_provider}:::#{project_remote}"
    end

    unless node.cache_setting(:initialized)
      bootstrap = true unless bootstrap_requested
      seed      = true unless seed_requested
    end

    provision = config.delete(:provision, true)

    if bootstrap
      # Bootstrap machine
      success = node.bootstrap(home, extended_config(:bootstrap, config)) do |op, data|
        block_given? ? yield("bootstrap_#{op}".to_sym, data) : data
      end
    end

    if success
      if seed
        save_config  = { :commit => true, :remote => remote_name, :push => true }

        if seed_project && remote_name
          # Reset project remote
          seed_info = Plugin::Project.translate_reference(seed_project)

          if seed_info
            seed_url    = seed_info[:url]
            seed_branch = seed_info[:revision] if seed_info[:revision]
          else
            seed_url = seed_project
          end
          set_remote(:origin, seed_url) if remote_name.to_sym == :edit
          set_remote(remote_name, seed_url)
          save_config[:pull] = false
        end

        # Save network changes (preliminary)
        success = node.save(extended_config(:node_save, save_config))

        if success && seed_project
          # Seed machine with remote project reference
          result = node.action(:node_seed, {
            :project_reference => seed_project,
            :project_branch    => seed_branch
          }) do |op, data|
            yield("seed_#{op}".to_sym, data)
          end
          success = result.status == code.success
        end
      end

      node.set_cache_setting(:initialized, true) if success

      if success && provision
        # Run configured provisioners on machine
        result = node.action(:node_provision, config) do |op, data|
          yield("provision_#{op}".to_sym, data)
        end
        success = result.status == code.success
      end

      # Update local network project
      success = load({ :remote => remote_name, :pull => true }) if success
    end
    success
  end

  #-----------------------------------------------------------------------------
  # Utilities

  def each_node_config(provider = nil)
    node_config.export.each do |node_provider, nodes|
      if provider.nil? || provider == node_provider
        nodes.each do |name, info|
          yield(node_provider, name, info)
        end
      end
    end
  end

  #---

  def batch(node_references, default_provider = nil, parallel = true, running = true, &code)
    node_references = array(node_references.clone)
    success         = true

    if has_nodes? && ! node_references.empty?
      # Execute action on selected nodes
      nodes = nodes_by_reference(node_references, default_provider)

      if CORL.parallel? && parallel
        values = []
        nodes.each do |node|
          node.password = node_password
          if ! running || node.running?
            values << Celluloid::Future.new(node, &code)
          end
        end
        values  = values.map { |future| future.value }
        success = false if values.include?(false)
      else
        nodes.each do |node|
          node.password = node_password
          if ! running || node.running?
            proc_success = code.call(node)
            if proc_success == false
              success = false
            end
          end
        end
      end
    end
    success
  end
end
end
end
