# typed: strict
# frozen_string_literal: true

module HasAuxTable
  class AuxTableConfig < T::Struct
    extend T::Sig

    const :aux_table_name, Symbol
    const :aux_association_name, Symbol
    const :main, ModelClassHelper
    const :aux, ModelClassHelper
    const :allow_redefining, T::Array[Symbol]
    sig do
      params(
        main_class: T.class_of(ActiveRecord::Base),
        aux_class: T.class_of(ActiveRecord::Base),
        aux_association_name: Symbol,
        allow_redefining: T::Array[Symbol]
      ).returns(AuxTableConfig)
    end
    def self.from_models(
      main_class:,
      aux_class:,
      aux_association_name:,
      allow_redefining: []
    )
      primary_key = aux_class.primary_key
      aux_rejected_column_names = [
        primary_key,
        "created_at",
        "updated_at"
      ].flatten.map(&:to_s).to_set

      new(
        aux_table_name: aux_class.table_name.to_sym,
        aux_association_name:,
        main:
          ModelClassHelper.new(
            klass: main_class,
            rejected_column_names: Set.new
          ),
        aux:
          ModelClassHelper.new(
            klass: aux_class,
            rejected_column_names: aux_rejected_column_names
          ),
        allow_redefining:
      )
    end

    sig { returns(T.untyped) }
    def load_aux_schema
      aux.klass.load_schema
    end

    sig do
      params(main_instance: ActiveRecord::Base).returns(ActiveRecord::Base)
    end
    def aux_model_for(main_instance)
      Util.ensure_is_instance_of!(main_instance, main.klass)
      aux_association = main_instance.association(self.aux_association_name)
      aux_association.target ||=
        (
          if main_instance.persisted?
            aux_association.load_target || aux_association.build
          else
            aux_association.build
          end
        )
    end

    sig do
      params(
        name: Symbol,
        value: T.untyped,
        block:
          T
            .proc
            .params(arel_attr: T.untyped, aux_bind: T.untyped)
            .returns(Arel::Nodes::Node)
      ).returns(Arel::Nodes::Node)
    end
    def aux_bind_attribute(name, value, &block)
      arel_attr = aux.klass.arel_table[name]
      aux_bind =
        aux.klass.predicate_builder.build_bind_attribute(arel_attr.name, value)
      block.call(arel_attr, aux_bind)
    end

    # Forward method call `method_name` to the aux model
    sig { params(method_name: Symbol).void }
    def define_aux_attribute_delegate(method_name)
      config = self
      main
        .klass
        .define_method(method_name) do |*args, **kwargs, &block|
          T.bind(self, ActiveRecord::Base)
          aux_model = config.aux_model_for(self)
          ret =
            T.unsafe(aux_model).public_send(
              method_name,
              *args,
              **kwargs,
              &block
            )
          ret
        end
    end

    sig do
      params(conditions: T::Hash[String, T.untyped]).returns(
        T::Hash[String, T.untyped]
      )
    end
    def remap_conditions(conditions)
      main_conds, aux_conds = partition_by_columns(conditions)
      main_conds.merge!(aux_association_name => aux_conds) if aux_conds.any?
      main_conds
    end

    sig do
      type_parameters(:K)
        .params(
          hash:
            T::Hash[
              T.all(T.type_parameter(:K), T.any(String, Symbol)),
              T.untyped
            ]
        )
        .returns(
          [
            T::Hash[
              T.all(T.type_parameter(:K), T.any(String, Symbol)),
              T.untyped
            ],
            T::Hash[
              T.all(T.type_parameter(:K), T.any(String, Symbol)),
              T.untyped
            ]
          ]
        )
    end
    def partition_by_columns(hash)
      main = {}
      aux = {}
      hash.each do |k, v|
        if self.aux.column_names.include?(k.to_s)
          # attribute is a column on the aux table
          aux[k] = v
          next
        elsif assoc = self.main.klass.reflect_on_association(k.to_s)
          if assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection)
            # attribute is an association on the main class
            fk = assoc.foreign_key
            if self.aux.column_names.include?(fk)
              # the association is a column on the aux table, `v` is
              # a model, get the primary key of the model
              aux[fk] = v && v.send(assoc.association_primary_key)
              next
            end
          elsif assoc.is_a?(ActiveRecord::Reflection::HasOneReflection) ||
                assoc.is_a?(ActiveRecord::Reflection::HasManyReflection)
            pk = assoc.active_record_primary_key
            if self.aux.column_names.include?(pk)
              aux[pk] = v && v.send(assoc.foreign_key)
              next
            end
          else
            raise "unsupported association type: #{assoc.class}"
          end
        end

        # attribute is not a column on the aux table or an association,
        # assume it's a column on the main table
        main[k] = v
      end
      [main, aux]
    end
  end
end
