#
# Author:: Christine Draper (<christine_draper@thirdwaveinsights.com>)
# Copyright:: Copyright (c) 2014 ThirdWave Insights LLC
# License:: Apache License, Version 2.0
#
# 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.
#

require 'chef/data_bag'
require 'chef/node'
require 'chef/encrypted_data_bag_item'
require 'chef/environment'
require 'chef/knife/core/object_loader'

class Chef
  class Knife
    module TopologyHelper
      
      # load one or more topologies from file
      def load_topologies(topology_file_path)

        if ! topology_file_path.end_with?('.js', '.json')
          show_usage
          ui.fatal "TOPOLOGY_FILE must be a '.js' or '.json' file"
          exit(1)
        end

        topologies = loader.object_from_file(topology_file_path)
        topologies = [topologies] if !topologies.kind_of?(Array)
        
        topologies
      end
      
      # create the topology data bag
      def create_bag(bag_name)
        # check that the name is valid
        begin
          Chef::DataBag.validate_name!(bag_name)
        rescue Chef::Exceptions::InvalidDataBagName => e
          ui.fatal(e.message)
          exit(1)
        end

        # create the data bag
        begin
          data_bag = Chef::DataBag.new
          data_bag.name(bag_name)
          data_bag.create
          ui.info("Created topology data bag [#{bag_name}]")
        rescue Net::HTTPServerException => e
          raise unless e.to_s =~ /^409/
          data_bag  = Chef::DataBag.load(bag_name)
          ui.info("Topology data bag #{bag_name} already exists")
        end

        data_bag
      end

        # make sure the chef environment exists
      def check_chef_env(chef_env_name)

        if chef_env_name
          begin
            chef_env = Chef::Environment.load(chef_env_name)
          rescue Net::HTTPServerException => e
            raise unless e.to_s =~ /^404/
            ui.info "Creating chef environment " + chef_env_name
            chef_env = Chef::Environment.new()
            chef_env.name(chef_env_name)
            chef_env.create
          end
        end

        chef_env
      end
      
      # recursive merge that retains all keys
      def prop_merge!(hash, other_hash)
        other_hash.each do |key, val|
          if val.kind_of?(Hash) && hash[key]
            prop_merge!(hash[key], val)
          else
            hash[key] = val
          end
        end
        
        hash
      end   

      # Merges topology properties into nodes, returning the merged nodes
      def merge_topo_properties(nodes, topo_hash)

        if nodes && nodes.length > 0
          merged_nodes = nodes ? nodes.clone : []
          merged_nodes.each do |nodeprops|

            normal_defaults = topo_hash['normal'] ? topo_hash['normal'].clone : {}
            nodeprops['normal'] ||= {}
            nodeprops['normal'] = prop_merge!(normal_defaults, nodeprops['normal'])

            nodeprops['chef_environment'] ||=  topo_hash['chef_environment'] if topo_hash['chef_environment']

            # merge in the topology tags
            nodeprops['tags'] ||= []
            nodeprops['tags'] |= topo_hash['tags'] if topo_hash['tags'] && topo_hash['tags'].length > 0
 
          end
        end

        merged_nodes

      end

      
    # Update an existing node
    def update_node(node_updates)
    
      config[:disable_editing] = true
    
      node_name = node_updates['name']
      begin
        
        # load then update and save the node
        node = Chef::Node.load(node_name)
        
        if node_updates['chef_environment'] && node_updates['chef_environment'] != node['chef_environment']
          check_chef_env(node_updates['chef_environment']) 
        end
        
        if updated_values = update_node_with_values(node, node_updates)
          ui.info "Updating #{updated_values.join(', ')} on node #{node.name}"
          node.save
          ui.output(format_for_display(node)) if config[:print_after]
        else
          ui.info "No  updates found for node #{node.name}"
        end

      rescue Net::HTTPServerException => e
        raise unless e.to_s =~ /^404/
        # Node has not been created
      end
      
      return node
    end

      # Make updates into the original node, returning the list of updated properties.
      def update_node_with_values(node, updates)
        updated_properties = []
        
        # merge the normal attributes (but not tags)
        normal_updates = updates['normal'] || {}
        normal_updates.delete('tags')
        original_normal = node.normal.clone()
        prop_merge!(node.normal, normal_updates) 
        updated_properties << 'normal' if (original_normal != node.normal)

        # merge with existing runlist
        if updates['run_list']
          updated_run_list = RunList.new
          updates['run_list'].each { |e| updated_run_list << e }
          if (updated_run_list != node.run_list)
            updated_properties << 'run_list'
            node.run_list(*updated_run_list)
          end
        end

        # update chef env
        new_chef_environment = updates['chef_environment']
        if new_chef_environment && new_chef_environment != node.chef_environment
          updated_properties << 'chef_environment'
          node.chef_environment(new_chef_environment)
        end

        # merge tags
        orig_num_tags = node.tags.length
        updates['tags'] ||= [] # make sure tags are initialized
        node.tag(*updates['tags'])
        updated_properties << 'tags' if node.tags.length > orig_num_tags

        # return false if no updates, else return array of property names
        updated_properties.length > 0 && updated_properties
      end

      # Load a topology from local data bag item file
      def load_from_file(bag_name, topo_name)
        
        topo_file = File.join(Dir.pwd, "#{topologies_path}", bag_name, topo_name + '.json')
        return unless (loader.file_exists_and_is_readable?(topo_file))
 
        item_data = loader.object_from_file(topo_file)
        item_data = if use_encryption
          secret = read_secret
          Chef::EncryptedDataBagItem.encrypt_data_bag_item(item_data, secret)
        else
          item_data
        end
        item = Chef::DataBagItem.new
        item.data_bag(bag_name)
        item.raw_data = item_data
        item
      end

      # read in the topology bag item
      def load_from_server(bag_name, item_name = nil)
        begin
          if (item_name)
            item = Chef::DataBagItem.load(bag_name, item_name)
            item = Chef::EncryptedDataBagItem.new(item.raw_data, read_secret) if use_encryption
          else
            item = Chef::DataBag.load(bag_name)
          end
        rescue Net::HTTPServerException => e
          raise unless e.to_s =~ /^404/
        end
        item
      end

      # Replace existing run list in a node
      def set_run_list(node, entries)
        node.run_list.run_list_items.clear
        entries.each { |e| node.run_list << e }
      end

      # Name of the topology bag
      def topo_bag_name(name=nil)
        @topo_bag_name = name if (name)
        @topo_bag_name ||= "topologies"
      end

      # Path for the topologies data bags.
      # For now, use the standard data_bags path for our topologies bags
      def topologies_path
        @topologies_path ||= "data_bags"
      end

      # Loader to get data bag items from file
      def loader
        @loader ||= Knife::Core::ObjectLoader.new(DataBagItem, ui)
      end

      # Determine if the bag items are/should be encrypted on server
      # NOTE: This option isnt currently enabled
      def use_encryption
        if config[:secret] && config[:secret_file]
          ui.fatal("please specify only one of --secret, --secret-file")
          exit(1)
        end
        config[:secret] || config[:secret_file]
      end

      # Return the secret key to encrypt/decrypt data bag items
      def read_secret
        if config[:secret]
          config[:secret]
        else
          Chef::EncryptedDataBagItem.load_secret(config[:secret_file])
        end
      end
      
      # initialize args for another knife command
      def initialize_cmd_args(args, new_name_args)
        args = args.dup
        args.shift(2 + @name_args.length) 
        cmd_args  = new_name_args + args
      end

      # run another knife command
      def run_cmd(command_class, args)        
        command = command_class.new(args)
        command.config[:config_file] = config[:config_file]
        command.configure_chef
        command.run
        
        command
      end
      
      # upload cookbooks - will warn and continue if upload fails (e.g. may be frozen)
      def upload_cookbooks(args)      
        begin
          run_cmd(Chef::Knife::TopoCookbookUpload, args)
        rescue Exception => e
            raise if Chef::Config[:verbosity] == 2
        end        
      end
      
      def display_name (topo)
        topo['name'] + ((topo['version']) ? " version " + topo['version'] : "")
      end

    end
  end
end