# frozen_string_literal: true

# Make sure the custom attributes are defined
require "decidim/attributes"

module Decidim
  module AttributeObject
    # This provides a dummy model implementation for replacing the Virtus models
    # using ActiveModel. This class is a lightweight version of
    # `ActiveModel::Model` with the `ActiveModel::Attributes` module and its
    # overridden methods.
    #
    # The main purpose of this class is to provide a backwards compatible API
    # for defining classes that hold attributes, such as the form classes.
    #
    # Usage:
    #   Replace all instances of `Virtus.model` with `Decidim::AttributeObject::Model`.
    module Model
      extend ActiveSupport::Concern

      include ActiveModel::AttributeAssignment
      include ActiveModel::Attributes
      include Decidim::AttributeObject::TypeMap

      included do
        extend ActiveModel::Naming
        extend ActiveModel::Translation

        # Default attributes for all models
        attribute :id
      end

      class_methods do
        def attribute(name, type = ActiveModel::Type::Value.new, **)
          typedef = AttributeObject.types.resolve(type, **)

          super(name, typedef[:type], **typedef[:options])

          # Create the boolean method alias with the question mark at the end
          # which is used in some places. This was a default behavior of
          # Virtus but not a feature in ActiveModel::Attributes.
          alias_method :"#{name}?", name if typedef[:type] == :boolean

          # Add the nested validation in case validations module is loaded and
          # the type reports it needs nested validations.
          return unless include?(ActiveModel::Validations)

          # The nested validator should be only added for those attributes that
          # inherit from the AttributeObject::Model type. Otherwise this would
          # be also added e.g. for ActiveRecord objects which would cause
          # unexpected validation errors.
          finaltype = attribute_types[name.to_s]
          return unless finaltype.respond_to?(:validate_nested?)
          return unless finaltype.validate_nested?

          validates_with(
            Decidim::AttributeObject::NestedValidator,
            _merge_attributes([name])
          )
        end
      end

      def initialize(attributes = {})
        # Parentheses needed not to pass the arguments.
        super()

        return unless attributes

        # Make sure the attributes is a hash
        base_attributes =
          if attributes.is_a?(Hash)
            attributes
          else
            attributes.to_h
          end

        # Only pass the existing attribute keys to assign_attributes
        # The regular expression matching makes sure we also include the "multi"
        # parameters, such as date fields passed from the view which are
        # formatted as date(1i), date(2i), date(3i). These are converted to
        # hashes below which are handled by the ActiveModel::Attributes types,
        # such as :date.
        correct_attributes = {}.tap do |attrs|
          base_attributes.each do |k, v|
            # Handle "multi" parameter attributes, such as date(1i), date(2i),
            # date(3i). This converts these three attributes to a single hash
            # attribute formatted as:
            #   { "date" => { 1 => ..., 2 => ..., 3 => ...  } }
            mp = k.to_s.match(/(.*)\(([0-9]+i)\)$/)
            if mp
              next unless attribute_names.include?(mp[1])

              attrs[mp[1]] ||= {}
              attrs[mp[1]][mp[2].to_i] = v
            else
              next unless attribute_names.include?(k.to_s)

              attrs[k] = v
            end
          end
        end

        assign_attributes(correct_attributes)
      end

      # This provides backwards compatibility for accessing the attributes
      # through symbols by calling `obj.attributes[:key]` or
      # `obj.attributes.slice(:key1, :key2)`. In the legacy Virtus models, this
      # returned a hash with symbol keys.
      #
      # Deprecated:
      # Attributes access through symbols is deprecated and may be removed in
      # future versions. Please refactor all your attributes calls to access the
      # attributes through string keys.
      def attributes
        super.with_indifferent_access
      end

      # Convenience method for accessing the attributes through
      # model[:attr_name] which is used in multiple places across the code.
      def [](attribute_name)
        public_send(attribute_name) if respond_to?(attribute_name)
      end

      # Convenience method for settings the attributes through
      # model[:attr_name] = "foo" which is used in some places across the code.
      def []=(attribute_name, value)
        public_send("#{attribute_name}=", value) if respond_to?("#{attribute_name}=")
      end

      # Convenience method used in initiatives
      def attributes_with_values
        to_h.compact
      end

      def to_h
        hash = attributes.to_h.symbolize_keys
        hash.delete(:id) if hash.has_key?(:id) && hash[:id].blank?

        hash
      end

      # The to_hash alias is needed for the as_json call which calls this method
      # if defined.
      alias to_hash to_h
    end
  end
end
