require 'puppet/node/environment'
require 'puppet/rails'
require 'puppet/rails/resource'
require 'puppet/rails/fact_name'
require 'puppet/rails/source_file'
require 'puppet/rails/benchmark'
require 'puppet/util/rails/collection_merger'

class Puppet::Rails::Host < ActiveRecord::Base
  include Puppet::Rails::Benchmark
  extend Puppet::Rails::Benchmark
  include Puppet::Util
  include Puppet::Util::CollectionMerger

  has_many :fact_values, :dependent => :destroy, :class_name => "Puppet::Rails::FactValue"
  has_many :fact_names, :through => :fact_values, :class_name => "Puppet::Rails::FactName"
  belongs_to :source_file
  has_many :resources, :dependent => :destroy, :class_name => "Puppet::Rails::Resource"

  def self.from_puppet(node)
    host = find_by_name(node.name) || new(:name => node.name)

    {"ipaddress" => "ip", "environment" => "environment"}.each do |myparam, itsparam|
      if value = node.send(myparam)
        host.send(itsparam + "=", value)
      end
    end

    host
  end

  # Override the setter for environment to force it to be a string, lest it
  # be YAML encoded.  See #4487.
  def environment=(value)
    super value.to_s
  end

  # returns a hash of fact_names.name => [ fact_values ] for this host.
  # Note that 'fact_values' is actually a list of the value instances, not
  # just actual values.
  def get_facts_hash
    fact_values = self.fact_values.find(:all, :include => :fact_name)
    return fact_values.inject({}) do | hash, value |
      hash[value.fact_name.name] ||= []
      hash[value.fact_name.name] << value
      hash
    end
  end


  # This is *very* similar to the merge_parameters method
  # of Puppet::Rails::Resource.
  def merge_facts(facts)
    db_facts = {}

    deletions = []
    self.fact_values.find(:all, :include => :fact_name).each do |value|
      deletions << value['id'] and next unless facts.include?(value['name'])
      # Now store them for later testing.
      db_facts[value['name']] ||= []
      db_facts[value['name']] << value
    end

    # Now get rid of any parameters whose value list is different.
    # This might be extra work in cases where an array has added or lost
    # a single value, but in the most common case (a single value has changed)
    # this makes sense.
    db_facts.each do |name, value_hashes|
      values = value_hashes.collect { |v| v['value'] }

      unless values == facts[name]
        value_hashes.each { |v| deletions << v['id'] }
      end
    end

    # Perform our deletions.
    Puppet::Rails::FactValue.delete(deletions) unless deletions.empty?

    # Lastly, add any new parameters.
    facts.each do |name, value|
      next if db_facts.include?(name)
      values = value.is_a?(Array) ? value : [value]

      values.each do |v|
        fact_values.build(:value => v, :fact_name => Puppet::Rails::FactName.find_or_create_by_name(name))
      end
    end
  end

  # Set our resources.
  def merge_resources(list)
    # keep only exported resources in thin_storeconfig mode
    list = list.select { |r| r.exported? } if Puppet.settings[:thin_storeconfigs]

    resources_by_id = nil
    debug_benchmark("Searched for resources") {
      resources_by_id = find_resources
    }

    debug_benchmark("Searched for resource params and tags") {
      find_resources_parameters_tags(resources_by_id)
    } if id

    debug_benchmark("Performed resource comparison") {
      compare_to_catalog(resources_by_id, list)
    }
  end

  def find_resources
    condition = { :exported => true } if Puppet.settings[:thin_storeconfigs]

    resources.find(:all, :include => :source_file, :conditions => condition || {}).inject({}) do | hash, resource |
      hash[resource.id] = resource
      hash
    end
  end

  def find_resources_parameters_tags(resources)
    find_resources_parameters(resources)
    find_resources_tags(resources)
  end

  def compare_to_catalog(existing, list)
    compiled = list.inject({}) do |hash, resource|
      hash[resource.ref] = resource
      hash
    end

    resources = nil
    debug_benchmark("Resource removal") {
      resources = remove_unneeded_resources(compiled, existing)
    }

    # Now for all resources in the catalog but not in the db, we're pretty easy.
    additions = nil
    debug_benchmark("Resource merger") {
      additions = perform_resource_merger(compiled, resources)
    }

    debug_benchmark("Resource addition") {
      additions.each do |resource|
        build_rails_resource_from_parser_resource(resource)
      end

      log_accumulated_marks "Added resources"
    }
  end

  def add_new_resources(additions)
    additions.each do |resource|
      Puppet::Rails::Resource.from_parser_resource(self, resource)
    end
  end

  # Turn a parser resource into a Rails resource.
  def build_rails_resource_from_parser_resource(resource)
    db_resource = nil
    accumulate_benchmark("Added resources", :initialization) {
      args = Puppet::Rails::Resource.rails_resource_initial_args(resource)

      db_resource = self.resources.build(args)

      # Our file= method does the name to id conversion.
      db_resource.file = resource.file
    }


    accumulate_benchmark("Added resources", :parameters) {
      resource.each do |param, value|
        Puppet::Rails::ParamValue.from_parser_param(param, value).each do |value_hash|
          db_resource.param_values.build(value_hash)
        end
      end
    }

    accumulate_benchmark("Added resources", :tags) {
      resource.tags.each { |tag| db_resource.add_resource_tag(tag) }
    }

    db_resource.save

    db_resource
  end


  def perform_resource_merger(compiled, resources)
    return compiled.values if resources.empty?

    # Now for all resources in the catalog but not in the db, we're pretty easy.
    additions = []
    compiled.each do |ref, resource|
      if db_resource = resources[ref]
        db_resource.merge_parser_resource(resource)
      else
        additions << resource
      end
    end
    log_accumulated_marks "Resource merger"

    additions
  end

  def remove_unneeded_resources(compiled, existing)
    deletions = []
    resources = {}
    existing.each do |id, resource|
      # it seems that it can happen (see bug #2010) some resources are duplicated in the
      # database (ie logically corrupted database), in which case we remove the extraneous
      # entries.
      if resources.include?(resource.ref)
        deletions << id
        next
      end

      # If the resource is in the db but not in the catalog, mark it
      # for removal.
      unless compiled.include?(resource.ref)
        deletions << id
        next
      end

      resources[resource.ref] = resource
    end
    # We need to use 'destroy' here, not 'delete', so that all
    # dependent objects get removed, too.
    Puppet::Rails::Resource.destroy(deletions) unless deletions.empty?

    resources
  end

  def find_resources_parameters(resources)
    params = Puppet::Rails::ParamValue.find_all_params_from_host(self)

    # assign each loaded parameters/tags to the resource it belongs to
    params.each do |param|
      resources[param['resource_id']].add_param_to_list(param) if resources.include?(param['resource_id'])
    end
  end

  def find_resources_tags(resources)
    tags = Puppet::Rails::ResourceTag.find_all_tags_from_host(self)

    tags.each do |tag|
      resources[tag['resource_id']].add_tag_to_list(tag) if resources.include?(tag['resource_id'])
    end
  end

  def to_puppet
    node = Puppet::Node.new(self.name)
    {"ip" => "ipaddress", "environment" => "environment"}.each do |myparam, itsparam|
      if value = send(myparam)
        node.send(itsparam + "=", value)
      end
    end

    node
  end
end
