module SymmetricEncryption
  module ActiveRecord
    module AttrEncrypted
      def self.included(base)
        base.extend ClassMethods
      end

      module ClassMethods
        # Transparently encrypt and decrypt values stored via ActiveRecord.
        #
        # Parameters:
        # * Symbolic names of each method to create which has a corresponding
        #   method already defined in rails starting with: encrypted_
        # * Followed by an optional hash:
        #     :random_iv [true|false]
        #       Whether the encrypted value should use a random IV every time the
        #       field is encrypted.
        #       It is recommended to set this to true where feasible. If the encrypted
        #       value could be used as part of a SQL where clause, or as part
        #       of any lookup, then it must be false.
        #       Setting random_iv to true will result in a different encrypted output for
        #       the same input string.
        #       Note: Only set to true if the field will never be used as part of
        #         the where clause in an SQL query.
        #       Note: When random_iv is true it will add a 8 byte header, plus the bytes
        #         to store the random IV in every returned encrypted string, prior to the
        #         encoding if any.
        #       Default: false
        #       Highly Recommended where feasible: true
        #
        #     :type [Symbol]
        #       The type for this field, #see SymmetricEncryption::COERCION_TYPES
        #       Default: :string
        #
        #     :compress [true|false]
        #       Whether to compress str before encryption
        #       Should only be used for large strings since compression overhead and
        #       the overhead of adding the 'magic' header may exceed any benefits of
        #       compression
        #       Note: Adds a 6 byte header prior to encoding, only if :random_iv is false
        #       Default: false
        def attr_encrypted(*attributes, random_iv: nil, type: :string, compress: false)
          # Ensure ActiveRecord has created all its methods first
          # Ignore failures since the table may not yet actually exist
          begin
            define_attribute_methods
          rescue StandardError
            nil
          end

          random_iv = true if random_iv.nil? && SymmetricEncryption.randomize_iv?

          if random_iv.nil?
            warn("attr_encrypted() no longer allows a default value for option `random_iv`. Add `random_iv: false` if it is required.")
          end

          attributes.each do |attribute|
            SymmetricEncryption::Generator.generate_decrypted_accessors(
              self,
              attribute,
              "encrypted_#{attribute}",
              random_iv: random_iv,
              type:      type,
              compress:  compress
            )
            encrypted_attributes[attribute.to_sym] = "encrypted_#{attribute}".to_sym
          end
        end

        # Contains a hash of encrypted attributes with virtual attribute names as keys and real attribute
        # names as values
        #
        # Example
        #
        #   class User < ActiveRecord::Base
        #     attr_encrypted :email
        #   end
        #
        #   User.encrypted_attributes  =>  { email: encrypted_email }
        def encrypted_attributes
          @encrypted_attributes ||= superclass.respond_to?(:encrypted_attributes) ? superclass.encrypted_attributes.dup : {}
        end

        # Return the name of all encrypted virtual attributes as an Array of symbols
        # Example: [:email, :password]
        def encrypted_keys
          @encrypted_keys ||= encrypted_attributes.keys
        end

        # Return the name of all encrypted columns as an Array of symbols
        # Example: [:encrypted_email, :encrypted_password]
        def encrypted_columns
          @encrypted_columns ||= encrypted_attributes.values
        end

        # Returns whether an attribute has been configured to be encrypted
        #
        # Example
        #
        #   class User < ActiveRecord::Base
        #     attr_accessor :name
        #     attr_encrypted :email
        #   end
        #
        #   User.encrypted_attribute?(:name) # false
        #   User.encrypted_attribute?(:email) # true
        def encrypted_attribute?(attribute)
          encrypted_keys.include?(attribute)
        end

        # Returns whether the attribute is the database column to hold the
        # encrypted data for a matching encrypted attribute
        #
        # Example
        #
        #   class User < ActiveRecord::Base
        #     attr_accessor :name
        #     attr_encrypted :email
        #   end
        #
        #   User.encrypted_column?(:encrypted_name) # false
        #   User.encrypted_column?(:encrypted_email) # true
        def encrypted_column?(attribute)
          encrypted_columns.include?(attribute)
        end
      end
    end
  end
end
