lib/mix/tasks/ecto.migrate.arango.ex

defmodule Mix.Tasks.Ecto.Migrate.Arango do
  @moduledoc """
  Runs Migration/Rollback functions from migration modules
  """

  use Mix.Task

  alias Mix.ArangoXEcto, as: Helpers

  @aliases [
    d: :dir
  ]

  @switches [
    dir: :string
  ]

  @impl true
  def run(args) do
    Mix.Task.run("app.start")

    db_name = Helpers.get_database_name!()

    case Helpers.migrated_versions(db_name) do
      [nil] ->
        Mix.raise("ArangoXEcto is not set up, run `mix ecto.setup.arango` first.")

      _ ->
        migrate(db_name, args)
    end
  end

  defp migrate(db_name, args) do
    case OptionParser.parse!(args, aliases: @aliases, strict: @switches) do
      {[], []} ->
        up(db_name)

      {[dir: "up"], _} ->
        up(db_name)

      {_, ["up"]} ->
        up(db_name)

      {[dir: "down"], _} ->
        down(db_name)

      {_, ["down"]} ->
        down(db_name)

      {_, ["rollback"]} ->
        down(db_name)

      {_, _} ->
        Mix.raise("Unknown arguments provided, #{inspect(Enum.join(args, " "))}")
    end
  end

  defp up(db_name) do
    migrated = Helpers.migrated_versions(db_name)

    pending_migrations()
    |> Enum.filter(fn file_path ->
      t_stamp = timestamp(file_path)
      not Enum.member?(migrated, t_stamp)
    end)
    |> maybe_up_migrations(db_name)
  end

  defp maybe_up_migrations([], _db_name), do: Mix.shell().info("No migrations to action :)")

  defp maybe_up_migrations(migrations, db_name) do
    Enum.each(migrations, fn file_path ->
      case migration_module(file_path).up() do
        nil ->
          Mix.shell().error("Up function has no actions")

        :ok ->
          file_path
          |> timestamp()
          |> Helpers.update_versions(db_name)

          Mix.shell().info("Successfully Migrated #{file_path}")

        {:error, reason} ->
          Mix.shell().info("Unable to Migrate #{file_path}")
          Mix.shell().error("Status: " <> inspect(reason))
      end
    end)
  end

  @spec down(binary()) :: :ok
  def down(db_name) do
    [last_migrated_version | _] = versions(db_name)

    module =
      last_migrated_version
      |> migration_path()
      |> migration_module()

    case module.down() do
      nil ->
        Mix.shell().error("Down function has no actions")

      :ok ->
        Helpers.remove_version(last_migrated_version, db_name)

        Mix.shell().info("Successfully Rolled Back #{last_migrated_version}")

      _ ->
        Mix.shell().info("Unable to Rollback #{last_migrated_version}")
    end
  end

  defp migration_module(path) do
    {{:module, module, _, _}, _} =
      Helpers.get_default_repo!()
      |> Helpers.path_to_priv_repo()
      |> Path.join("migrations")
      |> Path.join(path)
      |> Code.eval_file()

    module
  end

  defp migration_path(version) when not is_binary(version) do
    version
    |> to_string()
    |> migration_path()
  end

  defp migration_path(version) do
    Helpers.get_default_repo!()
    |> Helpers.path_to_priv_repo()
    |> Path.join("migrations")
    |> File.ls!()
    |> Enum.find(&String.starts_with?(&1, version))
  end

  defp pending_migrations do
    Helpers.get_default_repo!()
    |> Helpers.path_to_priv_repo()
    |> Path.join("migrations")
    |> File.ls!()
    |> Enum.filter(&(!String.starts_with?(&1, ".")))
    |> Enum.sort(&(timestamp(&1) <= timestamp(&2)))
  end

  defp timestamp(path) do
    path
    |> String.split("_")
    |> hd()
    |> String.to_integer()
  end

  defp versions(db_name) do
    Helpers.migrated_versions(db_name)
    |> Enum.sort(&(&1 >= &2))
  end
end