# Author:: Kevin Moser <kevin.moser@nordstrom.com>
# Copyright:: Copyright 2013, Nordstrom, Inc.
# 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.

class ChefVault::Item < Chef::DataBagItem
  attr_accessor :keys
  attr_accessor :encrypted_data_bag_item

  def initialize(vault, name)
    super() # Don't pass parameters
    @data_bag = vault
    @raw_data["id"] = name
    @keys = ChefVault::ItemKeys.new(vault, "#{name}_keys")
    @secret = generate_secret
    @encrypted = false
  end

  def load_keys(vault, keys)
    @keys = ChefVault::ItemKeys.load(vault, keys)
    @secret = secret
  end

  def clients(search=nil, action=:add)
    if search
      results_returned = false

      query = Chef::Search::Query.new
      query.search(:node, search)[0].each do |node|
        results_returned = true

        case action
        when :add
          begin
            keys.add(ChefVault::ChefPatch::ApiClient.load(node.name), @secret, "clients")
          rescue Net::HTTPServerException => http_error
            if http_error.response.code == "404"
              raise ChefVault::Exceptions::ClientNotFound,
                    "#{node.name} is not a valid chef client and/or node"
            else
              raise http_error
            end
          end
        when :delete
          keys.delete(node.name, "clients")
        else
          raise ChefVault::Exceptions::KeysActionNotValid,
                "#{action} is not a valid action"
        end
      end

      unless results_returned
        puts "WARNING: No clients were returned from search, you may not have "\
             "got what you expected!!"
      end
    else
      keys.clients
    end
  end

  def admins(admins=nil, action=:add)
    if admins
      admins.split(",").each do |admin|
        admin.strip!
        case action
        when :add
          begin
            keys.add(ChefVault::ChefPatch::User.load(admin), @secret, "admins")
          rescue Net::HTTPServerException => http_error
            if http_error.response.code == "404"
              raise ChefVault::Exceptions::AdminNotFound,
                    "#{admin} is not a valid chef admin"
            else
              raise http_error
            end
          end
        when :delete
          keys.delete(admin, "admins")
        else
          raise ChefVault::Exceptions::KeysActionNotValid,
                "#{action} is not a valid action"
        end
      end
    else
      keys.admins
    end
  end

  def remove(key)
    @raw_data.delete(key)
  end

  def secret
    if @keys.include?(Chef::Config[:node_name])
      private_key = OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())
      private_key.private_decrypt(Base64.decode64(@keys[Chef::Config[:node_name]]))
    else
      raise ChefVault::Exceptions::SecretDecryption, 
            "#{data_bag}/#{id} is not encrypted with your public key.  "\
            "Contact an administrator of the vault item to encrypt for you!"
    end
  end

  def rotate_keys!
    @secret = generate_secret

    unless clients.empty?
      clients.each do |client|
        clients("name:#{client}")
      end
    end

    unless admins.empty?
      admins.each do |admin|
        admins(admin)
      end
    end

    save
    reload_raw_data
  end

  def generate_secret
    OpenSSL::PKey::RSA.new(245).to_pem.lines.to_a[1..-2].join
  end

  def []=(key, value)
    reload_raw_data if @encrypted
    super
  end

  def [](key)
    reload_raw_data if @encrypted
    super
  end

  def save(item_id=@raw_data['id'])
    # save the keys first, raising an error if no keys were defined
    if keys.admins.empty? && keys.clients.empty?
      raise ChefVault::Exceptions::NoKeysDefined, 
            "No keys defined for #{item_id}"
    end

    keys.save

    # Make sure the item is encrypted before saving
    encrypt! unless @encrypted

    # Now save the encrypted data
    if Chef::Config[:solo]
      data_bag_path = File.join(Chef::Config[:data_bag_path],
                                data_bag)
      data_bag_item_path = File.join(data_bag_path, item_id)

      FileUtils.mkdir(data_bag_path) unless File.exists?(data_bag_path)
      File.open("#{data_bag_item_path}.json",'w') do |file| 
        file.write(JSON.pretty_generate(self))
      end
      
      self
    else
      begin
        chef_data_bag = Chef::DataBag.load(data_bag)
      rescue Net::HTTPServerException => http_error
        if http_error.response.code == "404"
          chef_data_bag = Chef::DataBag.new
          chef_data_bag.name data_bag
          chef_data_bag.create
        end
      end

      super
    end
  end

  def to_json(*a)
    json = super
    json.gsub(self.class.name, self.class.superclass.name)
  end

  def destroy
    keys.destroy

    if Chef::Config[:solo]
      data_bag_path = File.join(Chef::Config[:data_bag_path],
                                data_bag)
      data_bag_item_path = File.join(data_bag_path, @raw_data["id"])

      FileUtils.rm("#{data_bag_item_path}.json")

      nil
    else
      super(data_bag, id)
    end
  end    

  def self.load(vault, name)
    item = new(vault, name)
    item.load_keys(vault, "#{name}_keys")
  
    begin
      item.raw_data = 
        Chef::EncryptedDataBagItem.load(vault, name, item.secret).to_hash
    rescue Net::HTTPServerException => http_error
      if http_error.response.code == "404"
        raise ChefVault::Exceptions::ItemNotFound,
              "#{vault}/#{name} could not be found"
      else
        raise http_error
      end
    rescue Chef::Exceptions::ValidationFailed
      raise ChefVault::Exceptions::ItemNotFound,
            "#{vault}/#{name} could not be found"
    end

    item
  end

  private
  def encrypt!
    @raw_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(self, @secret)
    @encrypted = true
  end

  def reload_raw_data
    @raw_data = 
      Chef::EncryptedDataBagItem.load(@data_bag, @raw_data["id"], secret).to_hash
    @encrypted = false

    @raw_data
  end
end