require 'chef/provider/lwrp_base'
require 'chef/resource/chef_node'
require 'openssl'
require 'chef/provisioning/chef_provider_action_handler'

class Chef
class Provider
class Machine < Chef::Provider::LWRPBase
  provides :machine

  def action_handler
    @action_handler ||= Chef::Provisioning::ChefProviderActionHandler.new(self)
  end
  def action_handler=(value)
    @action_handler = value
  end

  use_inline_resources

  def whyrun_supported?
    true
  end

  action :allocate do
    if !new_driver
      raise "Driver not specified for machine #{machine_spec.name}"
    end
    if current_driver && current_driver.driver_url != new_driver.driver_url
      raise "Cannot move '#{machine_spec.name}' from #{current_driver.driver_url} to #{new_driver.driver_url}: machine moving is not supported.  Destroy and recreate."
    end
    new_driver.allocate_machine(action_handler, machine_spec, new_machine_options)
    machine_spec.from_image ||= new_resource.from_image if new_resource.from_image
    machine_spec.driver_url ||= new_driver.driver_url
    machine_spec.save(action_handler)
  end

  action :ready do
    action_allocate
    machine = current_driver.ready_machine(action_handler, machine_spec, current_machine_options)
    machine_spec.save(action_handler)
    machine
  end

  action :ready_only do
    machine = current_driver.ready_machine(action_handler, machine_spec, current_machine_options)
    machine_spec.save(action_handler)
    machine
  end

  action :setup do
    machine = action_ready
    begin
      machine.setup_convergence(action_handler)
      machine_spec.save(action_handler)
      upload_files(machine)
    ensure
      machine.disconnect
    end
  end

  action :converge do
    machine = action_ready
    begin
      machine.setup_convergence(action_handler)
      machine_spec.save(action_handler)
      upload_files(machine)
      # If we were asked to converge, or anything changed, or if a converge has never succeeded, converge.
      if new_resource.converge
        Chef::Log.info("Converging #{machine_spec.name} because 'converge true' is set ...")
        machine.converge(action_handler)
      elsif new_resource.converge.nil? && resource_updated?
        Chef::Log.info("Converging #{machine_spec.name} because the resource was updated ...")
        machine.converge(action_handler)
      elsif !machine_spec.node['automatic'] || machine_spec.node['automatic'].size == 0
        Chef::Log.info("Converging #{machine_spec.name} because it has never been converged (automatic attributes are empty) ...")
        machine.converge(action_handler)
      elsif new_resource.converge == false
        Chef::Log.debug("Not converging #{machine_spec.name} because 'converge false' is set.")
      end
    ensure
      machine.disconnect
    end
  end

  action :converge_only do
    if new_resource.converge == false
      Chef::Log.debug("Not converging #{machine_spec.name} because 'converge false' is set.")
    else
      machine = run_context.chef_provisioning.connect_to_machine(machine_spec, current_machine_options)
      begin
        machine.converge(action_handler)
      ensure
        machine.disconnect
      end
    end
  end

  action :stop do
    if current_driver
      current_driver.stop_machine(action_handler, machine_spec, current_machine_options)
    end
  end

  action :destroy do
    if current_driver
      current_driver.destroy_machine(action_handler, machine_spec, current_machine_options)
    end
    machine_spec.delete(action_handler)
  end

  attr_reader :machine_spec

  def new_driver
    run_context.chef_provisioning.driver_for(new_resource.driver)
  end

  def current_driver
    if machine_spec.driver_url
      run_context.chef_provisioning.driver_for(machine_spec.driver_url)
    end
  end

  def from_image_spec
    @from_image_spec ||= begin
      if new_resource.from_image
        chef_managed_entry_store.get!(:machine_image, new_resource.from_image)
      else
        nil
      end
    end
  end

  def new_machine_options
    machine_options(new_driver)
  end

  def current_machine_options
    machine_options(current_driver)
  end

  def machine_options(driver)
    configs = []

    configs << {
      :convergence_options =>
        [ :chef_server,
          :allow_overwrite_keys,
          :source_key, :source_key_path, :source_key_pass_phrase,
          :private_key_options,
          :ohai_hints,
          :public_key_path, :public_key_format,
          :admin, :validator,
          :chef_config
        ].inject({}) do |result, key|
          result[key] = new_resource.send(key)
          result
        end
    }

    # The current use case for this is adding a new attribute `aws_tags` to
    # the machine resource from the chef-provisioning-aws driver.  Because we
    # want that attribute to work for the recipe DSL, it needs to be added at the
    # Chef::Resource::Machine class level and not at an instance level.  Thus we
    # also need to pull the additional_machine_option_keys (:aws_tags) from the
    # Chef::Resource::Machine class level.  If you use two drivers (like AWS and
    # Azure) then all machine instances will still have the `aws_tags` attribute
    # DSL and will pass `:aws_tags` in on the machine_options.  They can simply
    # be ignored by the Azure driver.
    (self.class.additional_machine_option_keys || []).each do |k|
      configs << { k => new_resource.send(k)} if new_resource.send(k)
    end

    configs << { from_image: new_resource.from_image } if new_resource.from_image
    configs << new_resource.machine_options if new_resource.machine_options
    configs << driver.config[:machine_options] if driver.config[:machine_options]
    Cheffish::MergedConfig.new(*configs)
  end

  def self.additional_machine_option_keys
    @@additional_machine_option_keys ||= []
  end

  def load_current_resource
    if defined?(Chef::Provider::ChefNode)
      # Cheffish 1.x
      node_provider = Chef::Provider::ChefNode.new(new_resource, run_context)
    else
      # Cheffish 2.x
      node_provider = Chef::Resource::ChefNode.action_class.new(new_resource, run_context)
    end
    node_provider.load_current_resource
    json = node_provider.new_json
    json['normal']['chef_provisioning'] = node_provider.current_json['normal']['chef_provisioning']
    @machine_spec = chef_managed_entry_store.new_entry(:machine, new_resource.name, json)
  end

  def chef_managed_entry_store
    @chef_managed_entry_store ||= Provisioning.chef_managed_entry_store(new_resource.chef_server)
  end

  def self.upload_files(action_handler, machine, files)
    if files
      files.each_pair do |remote_file, local|
        if local.is_a?(Hash)
          if local[:local_path]
            machine.upload_file(action_handler, local[:local_path], remote_file)
          else
            machine.write_file(action_handler, remote_file, local[:content])
          end
        else
          machine.upload_file(action_handler, local, remote_file)
        end
      end
    end
  end

  private

  def upload_files(machine)
    Machine.upload_files(action_handler, machine, new_resource.files)
  end

end
end
end
