defmodule PrimerLive.Helpers.FormHelpers do
  @moduledoc false
  # Helper functions for components that interact with forms and changesets.

  import Phoenix.HTML.Form
  use PhoenixHTMLHelpers

  # Map of input name to atom value that can be used by Phoenix.HTML.Form to render the appropriate input element.
  @text_input_types %{
    "color" => :color_input,
    "date" => :date_input,
    "datetime-local" => :datetime_local_input,
    "email" => :email_input,
    "file" => :file_input,
    "hidden" => :hidden_input,
    "number" => :number_input,
    "password" => :password_input,
    "range" => :range_input,
    "search" => :search_input,
    "telephone" => :telephone_input,
    "text" => :text_input,
    "textarea" => :textarea,
    "time" => :time_input,
    "url" => :url_input
  }

  def text_input_types, do: @text_input_types

  @doc """
  Validates if the input is a form.
  """
  def form?(%Phoenix.HTML.Form{}), do: true

  # Handle other types of forms
  def form?(%_{} = maybe_form)
      when not is_nil(maybe_form.__struct__) do
    form_type = "#{maybe_form.__struct__}"

    String.contains?(form_type, "AshPhoenix.Form")
  end

  def form?(_), do: false

  @doc """
  Returns a `PrimerLive.FieldState` struct to facilitate display logic in component rendering functions.

      iex> PrimerLive.Helpers.FormHelpers.field_state(:f, :first_name, nil, nil)
      %PrimerLive.FieldState{valid?: false, changeset: nil, message: nil, field_errors: [], caption: nil}

      # If validation_message_fn returns a string it will be added to FieldState, regardless of the changeset action value:
      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> update_changeset = %Ecto.Changeset{changeset |
      ...>   action: :update,
      ...>   changes: %{first_name: "annette"}
      ...> }
      ...> PrimerLive.Helpers.FormHelpers.field_state(
      ...>   %Phoenix.HTML.Form{
      ...>     impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>     source: update_changeset
      ...>   },
      ...>   :first_name,
      ...>   fn _field_state -> "always" end,
      ...>   nil
      ...> )
      %PrimerLive.FieldState{valid?: false, required?: true, changeset: update_changeset, message: "always", field_errors: ["can't be blank"]}

      # If changeset action is :validate and no validation_message_fn is provided, the default field error is added to FieldState:
      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> validate_changeset = %Ecto.Changeset{changeset |
      ...>   action: :validate,
      ...>   changes: %{first_name: "annette"}
      ...> }
      ...> PrimerLive.Helpers.FormHelpers.field_state(
      ...>   %Phoenix.HTML.Form{
      ...>     impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>     source: validate_changeset
      ...>   },
      ...>   :first_name,
      ...>   nil,
      ...>   nil
      ...> )
      %PrimerLive.FieldState{valid?: false, required?: true, changeset: validate_changeset, message: "can't be blank", field_errors: ["can't be blank"]}

      # Custom success message: if changeset action is :update and a validation_message_fn is provided, the resulting message is added to FieldState:
      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> update_changeset = %Ecto.Changeset{changeset |
      ...>   action: :update,
      ...>   changes: %{first_name: "annette"},
      ...>   errors: [],
      ...>   valid?: true,
      ...> }
      ...> PrimerLive.Helpers.FormHelpers.field_state(
      ...>   %Phoenix.HTML.Form{
      ...>     impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>     source: update_changeset,
      ...>   },
      ...>   :first_name,
      ...>   fn field_state -> if field_state.valid?, do: "Great!" end,
      ...>   nil
      ...> )
      %PrimerLive.FieldState{valid?: true, required?: true, changeset: update_changeset, message: "Great!", field_errors: []}

      # Caption: return a caption if changeset is nil
      iex> PrimerLive.Helpers.FormHelpers.field_state(nil, nil, nil, fn _field_state -> "Caption" end)
      %PrimerLive.FieldState{required?: false, valid?: false, changeset: nil, message: nil, field_errors: [], caption: "Caption"}

      # Caption: return a static caption
      iex> PrimerLive.Helpers.FormHelpers.field_state(:f, :first_name, nil, "Caption")
      %PrimerLive.FieldState{required?: false, valid?: false, changeset: nil, message: nil, field_errors: [], caption: "Caption"}

      # Caption: return a caption regardless the state
      iex> PrimerLive.Helpers.FormHelpers.field_state(:f, :first_name, nil, fn _field_state -> "Caption" end)
      %PrimerLive.FieldState{required?: false, valid?: false, changeset: nil, message: nil, field_errors: [], caption: "Caption"}

      # Caption: return a caption dependent on the state
      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> validate_changeset = %Ecto.Changeset{changeset |
      ...>   action: :validate,
      ...>   changes: %{first_name: "annette"}
      ...> }
      ...> PrimerLive.Helpers.FormHelpers.field_state(
      ...>   %Phoenix.HTML.Form{
      ...>     impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>     source: validate_changeset
      ...>   },
      ...>   :first_name,
      ...>   nil,
      ...>   fn field_state -> if !field_state.valid?, do: "Please select your availability" end
      ...> )
      %PrimerLive.FieldState{caption: "Please select your availability", changeset: validate_changeset, field_errors: ["can't be blank"], ignore_errors?: false, message: "can't be blank", message_id: nil, required?: true, valid?: false}
  """
  def field_state(maybe_form, field, validation_message_fn, caption_fn) do
    form = get_form(maybe_form)
    changeset = get_form_changeset(form)

    get_field_state_for_changeset(
      form,
      changeset,
      %PrimerLive.FieldState{},
      field,
      validation_message_fn,
      caption_fn
    )
  end

  defp get_field_state_for_changeset(
         _form,
         changeset,
         field_state,
         _field,
         _validation_message_fn,
         caption_fn
       )
       when is_nil(changeset) do
    caption = get_caption(field_state, caption_fn)
    %{field_state | caption: caption}
  end

  defp get_field_state_for_changeset(
         form,
         changeset,
         field_state,
         field,
         validation_message_fn,
         caption_fn
       ) do
    with field_errors <- get_field_errors(changeset, field),
         required? <- get_field_required(form, changeset, field),
         valid? <- Enum.empty?(field_errors),
         field_state <- %{
           field_state
           | valid?: valid?,
             ignore_errors?: is_nil(changeset.action) && !valid?,
             field_errors: field_errors,
             changeset: changeset,
             required?: required?
         },
         message <-
           get_message(changeset.action, field_errors, field_state, validation_message_fn),
         caption <-
           get_caption(field_state, caption_fn) do
      %{field_state | message: message, caption: caption}
    end
  end

  # changeset.action is nil
  defp get_message(nil, _field_errors, _field_state, _), do: nil

  # validation_message_fn is nil, field_errors is empty
  defp get_message(_changeset_action, field_errors, _field_state, nil) when field_errors == [],
    do: nil

  # validation_message_fn is nil, field_errors exists
  defp get_message(_changeset_action, field_errors, _field_state, nil) do
    [field_error | _rest] = field_errors
    field_error
  end

  # validation_message_fn exists
  defp get_message(_changeset_action, _field_errors, field_state, validation_message_fn) do
    case :erlang.fun_info(validation_message_fn)[:arity] do
      0 -> validation_message_fn.()
      1 -> validation_message_fn.(field_state)
      _ -> nil
    end
  end

  defp get_caption(_field_state, nil), do: nil
  defp get_caption(_field_state, []), do: nil
  defp get_caption(_field_state, caption_str) when is_binary(caption_str), do: caption_str
  defp get_caption(_field_state, caption_slot) when is_list(caption_slot), do: nil

  defp get_caption(field_state, caption_fn) do
    case :erlang.fun_info(caption_fn)[:arity] do
      0 -> caption_fn.()
      1 -> caption_fn.(field_state)
      _ -> nil
    end
  end

  @doc """
  Extracts the form struct from either a form struct or a nested struct that contains a form.

      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> PrimerLive.Helpers.FormHelpers.get_form(%Phoenix.HTML.Form{
      ...>   impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>   source: changeset
      ...> })
      %Phoenix.HTML.Form{source: changeset, impl: Phoenix.HTML.FormData.Ecto.Changeset}

      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> PrimerLive.Helpers.FormHelpers.get_form(%Phoenix.HTML.Form{
      ...>   impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>   source: %Phoenix.HTML.Form{
      ...>     impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>     source: %Ecto.Changeset{changeset |
      ...>       action: :update,
      ...>       changes: %{first_name: "annette"}
      ...>     }
      ...>   }
      ...> })
      %Phoenix.HTML.Form{source: %Ecto.Changeset{changeset | action: :update, changes: %{first_name: "annette"}}, impl: Phoenix.HTML.FormData.Ecto.Changeset}

      iex> PrimerLive.Helpers.FormHelpers.get_form(%PrimerLive.Helpers.TestForm{
      ...>   impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>   source: %PrimerLive.Helpers.TestForm{
      ...>     impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>     source: %Phoenix.HTML.Form{
      ...>       impl: Phoenix.HTML.FormData.Ecto.Changeset
      ...>     }
      ...>   }
      ...> })
      %Phoenix.HTML.Form{source: nil, impl: Phoenix.HTML.FormData.Ecto.Changeset, id: nil, name: nil, data: nil, hidden: [], params: %{}, errors: [], options: [], index: nil, action: nil}

      iex> PrimerLive.Helpers.FormHelpers.get_form(:form)
      nil
  """
  # Handle different kinds of forms
  def get_form(%_{source: _} = maybe_form) when not is_nil(maybe_form.source.__struct__) do
    form_type = "#{maybe_form.source.__struct__}"

    if form_type |> String.contains?("Form") do
      # Find a possible nested form
      get_form(maybe_form.source)
    else
      maybe_form
    end
  end

  def get_form(%Phoenix.HTML.Form{} = form) do
    form
  end

  def get_form(_), do: nil

  @doc """
  Gets the form name - either an atom or a string.

      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> PrimerLive.Helpers.FormHelpers.get_form_name(%Phoenix.HTML.Form{
      ...>   impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>   name: :profile,
      ...>   source: changeset
      ...> })
      :profile

      iex> PrimerLive.Helpers.FormHelpers.get_form_name(%PrimerLive.Helpers.TestForm{
      ...>   impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>   source: %PrimerLive.Helpers.TestForm{
      ...>     name: "test",
      ...>     source: %Phoenix.HTML.Form{
      ...>       name: "profile",
      ...>     }
      ...>   }
      ...> })
      "profile"

      iex> PrimerLive.Helpers.FormHelpers.get_form_name(:profile)
      :profile

      iex> PrimerLive.Helpers.FormHelpers.get_form_name("profile")
      "profile"

      iex> PrimerLive.Helpers.FormHelpers.get_form_name(1)
      nil

      iex> PrimerLive.Helpers.FormHelpers.get_form_name(nil)
      nil
  """
  def get_form_name(%_{source: _} = maybe_form) when not is_nil(maybe_form.source.__struct__) do
    if "#{maybe_form.source.__struct__}" |> String.contains?("Form") do
      get_form_name(maybe_form.source)
    else
      get_form_name(maybe_form.name)
    end
  end

  def get_form_name(name) when is_atom(name), do: name
  def get_form_name(name) when is_binary(name), do: name

  def get_form_name(%Phoenix.HTML.Form{} = form) do
    form.name
  end

  def get_form_name(_), do: nil

  @doc """
  Returns the form changeset extracted from `form.source`.
  Returns nil if no changeset is found.

      iex> PrimerLive.Helpers.FormHelpers.get_form_changeset(nil)
      nil

      iex> PrimerLive.Helpers.FormHelpers.get_form_changeset(%{})
      nil

      iex> PrimerLive.Helpers.FormHelpers.get_form_changeset(%Phoenix.HTML.Form{})
      nil

      iex> PrimerLive.Helpers.FormHelpers.get_form_changeset(%Phoenix.HTML.Form{
      ...>   source: %Ecto.Changeset{}
      ...> })
      %Ecto.Changeset{action: nil, changes: %{}, errors: [], data: nil, valid?: false}
  """

  # Handle other types of changesets
  def get_form_changeset(%_{source: _} = form_or_changeset)
      when not is_nil(form_or_changeset.source.__struct__) do
    form_or_changeset_type = "#{form_or_changeset.source.__struct__}"

    if String.contains?(form_or_changeset_type, "Ash.Changeset") do
      ash_changeset = form_or_changeset
      _get_ash_framework_changeset(ash_changeset)
    else
      form_or_changeset.source
    end
  end

  def get_form_changeset(%Phoenix.HTML.Form{source: _} = form) do
    form.source
  end

  def get_form_changeset(_form), do: nil

  # Handle other types of changesets: Ash Framework
  def _get_ash_framework_changeset(ash_changeset), do: ash_changeset.source

  @doc """
  Returns all errors for a given field from a changeset.

      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> PrimerLive.Helpers.FormHelpers.get_field_errors(
      ...>   %Ecto.Changeset{
      ...>     changeset | action: :update, errors: []
      ...>   },
      ...>   :first_name
      ...> )
      []

      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> PrimerLive.Helpers.FormHelpers.get_field_errors(
      ...>   %Ecto.Changeset{
      ...>     changeset | action: :update,
      ...>       errors: [
      ...>         first_name: {"can't be blank", [validation: :required]},
      ...>         work_experience: {"invalid value", [validation: :required]}
      ...>       ],
      ...>   },
      ...>   :first_name
      ...> )
      ["can't be blank"]

      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> PrimerLive.Helpers.FormHelpers.get_field_errors(
      ...>   %Ecto.Changeset{
      ...>     changeset | action: :update,
      ...>       errors: [
      ...>         first_name: {"should be at most %{count} character(s)",
      ...>         [count: 255, validation: :length, kind: :max, type: :string]}
      ...>       ],
      ...>   },
      ...>   :first_name
      ...> )
      ["should be at most 255 character(s)"]

      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> PrimerLive.Helpers.FormHelpers.get_field_errors(
      ...>   %Ecto.Changeset{
      ...>     changeset | action: :update,
      ...>       errors: [
      ...>         first_name: {"can't be blank", [validation: :required]},
      ...>         work_experience: {"invalid value", [validation: :required]}
      ...>       ],
      ...>   },
      ...>   :work_experience
      ...> )
      ["invalid value"]
  """
  def get_field_errors(%Ecto.Changeset{} = changeset, field) do
    changeset.errors
    |> Enum.filter(fn {error_field, _content} -> error_field == field end)
    |> Enum.map(fn {_error_field, {content, details}} ->
      interpolate_error_details(content, details)
    end)
  end

  # Handle other types of changesets
  def get_field_errors(%_{} = maybe_changeset, field)
      when not is_nil(maybe_changeset.__struct__) do
    changeset_type = "#{maybe_changeset.__struct__}"

    if String.contains?(changeset_type, "Ash.Changeset") do
      ash_changeset = maybe_changeset
      _get_ash_framework_field_errors(ash_changeset, field)
    else
      []
    end
  end

  def get_field_errors(_, _), do: []

  # Handle other types of changesets: Ash Framework
  def _get_ash_framework_field_errors(ash_changeset, field) do
    if Map.has_key?(ash_changeset.attributes, field) do
      ash_changeset.errors
      |> Enum.filter(fn ash_error -> ash_error.field == field end)
      |> Enum.map(&map_ash_error/1)
    else
      []
    end
  end

  # sobelow_skip ["RCE.CodeModule"]
  defp map_ash_error(ash_error) do
    code_to_eval =
      if Kernel.function_exported?(Ash.Error.Changes.Required, :id, 1),
        do: "Ash.ErrorKind.message(e)",
        else: "Exception.message(e)"

    {message, _binding} =
      Code.eval_string(code_to_eval, [e: ash_error],
        file: __ENV__.file,
        line: __ENV__.line
      )

    message
  end

  @doc """
  Returns the required state for a given field from a changeset.

      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> PrimerLive.Helpers.FormHelpers.get_field_required(
      ...>   %Phoenix.HTML.Form{
      ...>     impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>     data: nil,
      ...>     source: changeset
      ...>  },
      ...>  changeset,
      ...>  :first_name
      ...> )
      true

      iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
      ...> PrimerLive.Helpers.FormHelpers.get_field_required(
      ...>   %Phoenix.HTML.Form{
      ...>     impl: Phoenix.HTML.FormData.Ecto.Changeset,
      ...>     data: nil,
      ...>     source: changeset
      ...>  },
      ...>  changeset,
      ...>  :work_experience
      ...> )
      false
  """
  def get_field_required(%Phoenix.HTML.Form{} = form, %Ecto.Changeset{} = _changeset, field) do
    input_validations(form, field)[:required] == true
  end

  # Handle other types of changesets
  def get_field_required(form, %_{} = maybe_changeset, field)
      when not is_nil(maybe_changeset.__struct__) do
    changeset_type = "#{maybe_changeset.__struct__}"

    if String.contains?(changeset_type, "Ash.Changeset") do
      ash_changeset = maybe_changeset
      get_ash_framework_field_required(form, ash_changeset, field)
    else
      false
    end
  end

  def get_field_required(_, _), do: false

  # Handle other types of changesets: Ash Framework
  defp get_ash_framework_field_required(form, ash_changeset, field) do
    Phoenix.HTML.FormData.input_validations(form, ash_changeset, field)[:required] == true
  end

  @doc """
  Get the input type atom from a name, e.g. "search" returns :search_input

  ## Tests

      iex> PrimerLive.Helpers.FormHelpers.text_input_type_as_atom("x")
      :text_input

      iex> PrimerLive.Helpers.FormHelpers.text_input_type_as_atom("text")
      :text_input

      iex> PrimerLive.Helpers.FormHelpers.text_input_type_as_atom("color")
      :color_input

  """
  def text_input_type_as_atom(type_name) do
    input_type = Map.get(text_input_types(), type_name)

    if is_nil(input_type), do: :text_input, else: input_type
  end

  defp interpolate_error_details(content, details) do
    Enum.reduce(details, content, fn {key, value}, content ->
      String.replace(content, "%{#{key}}", "#{inspect(value)}")
    end)
  end
end
