defmodule Igniter.Project.TaskAliases do
  @moduledoc "Codemods and utilities for interacting with task aliases in the mix.exs file"

  @doc """
  Adds an alias to the mix.exs file

  # Options

  - `:if_exists` - How to alter the alias if it already exists. Options are:
  - `:ignore` - Do nothing if the alias already exists. This is the default.
  - `:prepend` - Add the new alias to the beginning of the list (if it is not already present).
  - `{:prepend, value}` - Add a different value than the originally supplied alias to the beginning of the list (if it is not already present).
  - `{:replace_or_append, old, new}` - If the old value is in the list, it is replaced with the new value. Otherwise the new value is appended (if it is not already present).
  - `:append` - Add the new alias to the end of the list (if it is not already present).
  - `{:append, value}` - Add a different value than the originally supplied alias to the end of the list (if it is not already present).
  - `:warn` - Print a warning if the alias already exists.
  """

  @spec add_alias(
          Igniter.t(),
          atom() | String.t(),
          String.t() | {:code, term()} | list(String.t() | {:code, term()}),
          opts :: Keyword.t()
        ) :: Igniter.t()
  def add_alias(igniter, name, value, opts \\ []) do
    alter =
      case Keyword.get(opts, :if_exists, :ignore) do
        :append -> {:append, value}
        :prepend -> {:prepend, value}
        other -> other
      end

    name =
      if is_binary(name) do
        String.to_atom(name)
      else
        name
      end

    igniter
    |> Igniter.update_elixir_file("mix.exs", fn zipper ->
      case go_to_aliases(zipper) do
        {:ok, zipper} ->
          case alter do
            {:replace_or_append, old, new} ->
              case Igniter.Code.Keyword.set_keyword_key(
                     zipper,
                     name,
                     value,
                     &replace_or_append(&1, old, new, value)
                   ) do
                {:ok, zipper} ->
                  {:ok, zipper}

                :error ->
                  {:warning,
                   """
                   Could not modify mix task aliases. Attempted to alias `#{name}` to:

                       #{Sourceror.to_string(value)}

                   Please manually modify your `mix.exs` file accordingly.
                   """}
              end

            {prepend_or_append, add_value} when prepend_or_append in [:prepend, :append] ->
              case Igniter.Code.Keyword.set_keyword_key(
                     zipper,
                     name,
                     value,
                     &prepend_or_append(&1, add_value, prepend_or_append)
                   ) do
                {:ok, zipper} ->
                  {:ok, zipper}

                :error ->
                  {:warning,
                   """
                   Could not modify mix task aliases. Attempted to alias `#{name}` to:

                       #{Sourceror.to_string(value)}

                   Please manually modify your `mix.exs` file accordingly.
                   """}
              end

            other ->
              case Igniter.Code.Keyword.get_key(zipper, name) do
                {:ok, _} ->
                  case other do
                    :ignore ->
                      {:ok, zipper}

                    :error ->
                      {:ok, zipper}

                    :warn ->
                      {:warning,
                       """
                       Could not add alias for `#{name}` in `mix.exs` because it already exists. Attempted to alias it to:

                          #{Sourceror.to_string(value)}
                       """}
                  end

                :error ->
                  case Igniter.Code.Keyword.set_keyword_key(zipper, name, value, &{:ok, &1}) do
                    {:ok, zipper} ->
                      {:ok, zipper}

                    :error ->
                      {:warning,
                       """
                       Could not modify mix task aliases. Attempted to alias `#{name}` to:

                           #{Sourceror.to_string(value)}

                       Please manually modify your `mix.exs` file accordingly.
                       """}
                  end
              end
          end

        :error ->
          {:warning,
           """
           Could not modify mix task aliases. Attempted to alias `#{name}` to:

               #{Sourceror.to_string(value)}

           Please manually modify your `mix.exs` file accordingly.
           """}
      end
    end)
  end

  @doc "Modifies an existing alias, doing nothing if it doesn't exist"
  @spec modify_existing_alias(
          Igniter.t(),
          atom() | String.t(),
          (Sourceror.Zipper.t() ->
             {:ok, Sourceror.Zipper.t()} | :error)
        ) :: Igniter.t()
  def modify_existing_alias(igniter, name, updater) do
    igniter
    |> Igniter.update_elixir_file("mix.exs", fn zipper ->
      name =
        if is_binary(name) do
          String.to_atom(name)
        else
          name
        end

      case go_to_aliases(zipper) do
        {:ok, zipper} ->
          case Igniter.Code.Keyword.get_key(zipper, name) do
            {:ok, zipper} ->
              case updater.(zipper) do
                :error -> zipper
                other -> other
              end

            :error ->
              zipper
          end

        :error ->
          zipper
      end
    end)
  end

  defp prepend_or_append(zipper, value, prepend_or_append) when is_list(value) do
    if prepend_or_append == :prepend do
      Enum.reverse(value)
    else
      value
    end
    |> Enum.reduce_while({:ok, zipper}, fn value, {:ok, zipper} ->
      case prepend_or_append(zipper, value, prepend_or_append) do
        {:ok, zipper} -> {:cont, {:ok, zipper}}
        :error -> {:halt, :error}
      end
    end)
  end

  defp prepend_or_append(zipper, value, :append) do
    zipper =
      if Igniter.Code.List.list?(zipper) do
        zipper
      else
        Igniter.Code.Common.replace_code(zipper, [zipper.node])
      end

    Igniter.Code.List.append_new_to_list(zipper, value)
  end

  defp prepend_or_append(zipper, value, :prepend) do
    zipper =
      if Igniter.Code.List.list?(zipper) do
        zipper
      else
        Igniter.Code.Common.replace_code(zipper, [zipper.node])
      end

    Igniter.Code.List.prepend_new_to_list(zipper, value)
  end

  defp replace_or_append(zipper, old, new, value) do
    zipper =
      if Igniter.Code.List.list?(zipper) do
        zipper
      else
        Igniter.Code.Common.replace_code(zipper, [zipper.node])
      end

    case Igniter.Code.List.move_to_list_item(zipper, &Igniter.Code.Common.nodes_equal?(&1, old)) do
      {:ok, zipper} ->
        # TODO: this is very dumb. We need a special value that signifies you've
        # been given AST vs a value, like `%Igniter.AST{}` or something, and default to assuming
        # that what you've been given is a value.
        new =
          if is_binary(new) do
            {:__block__,
             [
               trailing_comments: [],
               leading_comments: [],
               delimiter: "\""
             ], [new]}
          else
            new
          end

        {:ok, Igniter.Code.Common.replace_code(zipper, new)}

      _ ->
        Igniter.Code.List.append_new_to_list(zipper, value)
    end
  end

  defp go_to_aliases(zipper) do
    with {:ok, zipper} <- Igniter.Code.Function.move_to_defp(zipper, :aliases, 0),
         zipper <- Igniter.Code.Common.rightmost(zipper),
         true <- Igniter.Code.List.list?(zipper) do
      {:ok, zipper}
    else
      _ ->
        with {:ok, zipper} <- Igniter.Code.Function.move_to_def(zipper, :project, 0),
             zipper <- Igniter.Code.Common.rightmost(zipper),
             true <- Igniter.Code.List.list?(zipper),
             {:aliases_key, _zipper, {:ok, zipper}} <-
               {:aliases_key, zipper, Igniter.Code.Keyword.get_key(zipper, :aliases)},
             {:aliases_is_list?, zipper, true} <-
               {:aliases_is_list?, zipper, Igniter.Code.List.list?(zipper)} do
          {:ok, zipper}
        else
          {:aliases_key, zipper, :error} ->
            with {:ok, zipper} <-
                   Igniter.Code.Keyword.set_keyword_key(
                     zipper,
                     :aliases,
                     quote do
                       aliases()
                     end,
                     &{:ok, &1}
                   ),
                 {:ok, zipper} <-
                   Igniter.Code.Common.move_upwards(
                     zipper,
                     &Igniter.Code.Function.function_call?(&1, :defmodule)
                   ),
                 {:ok, zipper} <- Igniter.Code.Common.move_to_do_block(zipper) do
              zipper
              |> Igniter.Code.Common.add_code("""
              defp aliases() do
                []
              end
              """)
              |> go_to_aliases()
            else
              _ ->
                :error
            end

          {:aliases_is_list?, zipper, false} ->
            if Igniter.Code.Function.function_call?(zipper) do
              case Igniter.Code.Function.get_local_function_call_name(zipper) do
                {:ok, name} ->
                  with {:ok, zipper} <-
                         Igniter.Code.Common.move_upwards(
                           zipper,
                           &Igniter.Code.Function.function_call?(&1, :defmodule)
                         ),
                       {:ok, zipper} <- Igniter.Code.Common.move_to_do_block(zipper),
                       {:ok, zipper} <- Igniter.Code.Function.move_to_defp(zipper, name, 0),
                       zipper <- Igniter.Code.Common.rightmost(zipper),
                       true <- Igniter.Code.List.list?(zipper) do
                    {:ok, zipper}
                  else
                    _ ->
                      :error
                  end

                :error ->
                  :error
              end
            else
              :error
            end

          _ ->
            :error
        end
    end
  end
end
