defmodule Committee.Hooks do
  @moduledoc """
  Helper functions for handling git hooks.
  """

  @hooks Committee.__hooks__()
  @target_path ".git/hooks"
  @identifier "# committee"

  @doc """
  Create the hooks files, backing up existing ones.
  """
  def create_hooks(hooks \\ @hooks) when is_list(hooks) do
    for_each_hook(hooks, &setup_hook/2)
  end

  defp setup_hook(file, hook) when hook in @hooks do
    if File.exists?(file) do
      content = File.read!(file)

      cond do
        not generated_by_committee?(content) ->
          :ok = File.rename(file, "#{file}.old")

          Mix.shell().info(
            "Existing #{hook} not generated by Committee. Renamed to #{file}.old.."
          )

          create_hook(file, hook)

        not current_version?(content) ->
          :ok = File.rename(file, "#{file}.old")

          Mix.shell().info(
            "Existing #{hook} version is outdated (#{hook_version(content)}), but current version is #{
              Committee.__version__()
            }. Updating hook.."
          )

          create_hook(file, hook)

        true ->
          Mix.shell().info(
            "Existing #{hook} version (#{hook_version(content)}) is up to date (#{
              Committee.__version__()
            }). Skipping update.."
          )
      end
    else
      create_hook(file, hook)
    end
  end

  defp create_hook(file, hook) do
    Mix.shell().info("Generating #{hook} hook..")
    File.write!(file, template_for(hook))
    make_executable(file)
  end

  @doc """
  Remove the hooks files.
  """
  def remove_hooks(hooks \\ @hooks) when is_list(hooks) do
    Mix.shell().info("Looking for hooks to delete..")
    for_each_hook(hooks, &remove_hook/2)
  end

  defp remove_hook(file, _hook) do
    if File.exists?(file) do
      Mix.shell().info("Removing #{file}..")
      File.rm!(file)
    else
      Mix.shell().info("#{file} not found..")
    end
  end

  @doc """
  Restore the hooks backups.
  """
  def restore_backups(hooks \\ @hooks) when is_list(hooks) do
    Mix.shell().info("Looking for backed up hooks to restore..")
    for_each_hook(hooks, &restore_backup/2)
  end

  defp restore_backup(file, _hook) do
    backup_file = "#{file}.old"

    if File.exists?(backup_file) do
      Mix.shell().info("Restoring #{backup_file}..")
      :ok = File.rename(backup_file, file)
    else
      Mix.shell().info("#{backup_file} not found..")
    end
  end

  # Helper function to iterate over hooks and perform some operation
  defp for_each_hook(hooks, func) do
    Enum.each(hooks, fn hook ->
      @target_path
      |> Path.join(snake_to_kebab(hook))
      |> func.(hook)
    end)
  end

  defp template_for(hook) do
    ~s"""
    #!/bin/sh

    #{@identifier}:#{Committee.__version__()}

    mix committee.runner #{hook}
    """
  end

  defp make_executable(file), do: System.cmd("chmod", ["+x", file])

  # Git hooks have to be in kebab case
  defp snake_to_kebab(string) when is_binary(string) do
    string |> String.replace("_", "-")
  end

  defp generated_by_committee?(content), do: String.contains?(content, @identifier)

  defp hook_version(content) do
    case Regex.run(~r/(?<=#{@identifier}:).+/, content) do
      nil ->
        nil

      [version] ->
        version
    end
  end

  defp current_version?(content), do: hook_version(content) == Committee.__version__()
end
