defmodule LinuxRollout.BugzillaClient do
  alias LinuxRollout.Util

  def auth_login!(base_url, login, api_key) do
    login = require_value(login, "Bugzilla login/email")
    api_key = require_value(api_key, "Bugzilla API key")

    IO.puts("Validating credentials against #{base_url} (this may take up to 60 seconds)...")
    user = validate_auth!(base_url, login, api_key)
    File.mkdir_p!(Util.bugzilla_credentials_dir())

    Util.write_json!(Util.bugzilla_credentials_file(base_url), %{
      "login" => login,
      "api_key" => api_key
    })

    display_name = user["real_name"] || user["name"] || login
    "Authenticated to #{base_url} as #{display_name}\n"
  end

  def auth_logout!(base_url) do
    path = Util.bugzilla_credentials_file(base_url)

    if File.exists?(path) do
      File.rm!(path)
      "Removed Bugzilla auth state: #{path}\n"
    else
      "No Bugzilla auth state was present.\n"
    end
  end

  def submit_request!(payload) do
    base_url = Map.fetch!(payload, "bugzilla_url")
    auth = load_auth!(base_url)
    bug_id = create_bug!(base_url, auth, payload)

    attachment_ids =
      payload
      |> Map.get("attachments", [])
      |> Enum.flat_map(fn attachment ->
        response =
          request!(base_url, "POST", "/bug/#{bug_id}/attachment", %{
            "api_key" => auth["api_key"],
            "data" => encoded_file!(Map.fetch!(attachment, "path")),
            "file_name" => Map.fetch!(attachment, "file_name"),
            "summary" => Map.fetch!(attachment, "summary"),
            "content_type" => Map.get(attachment, "content_type", "text/plain"),
            "is_patch" => false
          })

        response["ids"] || []
      end)

    %{
      "status" => "awaiting_remote_review",
      "bug_id" => bug_id,
      "bug_url" => bug_url(base_url, bug_id),
      "attachment_ids" => attachment_ids
    }
  end

  def submit_patch!(payload) do
    base_url = Map.fetch!(payload, "bugzilla_url")
    auth = load_auth!(base_url)
    bug_id = create_bug!(base_url, auth, payload)

    response =
      request!(base_url, "POST", "/bug/#{bug_id}/attachment", %{
        "api_key" => auth["api_key"],
        "data" => encoded_file!(Map.fetch!(payload, "patch_path")),
        "file_name" => Map.fetch!(payload, "patch_file_name"),
        "summary" => Map.fetch!(payload, "patch_summary"),
        "content_type" => "text/x-patch",
        "is_patch" => true,
        "comment" => "Patch prepared by the WireShare Linux rollout orchestrator."
      })

    %{
      "status" => "awaiting_remote_review",
      "bug_id" => bug_id,
      "bug_url" => bug_url(base_url, bug_id),
      "attachment_ids" => response["ids"] || []
    }
  end

  def ping!(base_url), do: request!(base_url, "GET", "/version")

  defp validate_auth!(base_url, login, api_key) do
    query = URI.encode_query(%{"api_key" => api_key})

    if login != "" do
      encoded_login = URI.encode_www_form(login)

      try do
        response = request!(base_url, "GET", "/user/#{encoded_login}?#{query}")
        users = response["users"] || []

        if users != [] do
          List.first(users)
        else
          fallback_validate_auth!(base_url, login, query)
        end
      rescue
        error in RuntimeError ->
          if String.contains?(Exception.message(error), "404") do
            fallback_validate_auth!(base_url, login, query)
          else
            reraise error, __STACKTRACE__
          end
      end
    else
      fallback_validate_auth!(base_url, login, query)
    end
  end

  defp fallback_validate_auth!(base_url, login, query) do
    response = request!(base_url, "GET", "/user/1?#{query}")

    case response["users"] || [] do
      [] -> %{"name" => login}
      [user | _] -> user
    end
  end

  defp load_auth!(base_url) do
    path = Util.bugzilla_credentials_file(base_url)

    case Util.read_json(path) do
      {:ok, auth} ->
        auth

      {:error, _reason} ->
        raise "Missing Bugzilla credentials for #{base_url}. Run `scripts/release_linux_rollout.sh auth login --target <target>` first."
    end
  end

  defp create_bug!(base_url, auth, payload) do
    response =
      request!(base_url, "POST", "/bug", %{
        "api_key" => Map.fetch!(auth, "api_key"),
        "product" => Map.fetch!(payload, "product"),
        "component" => Map.fetch!(payload, "component"),
        "summary" => Map.fetch!(payload, "summary"),
        "version" => Map.fetch!(payload, "version"),
        "description" => Map.fetch!(payload, "description"),
        "platform" => Map.fetch!(payload, "platform"),
        "severity" => Map.fetch!(payload, "severity"),
        "op_sys" => Map.get(payload, "op_sys", "All")
      })

    case response["id"] do
      bug_id when is_integer(bug_id) -> bug_id
      _ -> raise "Bugzilla did not return a bug id: #{inspect(response)}"
    end
  end

  defp request!(base_url, method, path, payload \\ nil) do
    body = if payload, do: IO.iodata_to_binary(:json.encode(payload)), else: nil

    args =
      [
        "-fsSL",
        "--connect-timeout",
        "20",
        "--max-time",
        "60",
        "-X",
        method,
        "-H",
        "Accept: application/json"
      ]
      |> maybe_append_body_args(payload)
      |> Kernel.++([rest_base(base_url) <> path])

    case Util.run("curl", args, input: body) do
      {:ok, output} ->
        try do
          :json.decode(output)
        rescue
          ArgumentError -> raise "Bugzilla returned invalid JSON"
        end

      {:error, %{output: output}} ->
        raise "Bugzilla request failed: #{String.trim(output)}"
    end
  end

  defp maybe_append_body_args(args, nil), do: args

  defp maybe_append_body_args(args, _payload) do
    args ++ ["-H", "Content-Type: application/json", "--data-binary", "@-"]
  end

  defp encoded_file!(path), do: path |> File.read!() |> Base.encode64()

  defp require_value(value, label) when value in [nil, ""], do: raise("#{label} is required")
  defp require_value(value, _label), do: value

  defp rest_base(base_url) do
    base = normalize_base_url(base_url)

    cond do
      String.ends_with?(base, "/rest") -> base
      String.ends_with?(base, "/rest.cgi") -> base
      true -> base <> "/rest"
    end
  end

  defp bug_url(base_url, bug_id), do: "#{normalize_base_url(base_url)}/show_bug.cgi?id=#{bug_id}"
  defp normalize_base_url(base_url), do: String.trim_trailing(base_url, "/")
end
