# frozen_string_literal: true

require "document/attribute_initializer"

module Document
  # This concern wraps the logic for embedding document in an active record object.
  # The document fields are stored in a jsonb column. The database migration
  # should look like this:
  #
  #   class CreateRenalwareTransplantRecipientWorkups < ActiveRecord::Migration
  #     def change
  #       create_table :transplants_recipient_workups do |t|
  #         t.belongs_to :patient, index: true, foreign_key: true
  #         t.timestamp :performed_at
  #         t.jsonb :document
  #         t.text :notes
  #
  #         t.timestamps null: false
  #       end
  #
  #       add_index :transplants_recipient_workups, :document, using: :gin
  #     end
  #   end
  #
  # You then have to create a class for the document under _/app/documents_
  # and provide a list of the attributes following the Virtus conventions.
  #
  # The class also includes the ActiveModel::Model module so you can use validations.
  #
  # Here's an example:
  #
  #   module Renalware
  #     module Transplants
  #       class RecipientWorkupDocument < Document::Embedded
  #         attribute :hx_tb, Boolean
  #         attribute :hx_dvt, Boolean
  #         attribute :pregnancies_no, Integer
  #         attribute :cervical_result, String
  #         attribute :cervical_date, Date
  #
  #         class Consent < Document::Embedded
  #           attribute :consent, Boolean
  #           attribute :consent_date, Date
  #
  #           validates_presence_of :consent_date, if: :consent
  #         end
  #
  #         validates_presence_of :cervical_date
  #       end
  #     end
  #   end
  #
  # You then include the {Document::Base} module in the parent ActiveRecord
  # and provide the document class to use.
  #
  # For instance:
  #
  #   module Renalware
  #     module Transplants
  #       class RecipientWorkup < ApplicationRecord
  #         include Document::Base
  #         has_document class_name: "Renalware::Transplants::RecipientWorkupDocument"
  #       end
  #     end
  #   end
  #
  # The document attributes can be localized, but a special convention must be followed
  # due to the namespaces, especially for simple_form.  Simply create a file
  # under _config/locales_ and provide the fields localization:
  #
  #   en:
  #     activemodel:
  #       attributes:
  #         renalware/transplants/recipient_workup_document:
  #           hx_tb: History of TB?
  #           hx_dvt: History of DVT?
  #           pregnancies_no: Number of pregnancies
  #           cervical_result: Cervical smear result
  #           cervical_date: Cervical smear date
  #         renalware/transplants/recipient_workup_document/consent:
  #           consent: Tx consent?
  #           consent_date: Tx consent date
  #     simple_form:
  #       hints:
  #         transplants_recipient_workup:
  #           cervical_date: The date and time of the cervical
  #           document:
  #             cervical_date: Just a date
  #       placeholders:
  #         transplants_recipient_workup:
  #           document:
  #             pregnancies_no: "0"
  #
  # Then in a form, you simply use the form builder fields_for helper:
  #
  #   = f.simple_fields_for :document, f.object.document do |fd|
  #     = fd.input :hx_tb, as: :boolean
  #     = fd.input :hx_dvt, as: :boolean
  #     = fd.input :cervical_date, as: :date
  #     = fd.simple_fields_for :consent, fd.object.consent do |fdd|
  #       = fdd.input :consent
  #       = fdd.input :consent_date, as: :date
  #
  #
  class Embedded
    include Virtus.model
    include ActiveModel::Model
    include ActiveModel::Validations::Callbacks
    extend Enumerize
    STRIPPABLE_TYPES = %w(Float Integer).freeze

    before_validation :strip_leading_trailing_whitespace_from_numbers

    def strip_leading_trailing_whitespace_from_numbers
      attributes.keys.each do |att|
        # Find the type defined in the document definition eg `attribute :weight, Integer``
        # Note that primitive could be a string or class, hence :to_s
        primitive = self.class.attribute_set[att].type.primitive.to_s

        # If the type is in STRIPPABLE_TYPES ie its a numeric type,
        # and it has arrived as a string (which responds to :strip) then
        # ensure there are no leading or trailing spaces, otherwise Virtus cannot
        # coerce it into the correct type. For example Virtus won't corece
        # " 1" into 1 but will coerce "1" into 1 (FYI the Dry::Types gem (the successor to Virtus)
        # rectifies this).
        # Note also that here in this before_validation callback, the act of assignment in
        # `self[att] =` prompts Virtus to re-attempt to coerce the value, which now, if space
        # has prevented it from doing so before, it will do successfully.
        next unless STRIPPABLE_TYPES.include?(primitive)

        if self[att].respond_to?(:strip)
          self[att] = self[att].strip
        end
      end
    end

    # Assign a default value to the attributes using a custom type.
    # Set a validation on nested object.
    #
    # You can specify an enum attribute by passing the `enums` options:
    #
    #   attribute :gender, enums: %i(male female)
    #
    def self.attribute(*args)
      attr_options = args.extract_options!
      attr_name, attr_type = *args

      AttributeInitializer
        .determine_initializer(self, attr_name, attr_type, attr_options)
        .call do |name, type, options|
          super(name, type, options)
        end
    end

    # Returns a list of the Virtus attributes in the model
    def self.attributes_list
      attribute_set.entries.map(&:name)
    end

    # rubocop:disable Style/ClassVars
    @@methods_to_ignore = []
    # rubocop:enable Style/ClassVars

    # Flag an old attribute to be ignored
    # when the document is deserialized from the database
    #
    #   class RecipientWorkupDocument < Document::Base
    #     old_attribute :hx_tb
    #   end
    def self.old_attribute(attribute)
      @@methods_to_ignore << attribute
      @@methods_to_ignore << "#{attribute}=".to_sym
    end

    # Flag a list of old attribtues to be ignored
    # when the document is deserialized from the database
    #
    #   class RecipientWorkupDocument < Document::Base
    #     old_attributes :hx_tb, :hx_tb_date, :foo_bar
    #   end
    def self.old_attributes(*list)
      list.each { |item| old_attribute(item) }
    end

    # Don't raise exception if known missing attribute
    def method_missing(method_sym, *arguments, &block)
      super unless @@methods_to_ignore.include? method_sym
    end
  end
end