lib/foundry/spark_meta/helpers.ex

defmodule Foundry.SparkMeta.Helpers do
  @moduledoc false

  def format_module_fqn(nil), do: nil

  def format_module_fqn(module) when is_atom(module) do
    module |> Atom.to_string() |> String.replace_prefix("Elixir.", "")
  end

  def format_module_fqn(module) when is_binary(module), do: module
  def format_module_fqn(_module), do: nil

  def get_attr_list(attrs, key) do
    case Keyword.get(attrs, key) do
      nil -> []
      list when is_list(list) -> Enum.map(list, &to_string/1)
      value -> [to_string(value)]
    end
  rescue
    _ -> []
  end

  def get_attr_raw(attrs, key) do
    case Keyword.get(attrs, key) do
      nil -> nil
      [value | _] -> value
      value -> value
    end
  rescue
    _ -> nil
  end

  def get_attr_single(attrs, key) do
    case Keyword.get(attrs, key) do
      nil -> nil
      [value | _] -> to_string(value)
      value -> to_string(value)
    end
  rescue
    _ -> nil
  end

  def module_source_path(module) when is_atom(module) do
    try do
      case module.__info__(:compile)[:source] do
        nil -> find_module_source_file(Atom.to_string(module))
        charlist ->
          path = to_string(charlist)
          if File.exists?(path), do: path, else: find_module_source_file(Atom.to_string(module))
      end
    rescue
      _ -> find_module_source_file(Atom.to_string(module))
    end
  end

  def find_module_source_file(module_str) do
    parts = String.split(module_str, ".")

    filename_lower =
      (parts
       |> Enum.drop(2)
       |> Enum.map(&Macro.underscore/1)
       |> Enum.join("/")) <> ".ex"

    cwd = File.cwd!()

    candidates = [
      Path.join(cwd, "lib/#{filename_lower}"),
      Path.join(cwd, filename_lower)
    ]

    case Enum.find(candidates, &File.exists?/1) do
      nil -> search_lib_files(Path.join(cwd, "lib"), module_str)
      file -> file
    end
  end

  def search_lib_files(lib_path, module_str) do
    try do
      clean_module_str = String.replace_prefix(module_str, "Elixir.", "")

      lib_path
      |> File.ls!()
      |> Enum.find_value(fn entry ->
        full_path = Path.join(lib_path, entry)

        cond do
          File.dir?(full_path) ->
            search_lib_files(full_path, module_str)

          String.ends_with?(entry, ".ex") ->
            case File.read(full_path) do
              {:ok, content} ->
                if String.contains?(content, "defmodule #{clean_module_str}"), do: full_path, else: nil

              _ ->
                nil
            end

          true ->
            nil
        end
      end)
    rescue
      _ -> nil
    end
  end

  def extract_runbook_from_source(module) do
    try do
      module_str = Atom.to_string(module)

      case module_source_path(module) do
        nil -> nil
        file -> extract_runbook_from_file(file, module_str)
      end
    rescue
      _ -> nil
    end
  end

  def extract_runbook_from_file(file, module_str) do
    try do
      clean_module_str = String.replace_prefix(module_str, "Elixir.", "")
      content = File.read!(file)
      lines = String.split(content, "\n")

      module_line_idx =
        Enum.find_index(lines, fn line ->
          String.contains?(line, "defmodule #{clean_module_str}")
        end)

      case module_line_idx do
        nil ->
          nil

        idx ->
          lines
          |> Enum.drop(idx + 1)
          |> Enum.take_while(&(not String.match?(&1, ~r/^\s*defmodule\s+\w/)))
          |> Enum.find_value(fn line ->
            case Regex.run(~r/@runbook\s+"([^"]+)"/, line) do
              [_, path] -> path
              _ -> nil
            end
          end)
      end
    rescue
      _ -> nil
    end
  end
end