defmodule GuardedStruct.Derive.SanitizerDerive do
  @spec call({atom(), any()}, list(any())) :: {any(), any()}
  def call({field, input}, nil), do: {field, input}

  def call({field, input}, actions) do
    converted_input = Enum.reduce(actions, input, fn i, acc -> sanitize(i, acc) end)
    {field, converted_input}
  end

  @spec sanitize(atom() | tuple(), any()) :: any()
  def sanitize(:trim, input) do
    if is_binary(input), do: String.trim(input), else: input
  end

  def sanitize(:upcase, input) do
    if is_binary(input), do: String.upcase(input), else: input
  end

  def sanitize(:downcase, input) do
    if is_binary(input), do: String.downcase(input), else: input
  end

  def sanitize(:capitalize, input) do
    if is_binary(input), do: String.capitalize(input), else: input
  end

  if Code.ensure_loaded?(HtmlSanitizeEx) do
    def sanitize(:basic_html, input) when is_binary(input), do: HtmlSanitizeEx.basic_html(input)

    def sanitize(:html5, input) when is_binary(input), do: HtmlSanitizeEx.html5(input)

    def sanitize(:markdown_html, input) when is_binary(input),
      do: HtmlSanitizeEx.markdown_html(input)

    def sanitize(:strip_tags, input) when is_binary(input), do: HtmlSanitizeEx.strip_tags(input)

    def sanitize({:tag, type}, input) when is_binary(input) do
      sanitize(:trim, input)
      |> then(&sanitize(if(is_binary(type), do: String.to_atom(type), else: type), &1))
      |> then(&sanitize(:trim, &1))
    end

    def sanitize(:string_float, input) when is_binary(input) do
      sanitize(:strip_tags, input)
      |> Float.parse()
      |> case do
        :error -> 0.0
        {converted_float, _} -> converted_float
      end
    rescue
      _ -> 0.0
    end

    def sanitize(:string_integer, input) when is_binary(input) do
      sanitize(:strip_tags, input)
      |> Integer.parse()
      |> case do
        :error -> 0
        {converted_integer, _} -> converted_integer
      end
    rescue
      _ -> 0
    end
  else
    def sanitize(:string_float, input) when is_binary(input) do
      Float.parse(input)
      |> case do
        :error -> 0.0
        {converted_float, _} -> converted_float
      end
    rescue
      _ -> 0.0
    end

    def sanitize(:string_integer, input) when is_binary(input) do
      Integer.parse(input)
      |> case do
        :error -> 0
        {converted_integer, _} -> converted_integer
      end
    rescue
      _ -> 0
    end
  end

  def sanitize(action, input) do
    case Application.get_env(:guarded_struct, :sanitize_derive) do
      nil ->
        input

      derive_module when is_list(derive_module) ->
        custom_derive(derive_module, action, input)

      derive_module ->
        derive_module.sanitize(action, input)
    end
  rescue
    _ -> input
  end

  defp custom_derive(derive_list, action, input) do
    Enum.reduce_while(derive_list, nil, fn item, _acc ->
      case validate_pattern(item, action, input) do
        nil -> {:cont, input}
        ouput -> {:halt, if(is_nil(ouput), do: input, else: ouput)}
      end
    end)
  end

  @spec validate_pattern(module(), list(any()), any()) :: any()
  def validate_pattern(module, action, input) do
    apply(module, :sanitize, [action, input])
  rescue
    _ -> nil
  end
end
