#    Copyright 2013 Mirantis, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

module Astute
  class CobblerManager
    def initialize(engine_attrs, reporter)
      raise "Settings for Cobbler must be set" if engine_attrs.blank?

      begin
        Astute.logger.info("Trying to instantiate cobbler engine: #{engine_attrs.inspect}")
        @engine = Astute::Provision::Cobbler.new(engine_attrs)
      rescue => e
        Astute.logger.error("Error occured during cobbler initializing")
        reporter.report({
                          'status' => 'error',
                          'error' => 'Cobbler can not be initialized',
                          'progress' => 100
                        })
        raise e
      end
    end

    def add_nodes(nodes)
      nodes.each do |node|
        cobbler_name = node['slave_name']
        begin
          Astute.logger.info("Adding #{cobbler_name} into cobbler")
          @engine.item_from_hash('system', cobbler_name, node, :item_preremove => true)
        rescue RuntimeError => e
          Astute.logger.error("Error occured while adding system #{cobbler_name} to cobbler")
          raise e
        end
      end
    ensure
      sync
    end

    def remove_nodes(nodes, retries=3, interval=2)
      nodes_to_remove = nodes.map {|node| node['slave_name']}.uniq
      Astute.logger.info("Total list of nodes to remove: #{nodes_to_remove.pretty_inspect}")
      retries.times do
        nodes_to_remove.select! do |name|
          unless @engine.system_exists?(name)
            Astute.logger.info("System is not in cobbler: #{name}")
            next
          else
            Astute.logger.info("Trying to remove system from cobbler: #{name}")
            @engine.remove_system(name)
          end
          @engine.system_exists?(name)
        end
        return if nodes_to_remove.empty?
        sleep(interval) if interval > 0
      end
    ensure
      Astute.logger.error("Cannot remove nodes from cobbler: #{nodes_to_remove.pretty_inspect}") if nodes_to_remove.present?
      sync
    end

    def reboot_nodes(nodes)
      splay = calculate_splay_between_nodes(nodes)
      nodes.inject({}) do |reboot_events, node|
        cobbler_name = node['slave_name']
        Astute.logger.debug("Trying to reboot node: #{cobbler_name}")

        #Sleep up to splay seconds before reboot for load balancing
        sleep splay
        reboot_events.merge(cobbler_name => @engine.power_reboot(cobbler_name))
      end
    end

    def check_reboot_nodes(reboot_events)
      begin
        Astute.logger.debug("Waiting for reboot to be complete: nodes: #{reboot_events.keys}")
        failed_nodes = []
        Timeout::timeout(Astute.config.reboot_timeout) do
          while not reboot_events.empty?
            reboot_events.each do |node_name, event_id|
              event_status = @engine.event_status(event_id)
              Astute.logger.debug("Reboot task status: node: #{node_name} status: #{event_status}")
              if event_status[2] =~ /^failed$/
                Astute.logger.error("Error occured while trying to reboot: #{node_name}")
                reboot_events.delete(node_name)
                failed_nodes << node_name
              elsif event_status[2] =~ /^complete$/
                Astute.logger.debug("Successfully rebooted: #{node_name}")
                reboot_events.delete(node_name)
              end
            end
            sleep(5)
          end
        end
      rescue Timeout::Error => e
        Astute.logger.debug("Reboot timeout: reboot tasks not completed for nodes #{reboot_events.keys}")
        raise e
      end
      failed_nodes
    end

    def edit_nodes(nodes, data)
      nodes.each do |node|
        cobbler_name = node['slave_name']
        begin
          Astute.logger.info("Changing cobbler system #{cobbler_name}")
          @engine.item_from_hash('system', cobbler_name, data, :item_preremove => false)
        rescue RuntimeError => e
          Astute.logger.error("Error occured while changing cobbler system #{cobbler_name}")
          raise e
        end
      end
    ensure
      sync
    end

    def netboot_nodes(nodes, state)
      nodes.each do |node|
        cobbler_name = node['slave_name']
        begin
          Astute.logger.info("Changing node netboot state #{cobbler_name}")
          @engine.netboot(cobbler_name, state)
        rescue RuntimeError => e
          Astute.logger.error("Error while changing node netboot state #{cobbler_name}")
          raise e
        end
      end
    ensure
      sync
    end

    def get_existent_nodes(nodes)
      existent_nodes = []
      nodes.each do |node|
        cobbler_name = node['slave_name']
        if @engine.system_exists?(cobbler_name)
          Astute.logger.info("Update #{cobbler_name}, node already exists in cobbler")
          existent_nodes << node
        end
      end
      existent_nodes
    end

    def get_mac_duplicate_names(nodes)
      mac_duplicate_names = []
      nodes.each do |node|
        Astute.logger.info("Trying to find MAC duplicates for node #{node['slave_name']}")
        if node['interfaces']
          node['interfaces'].each do |iname, ihash|
            if ihash['mac_address']
              Astute.logger.info("Trying to find system with MAC: #{ihash['mac_address']}")
              found_node = @engine.system_by_mac(ihash['mac_address'])
              mac_duplicate_names << found_node['name'] if found_node
            end
          end
        end
      end
      mac_duplicate_names.uniq
    end

    def sync
      Astute.logger.debug("Cobbler syncing")
      @engine.sync
    end

    private

    def calculate_splay_between_nodes(nodes)
      # For 20 nodes, 120 iops and 180 splay_factor splay will be 1.5749
      (nodes.size + 1)  / Astute.config.iops.to_f * Astute.config.splay_factor / nodes.size
    end

  end
end
