defmodule Mv.Membership.Setting.Changes.UpdateSingleMemberFieldVisibility do
  @moduledoc """
  Ash change that atomically updates a single field in the member_field_visibility JSONB map.

  This change uses PostgreSQL's jsonb_set function to atomically update a single key
  in the JSONB map, preventing lost updates in concurrent scenarios.

  ## Arguments
  - `field` - The member field name as a string (e.g., "street", "house_number")
  - `show_in_overview` - Boolean value indicating visibility

  ## Example
      settings
      |> Ash.Changeset.for_update(:update_single_member_field_visibility,
        %{},
        arguments: %{field: "street", show_in_overview: false}
      )
      |> Ash.update(domain: Mv.Membership)
  """
  use Ash.Resource.Change

  alias Ash.Error.Invalid
  alias Ecto.Adapters.SQL
  require Logger

  def change(changeset, _opts, _context) do
    with {:ok, field} <- get_and_validate_field(changeset),
         {:ok, show_in_overview} <- get_and_validate_boolean(changeset, :show_in_overview) do
      add_after_action(changeset, field, show_in_overview)
    else
      {:error, updated_changeset} -> updated_changeset
    end
  end

  defp get_and_validate_field(changeset) do
    case Ash.Changeset.get_argument(changeset, :field) do
      nil ->
        {:error,
         add_error(changeset,
           field: :member_field_visibility,
           message: "field argument is required"
         )}

      field ->
        valid_fields = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)

        if field in valid_fields do
          {:ok, field}
        else
          {:error,
           add_error(
             changeset,
             field: :member_field_visibility,
             message: "Invalid member field: #{field}"
           )}
        end
    end
  end

  defp get_and_validate_boolean(changeset, arg_name) do
    case Ash.Changeset.get_argument(changeset, arg_name) do
      nil ->
        {:error,
         add_error(
           changeset,
           field: :member_field_visibility,
           message: "#{arg_name} argument is required"
         )}

      value when is_boolean(value) ->
        {:ok, value}

      _ ->
        {:error,
         add_error(
           changeset,
           field: :member_field_visibility,
           message: "#{arg_name} must be a boolean"
         )}
    end
  end

  defp add_error(changeset, opts) do
    Ash.Changeset.add_error(changeset, opts)
  end

  defp add_after_action(changeset, field, show_in_overview) do
    # Use after_action to execute atomic SQL update
    Ash.Changeset.after_action(changeset, fn _changeset, settings ->
      # Use PostgreSQL jsonb_set for atomic update
      # jsonb_set(target, path, new_value, create_missing?)
      # path is an array: ['field_name']
      # new_value must be JSON: to_jsonb(boolean)
      sql = """
      UPDATE settings
      SET member_field_visibility = jsonb_set(
        COALESCE(member_field_visibility, '{}'::jsonb),
        ARRAY[$1::text],
        to_jsonb($2::boolean),
        true
      )
      WHERE id = $3
      RETURNING member_field_visibility
      """

      # Convert UUID string to binary for PostgreSQL
      uuid_binary = Ecto.UUID.dump!(settings.id)

      case SQL.query(Mv.Repo, sql, [field, show_in_overview, uuid_binary]) do
        {:ok, %{rows: [[updated_jsonb] | _]}} ->
          updated_visibility = normalize_jsonb_result(updated_jsonb)

          # Update the settings struct with the new visibility
          updated_settings = %{settings | member_field_visibility: updated_visibility}
          {:ok, updated_settings}

        {:ok, %{rows: []}} ->
          {:error,
           Invalid.exception(
             field: :member_field_visibility,
             message: "Settings not found"
           )}

        {:error, error} ->
          Logger.error("Failed to atomically update member_field_visibility: #{inspect(error)}")

          {:error,
           Invalid.exception(
             field: :member_field_visibility,
             message: "Failed to update visibility"
           )}
      end
    end)
  end

  defp normalize_jsonb_result(updated_jsonb) do
    case updated_jsonb do
      map when is_map(map) ->
        # Convert atom keys to strings if needed
        Enum.reduce(map, %{}, fn
          {k, v}, acc when is_atom(k) -> Map.put(acc, Atom.to_string(k), v)
          {k, v}, acc -> Map.put(acc, k, v)
        end)

      binary when is_binary(binary) ->
        case Jason.decode(binary) do
          {:ok, decoded} when is_map(decoded) ->
            decoded

          # Not a map after decode
          {:ok, _} ->
            %{}

          {:error, reason} ->
            Logger.warning("Failed to decode JSONB: #{inspect(reason)}")
            %{}
        end

      _ ->
        Logger.warning("Unexpected JSONB format: #{inspect(updated_jsonb)}")
        %{}
    end
  end
end
