require 'attr_vault/errors'
require 'attr_vault/keyring'
require 'attr_vault/secret'
require 'attr_vault/encryption'
require 'attr_vault/cryptor'

module AttrVault
  def self.included(base)
    base.extend(ClassMethods)
    base.include(InstanceMethods)
  end

  module InstanceMethods
    def before_save
      keyring = self.class.vault_keys
      current_key = keyring.current_key
      key_id = self[self.class.vault_key_field]
      record_key = self.class.vault_keys.fetch(key_id) unless key_id.nil?

      @vault_dirty_attrs ||= {}
      if !record_key.nil? && current_key != record_key
        # If the record key is not nil and not current, flag *all*
        # attrs as dirty, since we want to rewrite them all in order
        # to use the latest key. Note that when the record key is nil,
        # we're dealing with a new record, so there are no existing
        # vault attributes to rewrite. We only write these out when
        # they're set explicitly in a new record, in which case they
        # will be in the dirty attrs already and are handled below.
        self.class.vault_attrs.each do |attr|
          next if @vault_dirty_attrs.has_key? attr.name
          @vault_dirty_attrs[attr.name] = self.send(attr.name)
        end
      end
      # If any attr has plaintext_source_field and the plaintext field
      # has a value set, flag the attr as dirty using the plaintext
      # source value, then nil out the plaintext field.
      self.class.vault_attrs.reject { |attr| attr.plaintext_source_field.nil? }.each do |attr|
        unless self[attr.plaintext_source_field].nil?
          @vault_dirty_attrs[attr.name] = self[attr.plaintext_source_field]
          self[attr.plaintext_source_field] = nil
        end
      end
      self.class.vault_attrs.each do |attr|
        next unless @vault_dirty_attrs.has_key? attr.name

        value = @vault_dirty_attrs[attr.name]
        encrypted, hmac = Cryptor.encrypt(value, current_key.value)

        unless encrypted.nil?
          encrypted = Sequel.blob(encrypted)
        end
        unless hmac.nil?
          hmac = Sequel.blob(hmac)
        end

        self[attr.encrypted_field] = encrypted
        self[attr.hmac_field] = hmac
      end
      self[self.class.vault_key_field] = current_key.id
      @vault_dirty_attrs = {}
      super
    end
  end

  module ClassMethods
    def vault_keyring(keyring_data, key_field: :key_id)
      @key_field = key_field.to_sym
      @keyring = Keyring.load(keyring_data)
    end

    def vault_attr(name, opts={})
      attr = VaultAttr.new(name, opts)
      self.vault_attrs << attr

      define_method(name) do
        # if there is a plaintext source field, use that and ignore
        # the encrypted field
        if !attr.plaintext_source_field.nil? && !self[attr.plaintext_source_field].nil?
          return self[attr.plaintext_source_field]
        end

        keyring = self.class.vault_keys
        key_id = self[self.class.vault_key_field]
        record_key = self.class.vault_keys.fetch(key_id)

        encrypted_value = self[attr.encrypted_field]
        hmac =  self[attr.hmac_field]
        # TODO: cache decrypted value
        Cryptor.decrypt(encrypted_value, hmac, record_key.value)
      end

      define_method("#{name}=") do |value|
        @vault_dirty_attrs ||= {}
        @vault_dirty_attrs[name] = value
        # ensure that Sequel knows that this is in fact dirty and must
        # be updated--otherwise, the object is never saved,
        # #before_save is never called, and we never store the update
        self.modified! attr.encrypted_field
        self.modified! attr.hmac_field
      end
    end

    def vault_attrs
      @vault_attrs ||= []
    end

    def vault_key_field
      @key_field
    end

    def vault_keys
      @keyring
    end
  end

  class VaultAttr
    attr_reader :name, :encrypted_field, :hmac_field, :plaintext_source_field

    def initialize(name,
                   encrypted_field: "#{name}_encrypted",
                   hmac_field: "#{name}_hmac",
                   plaintext_source_field: nil)
      @name = name
      @encrypted_field = encrypted_field.to_sym
      @hmac_field = hmac_field.to_sym
      @plaintext_source_field = plaintext_source_field.to_sym unless plaintext_source_field.nil?
    end
  end
end