# 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_association_name, Symbol
    const :main_class, T.class_of(ActiveRecord::Base)
    const :model_class, T.class_of(ActiveRecord::Base)
    const :foreign_key, KeyType
    const :primary_key, KeyType

    sig { void }
    def load_aux_schema
      model_class.load_schema
    end

    sig { params(main_model: ActiveRecord::Base).returns(ActiveRecord::Base) }
    def ensure_aux_target(main_model)
      aux_association = main_model.association(self.aux_association_name)
      aux_association.target ||= aux_association.build
    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 = model_class.arel_table[name]
      aux_bind =
        model_class.predicate_builder.build_bind_attribute(
          arel_attr.name,
          value
        )
      block.call(arel_attr, aux_bind)
    end

    sig do
      params(
        main_class:
          T.all(
            T.class_of(ActiveRecord::Base),
            ActiveRecord::Associations::ClassMethods,
            Module
          ),
        method_name: Symbol
      ).void
    end
    def define_aux_attribute_delegate(main_class, method_name)
      aux_config = self
      main_class.define_method(method_name) do |*args, **kwargs|
        T.bind(self, ActiveRecord::Base)
        aux_model = aux_config.ensure_aux_target(self)
        T.unsafe(aux_model).public_send(method_name, *args, **kwargs)
      end
    end

    sig do
      params(
        relation: T.any(ActiveRecord::Relation, T.class_of(ActiveRecord::Base)),
        conditions: T::Hash[String, T.untyped]
      ).returns(ActiveRecord::Relation)
    end
    def apply_split_conditions!(relation, conditions)
      main_conditions, aux_conditions =
        self.partition_by_aux_columns(conditions)
      relation = relation.where(main_conditions) if main_conditions.any?
      if aux_conditions.any?
        relation = relation.where(aux_association_name => aux_conditions)
      end
      relation
    end

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

    sig do
      params(
        main_model: ActiveRecord::Base,
        aux_args: T::Hash[Symbol, T.untyped]
      ).void
    end
    def assign_aux_attributes(main_model, aux_args)
      aux_model = self.ensure_aux_target(main_model)
      aux_model.assign_attributes(aux_args)
    end

    sig { returns(T::Array[String]) }
    def aux_column_names
      @aux_column_names ||=
        T.let(
          begin
            rejected_columns = [
              self.foreign_key,
              self.primary_key,
              "created_at",
              "updated_at"
            ].flatten.map(&:to_s)

            model_class
              .column_names
              .reject { |col| rejected_columns.include?(col.to_s) }
              .map(&:to_s)
          end,
          T.nilable(T::Array[String])
        )
    end

    sig { params(name: T.any(Symbol, String)).returns(T::Boolean) }
    def is_aux_column?(name)
      aux_column_names.include?(name.to_s)
    end

    private

    sig do
      params(hash: T::Hash[String, T.untyped]).returns(
        [T::Hash[String, T.untyped], T::Hash[String, T.untyped]]
      )
    end
    def partition_by_aux_columns(hash)
      a, b, _ = hash.partition { |k, _| !self.is_aux_column?(k) }.map(&:to_h)
      [T.must(a), T.must(b)]
    end
  end
end
