defmodule Igniter.Installer.TaskHelpers do
  @moduledoc false
  @apps [:logger, :public_key, :ssl, :inets, :eex]
  @tasks [
    "igniter.install": %{
      mfa: {Igniter.CopiedTasks, :install, []}
    },
    "igniter.add": %{
      mfa: {Igniter.CopiedTasks, :add, []}
    },
    "igniter.remove": %{
      mfa: {Igniter.CopiedTasks, :remove, []}
    },
    "igniter.upgrade": %{
      mfa: {Igniter.CopiedTasks, :upgrade, []}
    },
    "igniter.apply_upgrades": %{
      mfa: {Igniter.CopiedTasks, :apply_upgrades, []}
    }
  ]

  def tasks do
    for {task_name, config} <- @tasks do
      task_name = to_string(task_name)
      {task_name, Map.put(config, :module, mod_for_task(task_name))}
    end
  end

  defp mod_for_task(task_name) do
    task_name
    |> String.split(".")
    |> Enum.map(&Macro.camelize/1)
    |> then(&[Mix.Tasks | &1])
    |> Module.concat()
  end

  def copy_docs do
    for {task_name, _} <- tasks() do
      {short_doc_output, 0} =
        System.cmd("mix", ["help", "--search", task_name], cd: "..")

      short_doc =
        short_doc_output
        |> String.split("\n")
        |> Enum.find(&String.starts_with?(&1, "mix #{task_name} "))
        |> String.split("#", parts: 2)
        |> Enum.at(1)
        |> String.trim()

      {long_doc_output, 0} = System.cmd("mix", ["help", task_name], cd: "..")

      long_doc =
        long_doc_output
        |> String.split("\n")
        |> Enum.slice(3..-3//1)
        |> Enum.join("\n")
        |> String.trim()

      File.mkdir_p!("priv/docs/#{task_name}")
      File.write!("priv/docs/#{task_name}/short.txt", short_doc)
      File.write!("priv/docs/#{task_name}/long.txt", long_doc)
    end
  end

  def long_doc(task_name) do
    path = "priv/docs/#{task_name}/long.txt"

    if File.exists?(path) do
      File.read!(path)
    else
      "unknown"
    end
  end

  def short_doc(task_name) do
    path = "priv/docs/#{task_name}/short.txt"

    if File.exists?(path) do
      File.read!(path)
    else
      "unknown"
    end
  end

  def wrap_task(task_name, {module, fun, args}, argv) do
    prevent_infinite_loops!(argv)
    load_deps(argv)
    ensure_apps_started!()
    update_igniter(argv)

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

    if Code.ensure_loaded?(module) do
      apply(module, fun, [argv] ++ args)
    else
      if File.exists?("mix.exs") do
        ensure_igniter_and_install(task_name, argv)
      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
  end

  defp ensure_igniter_and_install(task_name, argv) do
    cleanup? = ensure_igniter!()

    if !Process.get(:registered_on_exit?) do
      Process.put(:registered_on_exit?, true)

      System.at_exit(fn _ ->
        cleanup(cleanup?, argv)
      end)
    end

    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.reenable(task_name)
    Mix.Task.run(task_name, argv ++ ["--igniter-repeat"])
  end

  defp cleanup(cleanup?, argv) do
    if cleanup? do
      Igniter.Installer.Loading.with_spinner(
        "cleaning up",
        fn ->
          apply(Igniter.CopiedTasks, :remove, [["igniter", "--yes"]])
        end,
        verbose?: "--verbose" in argv
      )
    end
  end

  defp ensure_igniter! do
    contents =
      "mix.exs"
      |> File.read!()

    new_contents =
      contents
      |> add_igniter_dep()

    if contents == new_contents do
      contents
    else
      Igniter.Installer.Loading.with_spinner(
        "temporarily adding igniter",
        fn ->
          new_contents
          |> Code.format_string!()
        end
      )
    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})
    end

    File.write!("mix.exs", new_contents)
    new_contents != contents
  end

  defp load_deps(argv) do
    message =
      cond do
        "--igniter-repeat" in argv ->
          "setting up igniter"

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

        true ->
          "checking for igniter in project"
      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
    )
  end

  defp update_igniter(argv) do
    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
  end

  defp ensure_apps_started! do
    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

    Application.ensure_all_started(:rewrite)
  end

  defp prevent_infinite_loops!(argv) do
    if Enum.count(argv, &(&1 == "--igniter-repeat")) > 1 do
      Mix.shell().error("""
      There was a problem installing or setting up igniter.

      Run your command again with `--verbose` to see more detail.
      """)

      exit({:shutdown, 1})
    end
  end

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

    if String.contains?(contents, "{:igniter") do
      contents
    else
      Mix.Tasks.Igniter.New.add_igniter_dep(contents, version_requirement)
    end
  end
end
