# typed: strict
require('parlour')
require('sorbet-rails/model_utils')
require('sorbet-rails/model_plugins/plugins')

class SorbetRails::ModelRbiFormatter
  extend T::Sig
  extend SorbetRails::ModelPlugins
  include SorbetRails::ModelUtils

  sig { override.returns(T.class_of(ActiveRecord::Base)) }
  attr_reader :model_class

  sig { returns(T::Set[String]) }
  attr_reader :available_classes

  sig {
    params(
      model_class: T.class_of(ActiveRecord::Base),
      available_classes: T::Set[String],
    ).
    void
  }
  def initialize(model_class, available_classes)
    @model_class = T.let(model_class, T.class_of(ActiveRecord::Base))
    @available_classes = T.let(available_classes, T::Set[String])
    begin
      # Load all dynamic instance methods of this model by instantiating a fake model
      @model_class.new unless @model_class.abstract_class?
    rescue StandardError => err
      puts
      puts "Note: Unable to create new instance of #{model_class_name}"
      puts "Got a #{err.class}, with this message: #{err.message}"
      puts
    end
  end

  sig {returns(String)}
  def generate_rbi
    puts "-- Generate sigs for #{model_class_name} --"

    # Collect the instances of each plugin into an array
    plugin_instances = self.class.get_plugins.map do |plugin_klass|
      plugin_klass.new(model_class, available_classes)
    end

    generator = Parlour::RbiGenerator.new(break_params: 3)
    run_plugins(plugin_instances, generator)
    # Generate the base after the plugins because when ConflictResolver merge the modules,
    # it'll put the modules at the last position merged. Putting the base stuff
    # last will keep the order consistent and minimize changes when new plugins are added.
    generate_base_rbi(generator.root)

    Parlour::ConflictResolver.new.resolve_conflicts(generator.root) do |msg, candidates|
      puts "Conflict: #{msg}. Skip following methods"
      candidates.each do |c|
        puts "- Method `#{c.name}` generated by #{c.generated_by.class.name}"
      end
      nil
    end

    rbi = <<~MESSAGE
      # This is an autogenerated file for dynamic methods in #{self.model_class_name}
      # Please rerun bundle exec rake rails_rbi:models[#{self.model_class_name}] to regenerate.

    MESSAGE

    rbi += generator.rbi
    return rbi
  end

  sig { params(root: Parlour::RbiGenerator::Namespace).void }
  def generate_base_rbi(root)
    # This is the backbone of the model_rbi_formatter.
    # It could live in a base plugin but I consider it not replacable and better to leave here
    model_relation_rbi = root.create_class(
      self.model_relation_class_name,
      superclass: "ActiveRecord::Relation",
    )
    model_relation_rbi.create_include(self.model_query_methods_returning_relation_module_name)
    model_relation_rbi.create_constant(
      "Elem",
      value: "type_member {{fixed: #{model_class_name}}}",
    )

    model_assoc_relation_rbi = root.create_class(
      self.model_assoc_relation_class_name,
      superclass: "ActiveRecord::AssociationRelation",
    )
    model_assoc_relation_rbi.create_include(self.model_query_methods_returning_assoc_relation_module_name)
    model_assoc_relation_rbi.create_constant(
      "Elem",
      value: "type_member {{fixed: #{model_class_name}}}",
    )

    collection_proxy_rbi = root.create_class(
      self.model_assoc_proxy_class_name,
      superclass: "ActiveRecord::Associations::CollectionProxy",
    )
    collection_proxy_rbi.create_include(self.model_query_methods_returning_assoc_relation_module_name)
    collection_proxy_rbi.create_constant(
      "Elem",
      value: "type_member {{fixed: #{self.model_class_name}}}",
    )

    model_rbi = root.create_class(
      self.model_class_name,
      superclass: T.must(@model_class.superclass).name,
    )
    model_rbi.create_extend(self.model_query_methods_returning_relation_module_name)
    model_rbi.create_type_alias(
      self.model_relation_type_class_name,
      type: self.model_relation_type_alias
    )
  end

  sig {
    params(
      plugins: T::Array[Parlour::Plugin],
      generator: Parlour::RbiGenerator,
    ).
    void
  }
  def run_plugins(plugins, generator)
    allow_failure = ENV["SBR_DEBUG_MODE"] == "true"
    plugins.each do |plugin|
      begin
        generator.current_plugin = plugin
        plugin.generate(generator.root)
      rescue Exception => e
        raise e unless allow_failure
        puts "!!! Plugin #{plugin.class.name} threw an exception: #{e}"
      end
    end
  end
end
