defmodule Hex.Utils do
  @moduledoc false

  def package_name("hexpm", package), do: package
  def package_name(nil, package), do: package
  def package_name(repo, package), do: "#{repo}/#{package}"

  def safe_deserialize_erlang("") do
    nil
  end

  def safe_deserialize_erlang(binary) do
    case safe_binary_to_term(binary) do
      {:ok, term} ->
        term

      :error ->
        Mix.raise("Received malformed erlang from Hex API")
    end
  rescue
    ArgumentError ->
      Mix.raise("Received malformed erlang from Hex API")
  end

  def safe_serialize_erlang(term) do
    binarify(term)
    |> :erlang.term_to_binary()
  end

  def safe_binary_to_term!(binary, opts \\ []) do
    case safe_binary_to_term(binary, opts) do
      {:ok, term} ->
        term

      :error ->
        raise ArgumentError, "unsafe terms"
    end
  end

  def safe_binary_to_term(binary, opts \\ [])

  def safe_binary_to_term(binary, opts) when is_binary(binary) do
    term = :erlang.binary_to_term(binary, opts)
    safe_terms(term)
    {:ok, term}
  catch
    :throw, :safe_terms ->
      :error
  end

  defp safe_terms(list) when is_list(list) do
    safe_list(list)
  end

  defp safe_terms(tuple) when is_tuple(tuple) do
    safe_tuple(tuple, tuple_size(tuple))
  end

  defp safe_terms(map) when is_map(map) do
    fun = fn key, value, acc ->
      safe_terms(key)
      safe_terms(value)
      acc
    end

    :maps.fold(fun, map, map)
  end

  defp safe_terms(other)
       when is_atom(other) or is_number(other) or is_bitstring(other) or is_pid(other) or
              is_reference(other) do
    other
  end

  defp safe_terms(_other) do
    throw(:safe_terms)
  end

  defp safe_list([]), do: :ok

  defp safe_list([h | t]) when is_list(t) do
    safe_terms(h)
    safe_list(t)
  end

  defp safe_list([h | t]) do
    safe_terms(h)
    safe_terms(t)
  end

  defp safe_tuple(_tuple, 0), do: :ok

  defp safe_tuple(tuple, n) do
    safe_terms(:erlang.element(n, tuple))
    safe_tuple(tuple, n - 1)
  end

  def truncate(string, options \\ []) do
    length = options[:length] || 50
    omission = options[:omission] || "..."

    cond do
      not String.valid?(string) ->
        string

      String.length(string) < length ->
        string

      true ->
        String.slice(string, 0, length) <> omission
    end
  end

  # Unicode bidirectional formatting characters that can reorder displayed text.
  @bidi_controls [0x061C, 0x202A, 0x202B, 0x202C, 0x202D, 0x202E, 0x2066, 0x2067, 0x2068, 0x2069]

  # Escapes characters that could manipulate the terminal when printing an
  # untrusted string. Printable characters, newlines and tabs are kept as-is;
  # ANSI escapes, bidirectional overrides and other control characters are
  # rendered as visible escape sequences.
  def escape_terminal(binary) when is_binary(binary) do
    binary
    |> String.to_charlist()
    |> Enum.map_join(&escape_terminal_char/1)
  end

  defp escape_terminal_char(?\n), do: "\n"
  defp escape_terminal_char(?\t), do: "\t"
  defp escape_terminal_char(?\e), do: "\\e"
  defp escape_terminal_char(?\r), do: "\\r"
  defp escape_terminal_char(?\b), do: "\\b"
  defp escape_terminal_char(?\a), do: "\\a"
  defp escape_terminal_char(cp) when cp in @bidi_controls, do: unicode_escape(cp)
  defp escape_terminal_char(cp) when cp < 0x20, do: hex_escape(cp)
  defp escape_terminal_char(0x7F), do: hex_escape(0x7F)
  defp escape_terminal_char(cp) when cp in 0x80..0x9F, do: unicode_escape(cp)
  defp escape_terminal_char(cp), do: <<cp::utf8>>

  defp unicode_escape(cp), do: "\\u{" <> Integer.to_string(cp, 16) <> "}"

  defp hex_escape(cp) do
    "\\x" <> (cp |> Integer.to_string(16) |> String.pad_leading(2, "0"))
  end

  def binarify(term, opts \\ [])

  def binarify(binary, _opts) when is_binary(binary) do
    binary
  end

  def binarify(number, _opts) when is_number(number) do
    number
  end

  def binarify(atom, _opts) when is_nil(atom) or is_boolean(atom) do
    atom
  end

  def binarify(atom, _opts) when is_atom(atom) do
    Atom.to_string(atom)
  end

  def binarify(list, opts) when is_list(list) do
    for(elem <- list, do: binarify(elem, opts))
  end

  def binarify(tuple, opts) when is_tuple(tuple) do
    for(elem <- Tuple.to_list(tuple), do: binarify(elem, opts))
    |> List.to_tuple()
  end

  def binarify(map, opts) when is_map(map) do
    if Keyword.get(opts, :maps, true) do
      for(elem <- map, into: %{}, do: binarify(elem, opts))
    else
      for(elem <- map, do: binarify(elem, opts))
    end
  end

  def print_error_result({:error, reason}) do
    Hex.Shell.info(inspect(reason))
  end

  def print_error_result({:ok, {status, _headers, nil}}) do
    print_http_code(status)
  end

  def print_error_result({:ok, {status, _headers, ""}}) do
    print_http_code(status)
  end

  def print_error_result({:ok, {_status, _headers, body}}) when is_binary(body) do
    Hex.Shell.info(body)
  end

  def print_error_result({:ok, {status, _headers, body}}) when is_map(body) do
    message = body["message"]
    errors = body["errors"]

    if message do
      Hex.Shell.info(message)
    end

    if errors do
      pretty_errors(errors)
    end

    unless message || errors do
      print_http_code(status)
      Hex.Shell.info(body)
    end
  end

  defp pretty_errors(errors, depth \\ 0) do
    Enum.each(errors, fn
      {key, map} when is_map(map) ->
        Hex.Shell.info(indent(depth) <> key <> ":")
        pretty_errors(map, depth + 1)

      {key, value} ->
        message = pretty_error_message(value, depth)
        Hex.Shell.info(indent(depth) <> key <> ": " <> message)
    end)
  end

  defp pretty_error_message(message, depth) do
    if message =~ "\n" do
      message =
        message
        |> String.trim()
        |> String.replace("\n", "\n" <> indent(depth + 1))

      "\n" <> indent(depth + 1) <> message
    else
      message
    end
  end

  defp print_http_code(code), do: Hex.Shell.info(pretty_http_code(code))

  defp pretty_http_code(401), do: "Authentication failed (401)"
  defp pretty_http_code(403), do: "Forbidden (403)"
  defp pretty_http_code(404), do: "Entity not found (404)"
  defp pretty_http_code(422), do: "Validation failed (422)"
  defp pretty_http_code(code), do: "HTTP status code: #{code}"

  defp indent(0), do: "  "
  defp indent(depth), do: "  " <> indent(depth - 1)

  def hexdocs_url(organization, package)
      when organization in ["hexpm", nil],
      do: "https://#{package_subdomain(package)}.hexdocs.pm"

  def hexdocs_url(organization, package),
    do: "https://#{organization}.hexorgs.pm/#{package}"

  def hexdocs_url(organization, package, version)
      when organization in ["hexpm", nil],
      do: "https://#{package_subdomain(package)}.hexdocs.pm/#{version}"

  def hexdocs_url(organization, package, version),
    do: "https://#{organization}.hexorgs.pm/#{package}/#{version}"

  def hexdocs_module_url(organization, package, module)
      when organization in ["hexpm", nil],
      do: "https://#{package_subdomain(package)}.hexdocs.pm/#{module}.html"

  def hexdocs_module_url(organization, package, module),
    do: "https://#{organization}.hexorgs.pm/#{package}/#{module}.html"

  def hexdocs_module_url(organization, package, version, module)
      when organization in ["hexpm", nil],
      do: "https://#{package_subdomain(package)}.hexdocs.pm/#{version}/#{module}.html"

  def hexdocs_module_url(organization, package, version, module),
    do: "https://#{organization}.hexorgs.pm/#{package}/#{version}/#{module}.html"

  # Hex package names allow underscores. RFC 1123 hostname labels and
  # RFC 6125 wildcard SAN matching don't, so Fastly returns 421 for
  # underscore subdomains under *.hexdocs.pm. Map `_` -> `-` here; the
  # Fastly Compute subdomain handler reverses it for the bucket lookup.
  defp package_subdomain(package), do: String.replace(package, "_", "-")

  def package_retirement_reason(:RETIRED_OTHER), do: "other"
  def package_retirement_reason(:RETIRED_INVALID), do: "invalid"
  def package_retirement_reason(:RETIRED_SECURITY), do: "security"
  def package_retirement_reason(:RETIRED_DEPRECATED), do: "deprecated"
  def package_retirement_reason(:RETIRED_RENAMED), do: "renamed"
  def package_retirement_reason(other), do: other

  def package_retirement_message(%{reason: reason_code, message: message}) do
    "(#{package_retirement_reason(reason_code)}) #{message}"
  end

  def package_retirement_message(%{reason: reason_code}) do
    "(#{package_retirement_reason(reason_code)})"
  end

  def advisory_severity(:SEVERITY_NONE), do: "NONE"
  def advisory_severity(:SEVERITY_LOW), do: "LOW"
  def advisory_severity(:SEVERITY_MEDIUM), do: "MEDIUM"
  def advisory_severity(:SEVERITY_HIGH), do: "HIGH"
  def advisory_severity(:SEVERITY_CRITICAL), do: "CRITICAL"
  def advisory_severity(other), do: other

  def advisory_severity_color(:SEVERITY_LOW), do: :yellow
  def advisory_severity_color(:SEVERITY_MEDIUM), do: :yellow
  def advisory_severity_color(:SEVERITY_HIGH), do: :red
  def advisory_severity_color(:SEVERITY_CRITICAL), do: [:bright, :red]
  def advisory_severity_color(_), do: []

  def format_advisory_ansi(%{id: id, summary: summary} = advisory, line_prefix \\ "") do
    severity =
      case advisory do
        %{severity: s} ->
          [" ", advisory_severity_color(s), "(#{advisory_severity(s)})", :reset]

        _ ->
          []
      end

    aliases =
      case advisory do
        %{aliases: [_ | _] = aliases} ->
          ids = Enum.map_join(aliases, ", ", &alias_id/1)
          ["\n", line_prefix, "aka: ", ids]

        _ ->
          []
      end

    url =
      case advisory do
        %{html_url: url} -> ["\n", line_prefix, :underline, url, :reset]
        _ -> []
      end

    [id, severity, aliases, "\n", line_prefix, summary, url]
  end

  defp alias_id(%{id: id}), do: id
  defp alias_id(id) when is_binary(id), do: id

  # From https://github.com/fishcakez/dialyze/blob/6698ae582c77940ee10b4babe4adeff22f1b7779/lib/mix/tasks/dialyze.ex#L168
  def otp_version do
    major = :erlang.system_info(:otp_release) |> List.to_string()
    vsn_file = Path.join([:code.root_dir(), "releases", major, "OTP_VERSION"])

    try do
      {:ok, contents} = File.read(vsn_file)
      String.split(contents, "\n", trim: true)
    else
      [full] -> full
      _ -> major
    catch
      :error, _ -> major
    end
  end

  def windows_repo_path_fix(path) do
    case :os.type() do
      {:win32, _name} -> String.replace(path, ":", "-")
      {_family, _name} -> path
    end
  end

  def lock(tuple) when elem(tuple, 0) == :hex do
    if tuple_size(tuple) > 8 and Hex.Server.should_warn_lock_version?() do
      Hex.Shell.warn(
        "The mix.lock file was generated with a newer version of Hex. Update " <>
          "your client by running `mix local.hex` to avoid losing data."
      )
    end

    destructure(
      [:hex, name, version, inner_checksum, managers, deps, repo, outer_checksum],
      Tuple.to_list(tuple)
    )

    %{
      name: to_string(name),
      version: version,
      inner_checksum: inner_checksum,
      outer_checksum: outer_checksum,
      managers: managers,
      deps: lock_deps(deps),
      repo: repo || "hexpm"
    }
  end

  def lock(_) do
    nil
  end

  defp lock_deps(nil) do
    nil
  end

  defp lock_deps(deps) do
    Enum.map(deps, fn {app, req, opts} ->
      opts =
        opts
        |> Keyword.put_new(:repo, "hexpm")
        |> Keyword.update!(:hex, &to_string/1)

      {app, req, opts}
    end)
  end

  @doc """
  Returns the appropriate command for opening a file or URL with the system's default handler.

  Returns a tuple of {command, args, options} suitable for use with System.cmd/3.
  """
  def open_cmd(path) do
    case :os.type() do
      {:win32, _} ->
        {"cmd", win_cmd_args(path)}

      {:unix, :darwin} ->
        {"open", [path]}

      {:unix, _} ->
        cond do
          System.find_executable("xdg-open") ->
            {"xdg-open", [path]}

          # When inside WSL
          System.find_executable("cmd.exe") ->
            {"cmd.exe", win_cmd_args(path)}

          true ->
            {"open", [path]}
        end
    end
  end

  defp win_cmd_args(path) do
    ["/c", "start", String.replace(path, "&", "^&")]
  end

  @doc """
  Opens a file or URL with the system's default handler.

  In test environment, sends a message instead of actually executing the command.
  """
  def system_open(path) do
    path
    |> open_cmd()
    |> system_cmd()
  end

  if Mix.env() == :test do
    defp system_cmd({cmd, args}) do
      send(self(), {:hex_system_cmd, cmd, args})
    end
  else
    defp system_cmd({cmd, args}) do
      System.cmd(cmd, args)
    end
  end
end
