defmodule Soap.Response.Parser do
  @moduledoc """
  Provides a functions for parse an XML-like response body.
  """

  import SweetXml, only: [xpath: 2, sigil_x: 2]

  @soap_version_namespaces %{
    "1.1" => :"http://schemas.xmlsoap.org/soap/envelope/",
    "1.2" => :"http://www.w3.org/2003/05/soap-envelope"
  }
  @doc """
  Executing with XML response body.

  If a list is empty then `parse/1` returns full parsed response structure into map.
  """
  @spec parse(String.t(), atom()) :: map()
  def parse(xml_response, :fault) do
    fault_tag = get_fault_tag(xml_response)

    xml_response
    |> xpath(~x"//#{fault_tag}/*"l)
    |> parse_elements()
  end

  def parse(xml_response, _response_type) do
    body_tag = get_body_tag(xml_response)

    xml_response
    |> xpath(~x"//#{body_tag}/*"l)
    |> parse_elements()
  end

  @spec parse_record(tuple()) :: map() | String.t()
  defp parse_record({:xmlElement, tag_name, _, _, _, _, _, attributes, elements, _, _, _}) do
    attributes_map = parse_attributes(attributes)
    element_content = parse_elements(elements)

    case {attributes_map, element_content} do
      {attrs, content} when map_size(attrs) == 0 ->
        %{tag_name => content}

      {attrs, content} when map_size(content) == 0 ->
        %{tag_name => attrs}

      {attrs, content} when is_map(content) ->
        %{tag_name => Map.merge(attrs, content)}

      {attrs, content} ->
        %{tag_name => %{"_attributes" => attrs, "_value" => content}}
    end
  end

  defp parse_record({:xmlText, _, _, _, value, _}), do: transform_record_value(value)

  defp transform_record_value(nil), do: nil
  defp transform_record_value(value) when is_list(value), do: value |> to_string() |> String.trim()
  defp transform_record_value(value) when is_binary(value), do: value |> String.trim()

  @spec parse_elements(list() | tuple()) :: map() | String.t()
  defp parse_elements([]), do: %{}
  defp parse_elements(elements) when is_tuple(elements), do: parse_record(elements)

  defp parse_elements(elements) when is_list(elements) do
    elements
    |> Enum.map(&parse_record/1)
    |> parse_element_values()
  end

  @spec parse_element_values(list()) :: any()
  defp parse_element_values(elements) do
    cond do
      Enum.all?(elements, &is_map/1) && unique_tags?(elements) ->
        Enum.reduce(elements, &Map.merge/2)

      Enum.all?(elements, &is_map/1) ->
        elements |> Enum.map(&Map.to_list/1) |> List.flatten()

      true ->
        extract_value_from_list(elements)
    end
  end

  @spec extract_value_from_list(list()) :: any()
  defp extract_value_from_list([element]), do: element
  defp extract_value_from_list(elements), do: elements

  defp unique_tags?(elements) do
    keys =
      elements
      |> Enum.map(&Map.keys/1)
      |> List.flatten()

    Enum.uniq(keys) == keys
  end

  defp get_envelope_namespace(xml_response) do
    env_namespace = @soap_version_namespaces[soap_version()]

    xml_response
    |> xpath(~x"//namespace::*"l)
    |> Enum.find(fn {_, _, _, _, namespace_url} -> namespace_url == env_namespace end)
    |> elem(3)
  end

  defp get_fault_tag(xml_response) do
    xml_response
    |> get_envelope_namespace()
    |> List.to_string()
    |> apply_namespace_to_tag("Fault")
  end

  defp get_body_tag(xml_response) do
    xml_response
    |> get_envelope_namespace()
    |> List.to_string()
    |> apply_namespace_to_tag("Body")
  end

  defp apply_namespace_to_tag("", tag), do: tag
  defp apply_namespace_to_tag(env_namespace, tag), do: env_namespace <> ":" <> tag

  defp soap_version, do: Application.fetch_env!(:soap, :globals)[:version]

  @spec parse_attributes(list()) :: map()
  defp parse_attributes(attributes) do
    attributes
    |> Enum.map(fn {:xmlAttribute, name, _, _, _, _, _, _, value, _} ->
      {name, transform_record_value(value)}
    end)
    |> Enum.into(%{})
  end
end
