defmodule Mix.Tasks.Igniter.InitLibrary do
  @moduledoc """
  Set up a library to use Igniter. Adds the optional dependency
  and an install task.

  ## Args

  mix igniter.init_library my_lib

  * `--dry-run` - Run the task without making any changes.
  * `--yes` - Automatically answer yes to any prompts.
  * `--yes-to-deps` - Automatically answer yes to any prompts about installing new deps.
  * `--verbose` - display additional output from various operations
  """
  use Mix.Task

  @apps [:logger, :public_key, :ssl, :inets, :eex]

  @impl true
  @shortdoc "Set up a library to use Igniter. Adds the optional dependency and an install task."
  def run(argv) do
    app_name =
      case argv do
        [] ->
          Mix.shell().error("""
          Missing argument: app name

          Try running `mix igniter.init_library my_lib`.
          """)

          exit({:shutdown, 1})

        [app_name] ->
          app_name
      end

    archives_path = Mix.path_for(:archives)

    archive_apps =
      archives_path
      |> File.ls()
      |> case do
        {:ok, entries} ->
          entries

        _ ->
          []
      end
      |> Enum.map(&Mix.Local.archive_ebin/1)
      |> Enum.map(&Path.join([archives_path, &1, "*.app"]))
      |> Enum.flat_map(&Path.wildcard/1)
      |> Enum.map(&Path.basename(&1, ".app"))
      |> Enum.map(&String.to_atom/1)

    for app <- @apps ++ archive_apps do
      try do
        Mix.ensure_application!(app)
      rescue
        _ ->
          :ok
      end
    end

    message =
      cond do
        "--igniter-repeat" in argv ->
          "setting up igniter"

        "--from-igniter-new" in argv ->
          "installing igniter"

        true ->
          "checking for igniter in project"
      end

    if !Process.get(:updated_igniter?) do
      Igniter.Installer.Loading.with_spinner(
        "Updating project's igniter dependency",
        fn ->
          System.cmd("mix", ["deps.update", "igniter"], stderr_to_stdout: true)
        end,
        verbose?: "--verbose" in argv
      )

      Process.put(:updated_igniter?, true)
    end

    Igniter.Installer.Loading.with_spinner(
      message,
      fn ->
        if Code.ensure_loaded?(Igniter.Util.Install) do
          Mix.Task.run("deps.loadpaths", ["--no-deps-check"])
        end
      end,
      verbose?: "--verbose" in argv
    )

    argv = Enum.reject(argv, &(&1 in ["--igniter-repeat", "--from-igniter-new"]))

    if File.exists?("mix.exs") do
      contents =
        "mix.exs"
        |> File.read!()

      new_contents =
        contents
        |> add_igniter_dep()

      new_contents =
        if contents == new_contents do
          contents
        else
          new_contents
          |> Code.format_string!()
        end

      if new_contents == contents && !String.contains?(contents, "{:igniter,") do
        Mix.shell().error("""
        Failed to add igniter to mix.exs. Please add it manually and try again

        For more information, see: https://hexdocs.pm/igniter/readme.html#installation
        """)

        exit({:shutdown, 1})
      else
        File.write!("mix.exs", new_contents)

        Mix.Project.clear_deps_cache()
        Mix.Project.pop()
        Mix.Dep.clear_cached()

        old_undefined = Code.get_compiler_option(:no_warn_undefined)
        old_relative_paths = Code.get_compiler_option(:relative_paths)
        old_ignore_module_conflict = Code.get_compiler_option(:ignore_module_conflict)

        try do
          Code.compiler_options(
            relative_paths: false,
            no_warn_undefined: :all,
            ignore_module_conflict: true
          )

          _ = Code.compile_file("mix.exs")
        after
          Code.compiler_options(
            relative_paths: old_relative_paths,
            no_warn_undefined: old_undefined,
            ignore_module_conflict: old_ignore_module_conflict
          )
        end

        Igniter.Installer.Loading.with_spinner(
          "compiling igniter",
          fn ->
            case System.cmd("mix", ["deps.get"]) do
              {_, 0} ->
                :ok

              {output, status} ->
                raise("""
                mix deps.get failed with exit code #{status}.

                Output:

                #{output}
                """)
            end

            Mix.Task.reenable("deps.compile")
            Mix.Task.reenable("deps.loadpaths")
            Mix.Task.run("deps.compile", [])
            Mix.Task.run("deps.loadpaths", ["--no-deps-check"])
            Mix.Task.reenable("deps.compile")
            Mix.Task.reenable("deps.loadpaths")
          end,
          verbose?: "--verbose" in argv
        )

        Mix.Task.run("igniter.gen.task", ["#{app_name}.install"])
      end
    else
      Mix.shell().error("""
      Not in a mix project. No `mix.exs` file was found.

      Did you mean `mix igniter.new`?
      """)

      exit({:shutdown, 1})
    end
  end

  defp add_igniter_dep(contents) do
    version_requirement = "\"~> 0.6\""

    if String.contains?(contents, "{:igniter") do
      Mix.shell().info("Igniter is already in the project.")
      contents
    else
      add_igniter_dep(contents, version_requirement)
    end
  end

  defp add_igniter_dep(contents, version_requirement) do
    if String.contains?(contents, "defp deps do\n    []") do
      String.replace(
        contents,
        "defp deps do\n    []",
        "defp deps do\n    [{:igniter, #{version_requirement}, optional: true, runtime: false]"
      )
    else
      String.replace(
        contents,
        "defp deps do\n    [\n",
        "defp deps do\n    [\n      {:igniter, #{version_requirement}, optional: true, runtime: false},\n"
      )
    end
  end
end
