module Hibachi
  # Interaction object with the Chef JSON. This class is used to write
  # to and read from the configuration on disk, which is also used by
  # Chef to manage machine configuration. It's called "Node" because
  # that's what Chef Server calls each of the servers it's deploying
  # your code to.
  #
  # All operations on this class are "hard", that is, they will actually
  # write data out to the config file.
  class Node
    include ActiveModel::Model
    include Enumerable

    attr_accessor :attributes, :file_path

    validates :file_path, presence: true

    validate :file_exists
    validate :has_cookbook_attributes

    # Derive config from file at given path.
    def self.find at_path=""
      node = new file_path: at_path
      node.valid?
      node
    end

    # Test if the specified file we're supposed to manipulate does in
    # fact exist.
    def exists?
      @exists ||= File.exists? file_path
    end
    alias present? exists?

    # Iterate through all attributes.
    def each
      attributes.each { |attr| yield attr }
    end

    delegate :empty?, :to => :attributes
    delegate :any?, :to => :attributes

    # Find the attribute at a given key.
    def [] key
      attributes[key]
    end

    # Set the attribute at a given key.
    def []= key, value
      merge! key => value
    end

    # Merge incoming Hash with the Chef JSON.
    def merge! with_new_attributes={}
      attributes.merge! with_new_attributes
      update!
    end

    # Delete an attribute from the Hash and write JSON.
    def delete id
      attributes.delete id
      update!
    end

    # Delete all attributes from this recipe.
    def delete!
      @attributes = {}
      update!
    end

    # Attributes as initially populated by the parsed JSON file. Scoped
    # by the global cookbook.
    def attributes
      @attributes ||= parsed_json_attributes[Hibachi.config.cookbook] || {}
    end

    delegate :any?, :to => :attributes
    delegate :empty?, :to => :attributes

    protected
    # All attributes as parsed from the Chef JSON.
    def parsed_json_attributes
      JSON.parse(chef_json).with_indifferent_access
    end

    private
    def chef_json
      @raw_json ||= File.read file_path
    end

    def update!
      File.write file_path, pretty_formatted_json
      true
    rescue StandardError => exception
      logger.error exception.message
      exception.backtrace.each { |line| logger.error line }
      false
    end

    def pretty_formatted_json
      JSON.pretty_generate Hibachi.config.cookbook => attributes
    end

    def file_exists
      errors.add :file, "'#{file_path}' does not exist" unless exists?
    end

    def has_cookbook_attributes
      errors.add :cookbook, "could not be parsed from JSON" unless any?
    end
  end
end