defmodule MvWeb.MembershipFeeTypeLive.Form do
  @moduledoc """
  LiveView form for creating and editing membership fee types (Admin).

  ## Features
  - Create new membership fee types
  - Edit existing membership fee types (name, amount, description - NOT interval)
  - Amount change warning modal (shows impact on members)
  - Interval field grayed out on edit

  ## Permissions
  - Admin only
  """
  use MvWeb, :live_view

  import MvWeb.LiveHelpers, only: [current_actor: 1, submit_form: 3]

  require Ash.Query

  alias Mv.Membership.Member
  alias Mv.MembershipFees
  alias Mv.MembershipFees.MembershipFeeType
  alias MvWeb.Helpers.MembershipFeeHelpers

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_user={@current_user}>
      <.header>
        {@page_title}
        <:subtitle>
          {gettext("Use this form to manage membership fee types in your database.")}
        </:subtitle>
      </.header>

      <.form
        class="max-w-xl"
        for={@form}
        id="membership-fee-type-form"
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:name]} type="text" label={gettext("Name")} required />

        <.input
          field={@form[:amount]}
          label={gettext("Amount")}
          required
          phx-debounce="blur"
        />

        <div class="form-control">
          <label class="label" for="membership-fee-type-form_interval">
            <span class="label-text font-semibold">
              {gettext("Interval")}
              <span
                :if={is_nil(@membership_fee_type)}
                class="text-red-700 tooltip tooltip-right"
                data-tip={gettext("This field cannot be empty")}
              >
                *
              </span>
            </span>
          </label>
          <select
            class={[
              "select select-bordered w-full",
              @form.errors[:interval] && "select-error"
            ]}
            disabled={!is_nil(@membership_fee_type)}
            name="membership_fee_type[interval]"
            id="membership-fee-type-form_interval"
            required={is_nil(@membership_fee_type)}
            aria-label={gettext("Interval")}
          >
            <option value="">{gettext("Select interval")}</option>
            <option
              value="monthly"
              selected={@form[:interval].value == :monthly || @form[:interval].value == "monthly"}
            >
              {gettext("Monthly")}
            </option>
            <option
              value="quarterly"
              selected={@form[:interval].value == :quarterly || @form[:interval].value == "quarterly"}
            >
              {gettext("Quarterly")}
            </option>
            <option
              value="half_yearly"
              selected={
                @form[:interval].value == :half_yearly || @form[:interval].value == "half_yearly"
              }
            >
              {gettext("Half-yearly")}
            </option>
            <option
              value="yearly"
              selected={@form[:interval].value == :yearly || @form[:interval].value == "yearly"}
            >
              {gettext("Yearly")}
            </option>
          </select>
          <%= if @form.errors[:interval] do %>
            <%= for error <- List.wrap(@form.errors[:interval]) do %>
              <% {msg, _opts} = if is_tuple(error), do: error, else: {error, []} %>
              <p class="mt-1.5 flex gap-2 items-center text-sm text-error">
                <.icon name="hero-exclamation-circle" class="size-5" />
                {msg}
              </p>
            <% end %>
          <% end %>
          <%= if !is_nil(@membership_fee_type) do %>
            <label class="label">
              <span class="label-text-alt text-base-content/60">
                {gettext("Interval cannot be changed after creation.")}
              </span>
            </label>
          <% end %>
        </div>

        <.input
          field={@form[:description]}
          type="textarea"
          label={gettext("Description")}
          rows="3"
        />

        <div class="mt-4">
          <.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
            {gettext("Save Membership Fee Type")}
          </.button>
          <.button navigate={return_path(@return_to, @membership_fee_type)} type="button">
            {gettext("Cancel")}
          </.button>
        </div>
      </.form>

      <%!-- Amount Change Warning Modal --%>
      <%= if @show_amount_warning do %>
        <dialog id="amount-warning-modal" class="modal modal-open">
          <div class="modal-box">
            <h2 class="text-lg font-bold">{gettext("Change Amount?")}</h2>
            <div class="py-4 space-y-4">
              <div class="alert alert-warning">
                <.icon name="hero-exclamation-triangle" class="size-5" />
                <div>
                  <p class="font-semibold">
                    {gettext("Changing the amount will affect %{count} member(s).",
                      count: @affected_member_count
                    )}
                  </p>
                  <p class="mt-2 text-sm">
                    {gettext("Future unpaid cycles will be regenerated with the new amount.")}
                  </p>
                  <p class="mt-2 text-sm">
                    {gettext("Already paid cycles will remain with the old amount.")}
                  </p>
                </div>
              </div>

              <div class="space-y-2">
                <div class="flex justify-between">
                  <span class="text-base-content/70">{gettext("Current amount")}:</span>
                  <span class="font-mono font-semibold">
                    {MembershipFeeHelpers.format_currency(@old_amount)}
                  </span>
                </div>
                <div class="flex justify-between">
                  <span class="text-base-content/70">{gettext("New amount")}:</span>
                  <span class="font-mono font-semibold text-base-content">
                    {MembershipFeeHelpers.format_currency(@new_amount)}
                  </span>
                </div>
              </div>
            </div>

            <div class="modal-action">
              <button
                type="button"
                phx-click="cancel_amount_change"
                class="btn"
              >
                {gettext("Cancel")}
              </button>
              <button
                type="button"
                phx-click="confirm_amount_change"
                class="btn btn-primary"
              >
                {gettext("Confirm Change")}
              </button>
            </div>
          </div>
        </dialog>
      <% end %>
    </Layouts.app>
    """
  end

  @impl true
  def mount(params, _session, socket) do
    actor = current_actor(socket)

    membership_fee_type =
      case params["id"] do
        nil -> nil
        id -> Ash.get!(MembershipFeeType, id, domain: MembershipFees, actor: actor)
      end

    page_title =
      if is_nil(membership_fee_type),
        do: gettext("New Membership Fee Type"),
        else: gettext("Edit Membership Fee Type")

    {:ok,
     socket
     |> assign(:return_to, return_to(params["return_to"]))
     |> assign(:membership_fee_type, membership_fee_type)
     |> assign(:page_title, page_title)
     |> assign(:show_amount_warning, false)
     |> assign(:old_amount, nil)
     |> assign(:new_amount, nil)
     |> assign(:affected_member_count, 0)
     |> assign(:pending_amount, nil)
     |> assign_form()}
  end

  defp return_to("index"), do: "index"
  defp return_to(_), do: "index"

  @impl true
  def handle_event("validate", %{"membership_fee_type" => params}, socket) do
    # Merge with existing form values to preserve unchanged fields
    # Extract values directly from form fields to get current state
    existing_values = get_existing_form_values(socket.assigns.form)

    # Merge existing values with new params (new params take precedence)
    merged_params = Map.merge(existing_values, params)

    # Convert interval string to atom if present
    merged_params =
      if Map.has_key?(merged_params, "interval") && is_binary(merged_params["interval"]) &&
           merged_params["interval"] != "" do
        Map.update!(merged_params, "interval", fn val ->
          String.to_existing_atom(val)
        end)
      else
        merged_params
      end

    # Let Ash handle validation automatically - it will validate Decimal format
    validated_form = AshPhoenix.Form.validate(socket.assigns.form, merged_params)

    # Check if amount changed on edit
    socket = check_amount_change(socket, merged_params)

    {:noreply, assign(socket, form: validated_form)}
  end

  def handle_event("cancel_amount_change", _params, socket) do
    # Reset form to original amount
    form = socket.assigns.form

    original_amount =
      if socket.assigns.membership_fee_type do
        socket.assigns.membership_fee_type.amount
      else
        Decimal.new("0")
      end

    # Update form with original amount
    updated_form =
      AshPhoenix.Form.validate(form, %{
        "amount" => Decimal.to_string(original_amount)
      })

    {:noreply,
     socket
     |> assign(:form, updated_form)
     |> assign(:show_amount_warning, false)
     |> assign(:pending_amount, nil)}
  end

  def handle_event("confirm_amount_change", _params, socket) do
    # Update form with pending amount and hide warning
    # Preserve all existing form values (name, description, etc.)
    form = socket.assigns.form
    existing_values = get_existing_form_values(form)

    updated_form =
      if socket.assigns.pending_amount do
        # Merge existing values with confirmed amount to preserve all fields
        merged_params = Map.put(existing_values, "amount", socket.assigns.pending_amount)
        AshPhoenix.Form.validate(form, merged_params)
      else
        form
      end

    {:noreply,
     socket
     |> assign(:form, updated_form)
     |> assign(:show_amount_warning, false)
     |> assign(:pending_amount, nil)}
  end

  def handle_event("save", %{"membership_fee_type" => params}, socket) do
    # If amount warning was shown but not confirmed, don't save
    if socket.assigns.show_amount_warning do
      {:noreply, put_flash(socket, :error, gettext("Please confirm the amount change first"))}
    else
      actor = current_actor(socket)

      case submit_form(socket.assigns.form, params, actor) do
        {:ok, membership_fee_type} ->
          notify_parent({:saved, membership_fee_type})

          socket =
            socket
            |> put_flash(:info, gettext("Membership fee type saved successfully"))
            |> push_navigate(to: return_path(socket.assigns.return_to, membership_fee_type))

          {:noreply, socket}

        {:error, form} ->
          {:noreply, assign(socket, form: form)}
      end
    end
  end

  @spec notify_parent(any()) :: any()
  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})

  @spec assign_form(Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t()
  defp assign_form(%{assigns: %{membership_fee_type: membership_fee_type}} = socket) do
    form =
      if membership_fee_type do
        AshPhoenix.Form.for_update(
          membership_fee_type,
          :update,
          domain: MembershipFees,
          as: "membership_fee_type"
        )
      else
        AshPhoenix.Form.for_create(
          MembershipFeeType,
          :create,
          domain: MembershipFees,
          as: "membership_fee_type"
        )
      end

    assign(socket, form: to_form(form))
  end

  # Helper to extract existing form values to preserve them when only one field changes
  defp get_existing_form_values(form) do
    # Extract values directly from form fields to get current state
    # This ensures we get the actual current values, not just initial params
    %{}
    |> extract_form_value(form, :name, &to_string/1)
    |> extract_form_value(form, :amount, &format_amount_value/1)
    |> extract_form_value(form, :interval, &format_interval_value/1)
    |> extract_form_value(form, :description, &to_string/1)
  end

  # Helper to extract a single form field value
  defp extract_form_value(acc, form, field, formatter) do
    if form[field] && form[field].value do
      Map.put(acc, to_string(field), formatter.(form[field].value))
    else
      acc
    end
  end

  # Formats amount value (Decimal or string) to string
  defp format_amount_value(%Decimal{} = amount), do: Decimal.to_string(amount, :normal)
  defp format_amount_value(value) when is_binary(value), do: value
  defp format_amount_value(value), do: to_string(value)

  # Formats interval value (atom or string) to string
  defp format_interval_value(value) when is_atom(value), do: Atom.to_string(value)
  defp format_interval_value(value) when is_binary(value), do: value
  defp format_interval_value(value), do: to_string(value)

  @spec return_path(String.t(), MembershipFeeType.t() | nil) :: String.t()
  defp return_path("index", _membership_fee_type), do: ~p"/membership_fee_types"

  @spec get_affected_member_count(String.t(), Mv.Accounts.User.t() | nil) :: non_neg_integer()
  # Checks if amount changed and updates socket assigns accordingly
  defp check_amount_change(socket, params) do
    if socket.assigns.membership_fee_type && Map.has_key?(params, "amount") do
      # Get current amount from form and new amount from params
      current_form_amount = get_existing_form_values(socket.assigns.form)["amount"]
      new_amount_str = params["amount"]

      # Only check amount change if amount field is actually being changed in this validation
      # This prevents re-triggering the warning when other fields (name, description) are edited
      if current_form_amount != new_amount_str do
        handle_amount_change(socket, new_amount_str, socket.assigns.membership_fee_type.amount)
      else
        # Amount didn't change in this validation - keep current warning state
        # If warning was already confirmed (pending_amount is nil and show_amount_warning is false), keep it hidden
        # If warning is shown but not confirmed, keep it shown
        socket
      end
    else
      socket
    end
  end

  # Handles amount change detection and warning assignment
  defp handle_amount_change(socket, new_amount_str, old_amount) do
    case Decimal.parse(new_amount_str) do
      {new_amount, _} when is_struct(new_amount, Decimal) ->
        if Decimal.compare(new_amount, old_amount) != :eq do
          show_amount_warning(socket, old_amount, new_amount, new_amount_str)
        else
          hide_amount_warning(socket)
        end

      :error ->
        hide_amount_warning(socket)
    end
  end

  # Shows amount change warning with affected member count
  # Only calculates count if warning is being shown for the first time (false -> true)
  defp show_amount_warning(socket, old_amount, new_amount, new_amount_str) do
    # Only calculate count if warning is not already shown (optimization)
    affected_count =
      if socket.assigns.show_amount_warning do
        # Warning already shown, reuse existing count
        socket.assigns.affected_member_count
      else
        # Warning being shown for first time, calculate count
        get_affected_member_count(socket.assigns.membership_fee_type.id, current_actor(socket))
      end

    socket
    |> assign(:show_amount_warning, true)
    |> assign(:old_amount, old_amount)
    |> assign(:new_amount, new_amount)
    |> assign(:affected_member_count, affected_count)
    |> assign(:pending_amount, new_amount_str)
  end

  # Hides amount change warning
  defp hide_amount_warning(socket) do
    socket
    |> assign(:show_amount_warning, false)
    |> assign(:pending_amount, nil)
  end

  defp get_affected_member_count(fee_type_id, actor) do
    case Ash.count(Member |> Ash.Query.filter(membership_fee_type_id == ^fee_type_id),
           actor: actor
         ) do
      {:ok, count} -> count
      _ -> 0
    end
  end
end
