defprotocol LiveVue.Encoder do
  @moduledoc """
  Protocol for encoding values to JSON for LiveVue.

  This protocol is used to safely transform structs into plain maps before
  calculating JSON patches. It ensures that struct fields are explicitly
  exposed and prevents accidental exposure of sensitive data.

  It's very similar to Jason.Encoder, but it's converting structs to maps instead of strings.

  ## Deriving

  The protocol allows leveraging Elixir's `@derive` feature to simplify protocol
  implementation in trivial cases. Accepted options are:

  * `:only` - encodes only values of specified keys.
  * `:except` - encodes all struct fields except specified keys.

  By default all keys except the `:__struct__` key are encoded.

  ## Example

  Let's assume a presence of the following struct:

      defmodule User do
        defstruct [:name, :email, :password]
      end

  If we were to call `@derive LiveVue.Encoder` just before `defstruct`, an
  implementation would be generated that encodes all fields except `:__struct__`:

      defmodule User do
        @derive LiveVue.Encoder
        defstruct [:name, :email, :password]
      end

  If we called `@derive {LiveVue.Encoder, only: [:name, :email]}`, only the
  specified fields would be encoded:

      defmodule User do
        @derive {LiveVue.Encoder, only: [:name, :email]}
        defstruct [:name, :email, :password]
      end

  If we called `@derive {LiveVue.Encoder, except: [:password]}`, all fields
  except the specified ones would be encoded:

      defmodule User do
        @derive {LiveVue.Encoder, except: [:password]}
        defstruct [:name, :email, :password]
      end

  ## Deriving outside of the module

  If you don't own the struct you want to encode, you may use Protocol.derive/3 placed outside of any module:

  Protocol.derive(LiveVue.Encoder, User, only: [...])

  ## Custom implementations

  You may define your own implementation for the struct:

  defimpl LiveVue.Encoder, for: User do
    def encode(struct, opts) do
      struct
      |> Map.take([:first, :second])
      |> LiveVue.Encoder.encode(opts)
    end
  end
  """

  @type t :: term
  @type opts :: Keyword.t()
  @fallback_to_any true

  @doc """
  Encodes a value to one of the primitive types.
  """
  @spec encode(t, opts) :: any()
  def encode(value, opts \\ [])
end

# Primitive types that are already JSON serializable
defimpl LiveVue.Encoder, for: Integer do
  def encode(value, _opts), do: value
end

defimpl LiveVue.Encoder, for: Float do
  def encode(value, _opts), do: value
end

defimpl LiveVue.Encoder, for: BitString do
  def encode(value, _opts), do: value
end

defimpl LiveVue.Encoder, for: Atom do
  def encode(atom, _opts), do: atom
end

# Complex types that need recursive encoding
defimpl LiveVue.Encoder, for: List do
  def encode(list, opts) do
    Enum.map(list, &LiveVue.Encoder.encode(&1, opts))
  end
end

defimpl LiveVue.Encoder, for: Map do
  def encode(map, opts) do
    Map.new(map, fn {key, value} ->
      {key, LiveVue.Encoder.encode(value, opts)}
    end)
  end
end

# Date and time types - Jason encoder handles them
defimpl LiveVue.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do
  def encode(value, _opts) do
    # otherwise diff is comparing tuples, and that's not what we want
    @for.to_iso8601(value)
  end
end

defimpl LiveVue.Encoder, for: Phoenix.HTML.Form do
  def encode(%Phoenix.HTML.Form{} = form, opts) do
    LiveVue.Encoder.encode(
      %{
        name: form.name,
        values: encode_form_values(form, opts),
        errors: encode_form_errors(form) || %{},
        valid: get_form_validity(form)
      },
      opts
    )
  rescue
    error in [Protocol.UndefinedError] ->
      error =
        if Code.ensure_loaded?(Ecto) do
          case error.value do
            %Ecto.Association.NotLoaded{} ->
              Map.update!(error, :description, fn description ->
                [first | rest] = String.split(description, "\n\n")

                addition = """
                To prevent this error from happening in forms, you can explicitly encode form using LiveVue.Encoder.encode(form, nillify_not_loaded: true) option.
                """

                Enum.join([first | [addition | rest]], "\n\n")
              end)

            _val ->
              error
          end
        else
          error
        end

      reraise error, __STACKTRACE__
  end

  defp get_form_validity(%{source: %{valid?: valid}}), do: valid
  defp get_form_validity(_), do: true

  if Code.ensure_loaded?(Ecto) do
    @relations [:embed, :assoc]

    # for changeset, we need to collect actual values from the changeset
    # - if there are changes, we need to use changed values
    # - if there are no changes, we should use params (possibly invalid, to reflect intermediate state)
    # - if there are not changes and no params, we should use the data
    # it's a bit tricky, because we need to traverse embeds and assocs
    defp collect_changeset_values(%Ecto.Changeset{} = source, opts) do
      data = Map.new(source.types, fn {field, type} -> {field, get_field_value(source, field, type, opts)} end)

      result = if is_struct(source.data), do: Map.merge(source.data, data), else: data

      # Filter out __meta__ field from Ecto schemas
      Map.delete(result, :__meta__)
    end

    defp get_field_value(source, field, {tag, %{cardinality: :one}}, opts) when tag in @relations do
      case Map.fetch(source.changes, field) do
        {:ok, nil} ->
          nil

        {:ok, %Ecto.Changeset{} = changeset} ->
          collect_changeset_values(changeset, opts)

        # there are no changes underneath, so we can just use the data
        :error ->
          case Map.fetch!(source.data, field) do
            %Ecto.Association.NotLoaded{} = not_loaded ->
              # I was thinking how to solve this correctly
              # but for now I think ignoring this is the best option
              if opts[:nillify_not_loaded], do: nil, else: not_loaded

            %{__meta__: _} = value ->
              Map.delete(value, :__meta__)

            value ->
              value
          end
      end
    end

    defp get_field_value(source, field, {tag, %{cardinality: :many}}, opts) when tag in @relations do
      case Map.fetch(source.changes, field) do
        {:ok, changesets} ->
          Enum.map(changesets, &collect_changeset_values(&1, opts))

        :error ->
          case Map.fetch!(source.data, field) do
            %Ecto.Association.NotLoaded{} = not_loaded ->
              # let's return nil for not loaded associations
              if opts[:nillify_not_loaded], do: nil, else: not_loaded

            [%{__meta__: _} | _] = value ->
              Enum.map(value, &Map.delete(&1, :__meta__))

            value ->
              value
          end
      end
    end

    defp get_field_value(source, field, _type, _opts) do
      Phoenix.HTML.FormData.Ecto.Changeset.input_value(source, %{params: source.params}, field)

      # # First try to get the value using the standard Phoenix input_value function
      # case Phoenix.HTML.FormData.Ecto.Changeset.input_value(source, %{params: source.params}, field) do
      #   nil ->
      #     # If that returns nil, check if there's a field with [] suffix in params
      #     # This handles the case where multiple checkbox fields have names like "preferences[]"
      #     field_with_brackets = "#{field}[]"
      #     case Map.get(source.params, field_with_brackets) do
      #       nil -> nil
      #       value -> value
      #     end
      #   value -> value
      # end
    end

    def encode_form_values(%{impl: Phoenix.HTML.FormData.Ecto.Changeset, source: source}, opts) do
      source |> collect_changeset_values(opts) |> LiveVue.Encoder.encode(opts)
    end
  end

  # Fallback for other form implementations (like to_form backed by maps)
  def encode_form_values(form, opts) do
    base_values =
      form.hidden
      |> Map.new()
      |> Map.merge(form.data)
      |> Map.merge(Map.new(form.params))

    # For non-changeset forms, we don't have type information for embeds
    # so just encode the values as-is
    LiveVue.Encoder.encode(base_values, opts)
  end

  if Code.ensure_loaded?(Ecto) do
    defp collect_changeset_errors(%Ecto.Changeset{} = changeset) do
      # Collect direct errors from this changeset
      errors = translate_errors(changeset.errors)

      # Collect errors from nested embedded changesets
      Enum.reduce(changeset.changes, errors, fn {field, value}, acc ->
        case Map.get(changeset.types, field) do
          {tag, %{cardinality: :one}} when tag in @relations ->
            # Single embedded changeset
            embed_errors = collect_changeset_errors(value)
            if embed_errors == %{}, do: acc, else: Map.put(acc, field, embed_errors)

          {tag, %{cardinality: :many}} when tag in @relations ->
            # List of embedded changesets
            list_errors =
              Enum.map(value, fn embed_changeset ->
                embed_errors = collect_changeset_errors(embed_changeset)
                if embed_errors == %{}, do: nil, else: embed_errors
              end)

            # Only include the field if there are any errors in the list
            if Enum.all?(list_errors, &is_nil/1), do: acc, else: Map.put(acc, field, list_errors)

          _ ->
            acc
        end
      end)
    end

    def encode_form_errors(%{impl: Phoenix.HTML.FormData.Ecto.Changeset} = form) do
      collect_changeset_errors(form.source)
    end
  end

  # Fallback for other form implementations (like test mocks)
  def encode_form_errors(form) do
    translate_errors(form.errors)
  end

  defp translate_errors(errors) do
    Map.new(errors, fn {field, error} -> {field, error |> List.wrap() |> Enum.map(&translate_error/1)} end)
  end

  defp translate_error({msg, opts}) do
    backend = Application.get_env(:live_vue, :gettext_backend, nil)
    count = opts[:count]

    cond do
      backend != nil and count != nil and Code.ensure_loaded?(Gettext) ->
        apply(Gettext, :dngettext, [backend, "errors", msg, msg, count, opts])

      backend != nil and Code.ensure_loaded?(Gettext) ->
        apply(Gettext, :dgettext, [backend, "errors", msg, opts])

      true ->
        Enum.reduce(opts, msg, fn {key, value}, acc ->
          String.replace(acc, "%{#{key}}", value |> List.wrap() |> Enum.map_join(", ", &to_string/1))
        end)
    end
  end
end

# Default implementation for structs - convert to map and encode recursively
defimpl LiveVue.Encoder, for: Any do
  defmacro __deriving__(module, struct, opts) do
    fields = fields_to_encode(struct, opts)

    quote do
      defimpl LiveVue.Encoder, for: unquote(module) do
        def encode(struct, opts) do
          struct
          |> Map.take(unquote(fields))
          |> LiveVue.Encoder.encode(opts)
        end
      end
    end
  end

  def encode(%{__struct__: module} = struct, _opts) do
    raise Protocol.UndefinedError,
      protocol: @protocol,
      value: struct,
      description: """
      LiveVue.Encoder protocol must always be explicitly implemented.

      It's used to encode structs to JSON for LiveVue. It's very similar to Jason.Encoder,
      but it's converting structs to maps so LiveVue can diff them correctly.

      If you own the struct, you can derive the implementation specifying \
      which fields should be encoded:

          defmodule #{inspect(module)} do
            @derive {LiveVue.Encoder, only: [...]}
            defstruct ...
          end

      If you don't own the struct you want to encode, \
      you may use Protocol.derive/3 placed outside of any module:

          Protocol.derive(LiveVue.Encoder, #{inspect(module)}, only: [...])
          Protocol.derive(LiveVue.Encoder, #{inspect(module)})

      Nothing prevents you from defining your own implementation for the struct:

      defimpl LiveVue.Encoder, for: #{inspect(module)} do
        def encode(struct, opts) do
          struct
          |> Map.take([:first, :second])
          |> LiveVue.Encoder.encode(opts)
        end
      end
      """
  end

  def encode(value, _opts) do
    # For any other type, try to pass through as-is
    # This covers things like PIDs, references, etc.
    value
  end

  defp fields_to_encode(struct, opts) do
    fields = Map.keys(struct)

    cond do
      only = Keyword.get(opts, :only) ->
        case only -- fields do
          [] ->
            only

          error_keys ->
            raise ArgumentError,
                  ":only specified keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
                    "#{inspect(fields -- [:__struct__])}"
        end

      except = Keyword.get(opts, :except) ->
        case except -- fields do
          [] ->
            fields -- [:__struct__ | except]

          error_keys ->
            raise ArgumentError,
                  ":except specified keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
                    "#{inspect(fields -- [:__struct__])}"
        end

      true ->
        fields -- [:__struct__]
    end
  end
end

# Phoenix LiveView AsyncResult struct
defimpl LiveVue.Encoder, for: Phoenix.LiveView.AsyncResult do
  def encode(%Phoenix.LiveView.AsyncResult{} = struct, opts) do
    LiveVue.Encoder.encode(
      %{
        ok: struct.ok?,
        loading: struct.loading,
        failed: encode_failed(struct.failed),
        result: struct.result
      },
      opts
    )
  end

  # Unwrap error tuples for JSON compatibility
  defp encode_failed({:error, reason}), do: reason
  defp encode_failed({:exit, reason}), do: reason
  defp encode_failed(other), do: other
end

# Phoenix LiveView Upload structs
# Explicit implementation of LiveVue.Encoder for UploadConfig
defimpl LiveVue.Encoder, for: Phoenix.LiveView.UploadConfig do
  def encode(%Phoenix.LiveView.UploadConfig{} = struct, opts) do
    errors =
      Enum.map(struct.errors, fn {key, value} ->
        %{ref: key, error: LiveVue.Encoder.encode(value, opts)}
      end)

    entries =
      Enum.map(struct.entries, fn entry ->
        encoded = LiveVue.Encoder.encode(entry, opts)
        entry_errors = errors |> Enum.filter(&(&1.ref == entry.ref)) |> Enum.map(& &1.error)
        Map.put(encoded, :errors, entry_errors)
      end)

    LiveVue.Encoder.encode(
      %{
        ref: struct.ref,
        name: struct.name,
        accept: struct.accept,
        max_entries: struct.max_entries,
        auto_upload: struct.auto_upload?,
        entries: entries,
        errors: errors
      },
      opts
    )
  end
end

# Explicit implementation of LiveVue.Encoder for UploadEntry
defimpl LiveVue.Encoder, for: Phoenix.LiveView.UploadEntry do
  def encode(%Phoenix.LiveView.UploadEntry{} = struct, opts) do
    LiveVue.Encoder.encode(
      %{
        ref: struct.ref,
        client_name: struct.client_name,
        client_size: struct.client_size,
        client_type: struct.client_type,
        progress: struct.progress,
        done: struct.done?,
        valid: struct.valid?,
        preflighted: struct.preflighted?
      },
      opts
    )
  end
end
