defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDate do
  @moduledoc """
  Ash change module that automatically calculates and sets the membership_fee_start_date.

  ## Logic

  1. Only executes if `membership_fee_start_date` is not manually set
  2. Requires both `join_date` and `membership_fee_type_id` to be present
  3. Reads `include_joining_cycle` setting from global Settings
  4. Reads `interval` from the assigned `membership_fee_type`
  5. Calculates the start date:
     - If `include_joining_cycle = true`: First day of the joining cycle
     - If `include_joining_cycle = false`: First day of the next cycle after joining

  ## Usage

  In a Member action:

      create :create_member do
        # ...
        change Mv.MembershipFees.Changes.SetMembershipFeeStartDate
      end

  The change module handles all prerequisite checks internally (join_date, membership_fee_type_id).
  If any required data is missing, the changeset is returned unchanged with a warning logged.
  """
  use Ash.Resource.Change

  require Logger

  alias Mv.MembershipFees.CalendarCycles

  @impl true
  def change(changeset, _opts, context) do
    # Only calculate if membership_fee_start_date is not already set
    if has_start_date?(changeset) do
      changeset
    else
      calculate_and_set_start_date(changeset, context)
    end
  end

  # Check if membership_fee_start_date is already set (either in changeset or data)
  defp has_start_date?(changeset) do
    # Check if it's being set in this changeset
    case Ash.Changeset.fetch_change(changeset, :membership_fee_start_date) do
      {:ok, date} when not is_nil(date) ->
        true

      _ ->
        # Check if it already exists in the data (for updates)
        case changeset.data do
          %{membership_fee_start_date: date} when not is_nil(date) -> true
          _ -> false
        end
    end
  end

  defp calculate_and_set_start_date(changeset, context) do
    actor = Map.get(context || %{}, :actor)
    opts = if actor, do: [actor: actor], else: []

    with {:ok, join_date} <- get_join_date(changeset),
         {:ok, membership_fee_type_id} <- get_membership_fee_type_id(changeset),
         {:ok, interval} <- get_interval(membership_fee_type_id, opts),
         {:ok, include_joining_cycle} <- get_include_joining_cycle() do
      start_date = calculate_start_date(join_date, interval, include_joining_cycle)
      Ash.Changeset.force_change_attribute(changeset, :membership_fee_start_date, start_date)
    else
      {:error, :join_date_not_set} ->
        # Missing join_date is expected for partial creates
        changeset

      {:error, :membership_fee_type_not_set} ->
        # Missing membership_fee_type_id is expected for partial creates
        changeset

      {:error, :membership_fee_type_not_found} ->
        # This is a data integrity error - membership_fee_type_id references non-existent type
        # Return changeset error to fail the action
        Ash.Changeset.add_error(
          changeset,
          field: :membership_fee_type_id,
          message: "not found"
        )

      {:error, reason} ->
        # Log warning for other unexpected errors
        Logger.warning("Could not auto-set membership_fee_start_date: #{inspect(reason)}")
        changeset
    end
  end

  defp get_join_date(changeset) do
    # First check the changeset for changes
    case Ash.Changeset.fetch_change(changeset, :join_date) do
      {:ok, date} when not is_nil(date) ->
        {:ok, date}

      _ ->
        # Then check existing data
        case changeset.data do
          %{join_date: date} when not is_nil(date) -> {:ok, date}
          _ -> {:error, :join_date_not_set}
        end
    end
  end

  defp get_membership_fee_type_id(changeset) do
    # First check the changeset for changes
    case Ash.Changeset.fetch_change(changeset, :membership_fee_type_id) do
      {:ok, id} when not is_nil(id) ->
        {:ok, id}

      _ ->
        # Then check existing data
        case changeset.data do
          %{membership_fee_type_id: id} when not is_nil(id) -> {:ok, id}
          _ -> {:error, :membership_fee_type_not_set}
        end
    end
  end

  defp get_interval(membership_fee_type_id, opts) do
    case Ash.get(Mv.MembershipFees.MembershipFeeType, membership_fee_type_id, opts) do
      {:ok, %{interval: interval}} -> {:ok, interval}
      {:error, _} -> {:error, :membership_fee_type_not_found}
    end
  end

  defp get_include_joining_cycle do
    case Mv.Membership.get_settings() do
      {:ok, %{include_joining_cycle: include}} -> {:ok, include}
      {:error, _} -> {:ok, true}
    end
  end

  @doc """
  Calculates the membership fee start date based on join date, interval, and settings.

  ## Parameters

    - `join_date` - The date the member joined
    - `interval` - The billing interval (:monthly, :quarterly, :half_yearly, :yearly)
    - `include_joining_cycle` - Whether to include the joining cycle

  ## Returns

    The calculated start date (first day of the appropriate cycle).

  ## Examples

      iex> calculate_start_date(~D[2024-03-15], :yearly, true)
      ~D[2024-01-01]

      iex> calculate_start_date(~D[2024-03-15], :yearly, false)
      ~D[2025-01-01]

      iex> calculate_start_date(~D[2024-03-15], :quarterly, true)
      ~D[2024-01-01]

      iex> calculate_start_date(~D[2024-03-15], :quarterly, false)
      ~D[2024-04-01]

  """
  @spec calculate_start_date(Date.t(), CalendarCycles.interval(), boolean()) :: Date.t()
  def calculate_start_date(join_date, interval, include_joining_cycle) do
    if include_joining_cycle do
      # Start date is the first day of the joining cycle
      CalendarCycles.calculate_cycle_start(join_date, interval)
    else
      # Start date is the first day of the next cycle after joining
      join_cycle_start = CalendarCycles.calculate_cycle_start(join_date, interval)
      CalendarCycles.next_cycle_start(join_cycle_start, interval)
    end
  end
end
