defmodule Mv.MembershipFees.Changes.ValidateSameInterval do
  @moduledoc """
  Validates that membership fee type changes only allow same-interval types.

  Prevents changing from yearly to monthly, etc. (MVP constraint).

  ## Usage

  In a Member action:

      update :update_member do
        # ...
        change Mv.MembershipFees.Changes.ValidateSameInterval
      end

  The change module only executes when `membership_fee_type_id` is being changed.
  If the new type has a different interval than the current type, a validation error is returned.
  """
  use Ash.Resource.Change

  @impl true
  def change(changeset, _opts, context) do
    if changing_membership_fee_type?(changeset) do
      validate_interval_match(changeset, context)
    else
      changeset
    end
  end

  # Check if membership_fee_type_id is being changed
  defp changing_membership_fee_type?(changeset) do
    Ash.Changeset.changing_attribute?(changeset, :membership_fee_type_id)
  end

  # Validate that the new type has the same interval as the current type
  defp validate_interval_match(changeset, context) do
    current_type_id = get_current_type_id(changeset)
    new_type_id = get_new_type_id(changeset)
    actor = Map.get(context || %{}, :actor)

    cond do
      # If no current type, allow any change (first assignment)
      is_nil(current_type_id) ->
        changeset

      # If new type is nil, reject the change (membership_fee_type_id is required)
      is_nil(new_type_id) ->
        add_nil_type_error(changeset)

      # Both types exist - validate intervals match
      true ->
        validate_intervals_match(changeset, current_type_id, new_type_id, actor)
    end
  end

  # Validates that intervals match when both types exist
  defp validate_intervals_match(changeset, current_type_id, new_type_id, actor) do
    case get_intervals(current_type_id, new_type_id, actor) do
      {:ok, current_interval, new_interval} ->
        if current_interval == new_interval do
          changeset
        else
          add_interval_mismatch_error(changeset, current_interval, new_interval)
        end

      {:error, reason} ->
        # Fail closed: If we can't load the types, reject the change
        # This prevents inconsistent data states
        add_type_validation_error(changeset, reason)
    end
  end

  # Get current type ID from changeset data
  defp get_current_type_id(changeset) do
    case changeset.data do
      %{membership_fee_type_id: type_id} -> type_id
      _ -> nil
    end
  end

  # Get new type ID from changeset
  defp get_new_type_id(changeset) do
    case Ash.Changeset.fetch_change(changeset, :membership_fee_type_id) do
      {:ok, type_id} -> type_id
      :error -> nil
    end
  end

  # Get intervals for both types (actor required for authorization when resource has policies)
  defp get_intervals(current_type_id, new_type_id, actor) do
    alias Mv.MembershipFees.MembershipFeeType

    opts = if actor, do: [actor: actor], else: []

    case {
      Ash.get(MembershipFeeType, current_type_id, opts),
      Ash.get(MembershipFeeType, new_type_id, opts)
    } do
      {{:ok, current_type}, {:ok, new_type}} ->
        {:ok, current_type.interval, new_type.interval}

      _ ->
        {:error, :type_not_found}
    end
  end

  # Add validation error for interval mismatch
  defp add_interval_mismatch_error(changeset, current_interval, new_interval) do
    current_interval_name = format_interval(current_interval)
    new_interval_name = format_interval(new_interval)

    message =
      "Cannot change membership fee type: current type uses #{current_interval_name} interval, " <>
        "new type uses #{new_interval_name} interval. Only same-interval changes are allowed."

    Ash.Changeset.add_error(
      changeset,
      field: :membership_fee_type_id,
      message: message
    )
  end

  # Add validation error when types cannot be loaded
  defp add_type_validation_error(changeset, _reason) do
    message =
      "Could not validate membership fee type intervals. " <>
        "The current or new membership fee type no longer exists. " <>
        "This may indicate a data consistency issue."

    Ash.Changeset.add_error(
      changeset,
      field: :membership_fee_type_id,
      message: message
    )
  end

  # Add validation error when trying to set membership_fee_type_id to nil
  defp add_nil_type_error(changeset) do
    message = "Cannot remove membership fee type. A membership fee type is required."

    Ash.Changeset.add_error(
      changeset,
      field: :membership_fee_type_id,
      message: message
    )
  end

  # Format interval atom to human-readable string
  defp format_interval(:monthly), do: "monthly"
  defp format_interval(:quarterly), do: "quarterly"
  defp format_interval(:half_yearly), do: "half-yearly"
  defp format_interval(:yearly), do: "yearly"
  defp format_interval(interval), do: to_string(interval)
end
