defmodule Mv.Membership.Setting.Changes.UpdateSingleMemberField do
  @moduledoc """
  Ash change that atomically updates visibility and required for a single member field.

  Updates both `member_field_visibility` and `member_field_required` JSONB maps
  in one SQL UPDATE to avoid lost updates when saving from the settings UI.

  ## Arguments
  - `field` - The member field name as a string (e.g., "street", "first_name")
  - `show_in_overview` - Boolean value indicating visibility in member overview
  - `required` - Boolean value indicating whether the field is required in member forms

  ## Example
      settings
      |> Ash.Changeset.for_update(:update_single_member_field, %{},
        arguments: %{field: "first_name", show_in_overview: true, required: true}
      )
      |> 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),
         {:ok, required} <- get_and_validate_boolean(changeset, :required) do
      add_after_action(changeset, field, show_in_overview, required)
    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: :field,
           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: :field,
             message: "Invalid member field: #{field}"
           )}
        end
    end
  end

  defp get_and_validate_boolean(changeset, :show_in_overview = arg_name) do
    do_validate_boolean(changeset, arg_name, :show_in_overview)
  end

  defp get_and_validate_boolean(changeset, :required = arg_name) do
    do_validate_boolean(changeset, arg_name, :member_field_required)
  end

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

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

      _ ->
        {:error,
         add_error(
           changeset,
           field: error_field,
           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, required) do
    Ash.Changeset.after_action(changeset, fn _changeset, settings ->
      # Update both JSONB columns in one statement
      sql = """
      UPDATE settings
      SET
        member_field_visibility = jsonb_set(
          COALESCE(member_field_visibility, '{}'::jsonb),
          ARRAY[$1::text],
          to_jsonb($2::boolean),
          true
        ),
        member_field_required = jsonb_set(
          COALESCE(member_field_required, '{}'::jsonb),
          ARRAY[$1::text],
          to_jsonb($3::boolean),
          true
        ),
        updated_at = (now() AT TIME ZONE 'utc')
      WHERE id = $4
      RETURNING member_field_visibility, member_field_required
      """

      uuid_binary = Ecto.UUID.dump!(settings.id)

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

          updated_settings = %{
            settings
            | member_field_visibility: vis,
              member_field_required: req
          }

          {:ok, updated_settings}

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

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

          {:error,
           Invalid.exception(
             field: :member_field_required,
             message: "Failed to update member field settings"
           )}
      end
    end)
  end

  defp normalize_jsonb_result(updated_jsonb) do
    case updated_jsonb do
      map when is_map(map) ->
        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

          {:ok, _} ->
            %{}

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

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