module Tapioca::RBIFilesHelper

def duplicated_nodes_from_index(index, shim_rbi_dir:, todo_rbi_file:)

: (RBI::Index index, shim_rbi_dir: String, todo_rbi_file: String) -> Hash[String, Array[RBI::Node]]
def duplicated_nodes_from_index(index, shim_rbi_dir:, todo_rbi_file:)
  duplicates = {}
  say("Looking for duplicates... ")
  time = Benchmark.realtime do
    index.keys.each do |key|
      nodes = index[key]
      next unless shims_or_todos_have_duplicates?(nodes, shim_rbi_dir: shim_rbi_dir, todo_rbi_file: todo_rbi_file)
      duplicates[key] = nodes
    end
  end
  say(" Done ", :green)
  say("(#{time.round(2)}s)")
  duplicates
end

def extract_methods_and_attrs(nodes)

: (Array[RBI::Node] nodes) -> Array[(RBI::Method | RBI::Attr)]
def extract_methods_and_attrs(nodes)
  T.cast(
    nodes.select do |node|
      node.is_a?(RBI::Method) || node.is_a?(RBI::Attr)
    end,
    T::Array[T.any(RBI::Method, RBI::Attr)],
  )
end

def extract_shims_and_todos(nodes, shim_rbi_dir:, todo_rbi_file:)

: (Array[RBI::Node] nodes, shim_rbi_dir: String, todo_rbi_file: String) -> Array[RBI::Node]
def extract_shims_and_todos(nodes, shim_rbi_dir:, todo_rbi_file:)
  nodes.select do |node|
    node.loc&.file&.start_with?(shim_rbi_dir) || node.loc&.file == todo_rbi_file
  end
end

def gem_name_from_rbi_path(path)

: (String path) -> String
def gem_name_from_rbi_path(path)
  T.must(File.basename(path, ".rbi").split("@").first)
end

def has_duplicated_methods_and_attrs?(nodes, shims_or_todos)

: (Array[RBI::Node] nodes, Array[RBI::Node] shims_or_todos) -> bool
def has_duplicated_methods_and_attrs?(nodes, shims_or_todos)
  shims_or_todos_props = extract_methods_and_attrs(shims_or_todos)
  if shims_or_todos_props.any?
    shims_or_todos_props.each do |shim_or_todo_prop|
      other_nodes = extract_methods_and_attrs(nodes) - [shim_or_todo_prop]
      if shim_or_todo_prop.sigs.empty?
        # If the node doesn't have a signature and is an attribute accessor, we have a duplicate
        return true if shim_or_todo_prop.is_a?(RBI::Attr)
        # Now we know it's a method
        # If the node has no parameters and we compare it against an attribute of the same name, it's a duplicate
        return true if shim_or_todo_prop.params.empty? && other_nodes.grep(RBI::Attr).any?
        # If the node has parameters, we compare them against all the other methods
        # If at least one of them has the same parameters, it's a duplicate
        return true if other_nodes.grep(RBI::Method).any? { |other| shim_or_todo_prop.params == other.params }
      end
      # We compare the shim or todo prop with all the other props of the same name
      other_nodes.each do |node|
        # Another prop has the same sig, we have a duplicate
        return true if shim_or_todo_prop.sigs.any? { |sig| node.sigs.include?(sig) }
      end
    end
  end
  false
end

def has_duplicated_mixins?(shims_or_todos)

: (Array[RBI::Node] shims_or_todos) -> bool
def has_duplicated_mixins?(shims_or_todos)
  # Don't forget `shims_or_todos` is a list of nodes with the same qualified name, so if we find two mixins of the
  # same name, they _are_ about the same thing, like two `include(A)` or two `requires_ancestor(A)` so this is a
  # duplicate
  shims_or_todos.any? { |node| node.is_a?(RBI::Mixin) || node.is_a?(RBI::RequiresAncestor) }
end

def has_duplicated_scopes?(all_nodes, shims_or_todos)

: (Array[RBI::Node], Array[RBI::Node]) -> bool
def has_duplicated_scopes?(all_nodes, shims_or_todos)
  shims_or_todos_scopes = shims_or_todos.grep(RBI::Scope)
  return false if shims_or_todos_scopes.empty?
  # Extract the empty scopes from the shims or todos
  # We do not care about non-empty scopes because they hold definitions that we will check against Tapioca's
  # generated RBI files in another iteration.
  shims_or_todos_empty_scopes = shims_or_todos_scopes.select(&:empty?)
  # Extract the nodes that are not shims or todos (basically the nodes from the RBI files generated by Tapioca)
  not_shims_or_todos = all_nodes - shims_or_todos
  shims_or_todos_empty_scopes.any? do |scope|
    # Empty modules are always duplicates
    break true unless scope.is_a?(RBI::Class)
    # Empty classes without parents are also duplicates
    parent_name = scope.superclass_name
    break true unless parent_name
    # Empty classes that are not redefining the parent are also duplicates
    break true if not_shims_or_todos.any? do |node|
      node.is_a?(RBI::Class) && node.superclass_name == parent_name
    end
  end
end

def index_rbi(index, kind, file)

: (RBI::Index index, String kind, String file) -> void
def index_rbi(index, kind, file)
  return unless File.exist?(file)
  say("Loading #{kind} RBIs from #{file}... ")
  time = Benchmark.realtime do
    parse_and_index_files(index, [file], number_of_workers: 1)
  end
  say(" Done ", :green)
  say("(#{time.round(2)}s)")
end

def index_rbis(index, kind, dir, number_of_workers:)

: (RBI::Index index, String kind, String dir, number_of_workers: Integer?) -> void
def index_rbis(index, kind, dir, number_of_workers:)
  return unless Dir.exist?(dir) && !Dir.empty?(dir)
  if kind == "payload"
    say("Loading Sorbet payload... ")
  else
    say("Loading #{kind} RBIs from #{dir}... ")
  end
  time = Benchmark.realtime do
    files = Dir.glob("#{dir}/**/*.rbi").sort
    parse_and_index_files(index, files, number_of_workers: number_of_workers)
  end
  say(" Done ", :green)
  say("(#{time.round(2)}s)")
end

def location_to_payload_url(loc, path_prefix:)

: (RBI::Loc loc, path_prefix: String?) -> String
def location_to_payload_url(loc, path_prefix:)
  return loc.to_s unless path_prefix
  url = loc.file || ""
  return loc.to_s unless url.start_with?(path_prefix)
  url = url.sub(path_prefix, SorbetHelper::SORBET_PAYLOAD_URL)
  url = "#{url}#L#{loc.begin_line}"
  url
end

def parse_and_index_files(index, files, number_of_workers:)

: (RBI::Index index, Array[String] files, number_of_workers: Integer?) -> void
def parse_and_index_files(index, files, number_of_workers:)
  executor = Executor.new(files, number_of_workers: number_of_workers)
  trees = executor.run_in_parallel do |file|
    next if Spoom::Sorbet::Sigils.file_strictness(file) == "ignore"
    RBI::Parser.parse_file(file)
  rescue RBI::ParseError => e
    say_error("\nWarning: #{e} (#{e.location})", :yellow)
    nil
  end.compact
  index.visit_all(trees)
end

def shims_or_todos_have_duplicates?(nodes, shim_rbi_dir:, todo_rbi_file:)

: (Array[RBI::Node] nodes, shim_rbi_dir: String, todo_rbi_file: String) -> bool
Do the list of `nodes` sharing the same name have duplicates?
def shims_or_todos_have_duplicates?(nodes, shim_rbi_dir:, todo_rbi_file:)
  # If there is only one node, there are no duplicates
  return false if nodes.size == 1
  # Extract the nodes from the sorbet/rbi/shims/ directory and the todo.rbi file
  shims_or_todos = extract_shims_and_todos(nodes, shim_rbi_dir: shim_rbi_dir, todo_rbi_file: todo_rbi_file)
  return false if shims_or_todos.empty?
  # First let's look into scopes (classes, modules, sclass) for duplicates
  has_duplicated_scopes?(nodes, shims_or_todos) ||
    # Then let's look into mixins
    has_duplicated_mixins?(shims_or_todos) ||
    # Finally, let's compare the methods and attributes with the same name
    has_duplicated_methods_and_attrs?(nodes, shims_or_todos)
end

def update_gem_rbis_strictnesses(errors, gem_dir)

: (Array[Spoom::Sorbet::Errors::Error] errors, String gem_dir) -> void
def update_gem_rbis_strictnesses(errors, gem_dir)
  files = []
  errors.each do |error|
    # Collect the file with error
    files << error.file
    error.more.each do |line|
      # Also collect the conflicting definition file paths
      next unless line.include?("Previous definition")
      files << line.split(":").first&.strip
    end
  end
  files
    .uniq
    .sort
    .select { |file| file.start_with?(gem_dir) }
    .each do |file|
      Spoom::Sorbet::Sigils.change_sigil_in_file(file, "false")
      say("\n  Changed strictness of #{file} to `typed: false` (conflicting with DSL files)", [:yellow, :bold])
    end
  say("\n")
end

def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], compilers: [])

: (command: String, gem_dir: String, dsl_dir: String, auto_strictness: bool, ?gems: Array[Gemfile::GemSpec], ?compilers: T::Enumerable[singleton(Dsl::Compiler)]) -> void
def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], compilers: [])
  error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
  say("Checking generated RBI files... ")
  res = sorbet(
    "--no-config",
    "--error-url-base=#{error_url_base}",
    "--stop-after namer",
    dsl_dir,
    gem_dir,
  )
  say(" Done", :green)
  errors = Spoom::Sorbet::Errors::Parser.parse_string(res.err || "")
  if errors.empty?
    say("  No errors found\n\n", [:green, :bold])
    return
  end
  parse_errors = errors.select { |error| error.code < 4000 }
  error_messages = []
  if parse_errors.any?
    error_messages << set_color(<<~ERR, :red)
      ##### INTERNAL ERROR #####
      There are parse errors in the generated RBI files.
      This seems related to a bug in Tapioca.
      Please open an issue at https://github.com/Shopify/tapioca/issues/new with the following information:
      Tapioca v#{Tapioca::VERSION}
      Command:
        #{command}
    ERR
    error_messages << set_color(<<~ERR, :red) if gems.any?
      Gems:
      #{gems.map { |gem| "  #{gem.name} (#{gem.version})" }.join("\n")}
    ERR
    error_messages << set_color(<<~ERR, :red) if compilers.any?
      Compilers:
      #{compilers.map { |compiler| "  #{compiler.name}" }.join("\n")}
    ERR
    error_messages << set_color(<<~ERR, :red)
      Errors:
      #{parse_errors.map { |error| "  #{error}" }.join("\n")}
      ##########################
    ERR
  end
  if auto_strictness
    redef_errors = errors.select { |error| error.code == 4010 }
    update_gem_rbis_strictnesses(redef_errors, gem_dir)
  end
  Kernel.raise Thor::Error, error_messages.join("\n") if parse_errors.any?
end