# typed: strict
# frozen_string_literal: true

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

    const :klass, T.class_of(ActiveRecord::Base)
    const :rejected_column_names, T::Set[String]

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

    sig { returns(T::Array[String]) }
    def column_names
      @column_names ||=
        T.let(
          begin
            klass
              .column_names
              .reject { |col| rejected_column_names.include?(col.to_s) }
              .map(&:to_s)
          end,
          T.nilable(T::Array[String])
        )
    end

    sig { returns(T::Hash[String, ActiveRecord::ConnectionAdapters::Column]) }
    def columns_hash
      @columns_hash ||=
        T.let(
          slice_by_columns(klass.columns_hash),
          T.nilable(T::Hash[String, ActiveRecord::ConnectionAdapters::Column])
        )
    end

    sig { returns(T::Hash[String, ActiveModel::Type]) }
    def attribute_types
      @attribute_types ||=
        T.let(
          slice_by_columns(klass.attribute_types),
          T.nilable(T::Hash[String, ActiveModel::Type])
        )
    end

    sig { returns(T::Hash[String, ActiveModel::Attribute]) }
    def default_attributes
      @default_attributes ||=
        T.let(
          begin
            da = klass._default_attributes
            da.keys.map { |k, v| [k, da[k]] }.to_h.slice(*self.column_names)
          end,
          T.nilable(T::Hash[String, ActiveModel::Attribute])
        )
    end

    sig do
      params(instance: ActiveRecord::Base).returns(T::Hash[String, T.untyped])
    end
    def attributes_on(instance)
      Util.ensure_is_instance_of!(instance, self.klass)
      unless instance.class <= self.klass
        raise("#{instance.class.name} not a #{self.klass.name}")
      end
      slice_by_columns(instance.attributes)
    end

    sig do
      type_parameters(:K, :T)
        .params(
          hash:
            T::Hash[
              T.all(T.type_parameter(:K), T.any(String, Symbol)),
              T.type_parameter(:T)
            ]
        )
        .returns(
          [
            T::Hash[
              T.all(T.type_parameter(:K), T.any(String, Symbol)),
              T.type_parameter(:T)
            ],
            T::Hash[
              T.all(T.type_parameter(:K), T.any(String, Symbol)),
              T.type_parameter(:T)
            ]
          ]
        )
    end
    def partition_by_columns(hash)
      a, b =
        hash
          .partition { |k, _| !self.column_names.include?(k.to_s) }
          .map(&:to_h)
      [T.must(a), T.must(b)]
    end

    private

    sig do
      type_parameters(:T)
        .params(hash: T::Hash[String, T.type_parameter(:T)])
        .returns(T::Hash[String, T.type_parameter(:T)])
    end
    def slice_by_columns(hash)
      T.unsafe(hash).slice(*self.column_names)
    end
  end

  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 :aux_class, T.class_of(ActiveRecord::Base)
    const :foreign_key, KeyType
    const :primary_key, KeyType

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

    sig { returns(ModelClassHelper) }
    def aux
      @aux ||=
        T.let(
          ModelClassHelper.new(
            klass: self.aux_class,
            rejected_column_names: self.aux_rejected_column_names.to_set
          ),
          T.nilable(ModelClassHelper)
        )
    end

    sig { returns(ModelClassHelper) }
    def main
      @main ||=
        T.let(
          ModelClassHelper.new(
            klass: self.main_class,
            rejected_column_names: Set.new
          ),
          T.nilable(ModelClassHelper)
        )
    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_class)
      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_class.arel_table[name]
      aux_bind =
        aux_class.predicate_builder.build_bind_attribute(arel_attr.name, value)
      block.call(arel_attr, aux_bind)
    end

    sig { params(method_name: Symbol).void }
    def define_aux_attribute_delegate(method_name)
      config = self
      main_class.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)
        puts "#{self.class.name}##{method_name} -> #{config.aux_association_name} -> (#{args.inspect} => #{ret})"
        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 = aux.partition_by_columns(conditions)
      main_conds.merge!(aux_association_name => aux_conds) if aux_conds.any?
      main_conds
    end

    private

    sig { returns(T::Set[String]) }
    def aux_rejected_column_names
      @aux_rejected_column_names ||=
        T.let(
          [foreign_key, primary_key, "created_at", "updated_at"].flatten
            .map(&:to_s)
            .to_set,
          T.nilable(T::Set[String])
        )
    end
  end
end
