defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
  @moduledoc """
  LiveComponent for displaying and managing membership fees for a member.

  ## Features
  - Display all membership fee cycles in a table
  - Change membership fee type (with same-interval validation)
  - Change cycle status (paid/unpaid/suspended)
  - Regenerate cycles manually
  - Delete cycles (with confirmation)
  - Edit cycle amount (with modal)
  """
  use MvWeb, :live_component

  require Ash.Query
  import MvWeb.LiveHelpers, only: [current_actor: 1]
  import MvWeb.Authorization, only: [can?: 3]

  alias Mv.Membership
  alias Mv.MembershipFees
  alias Mv.MembershipFees.MembershipFeeType
  alias Mv.MembershipFees.MembershipFeeCycle
  alias Mv.MembershipFees.CycleGenerator
  alias Mv.MembershipFees.CalendarCycles
  alias MvWeb.Helpers.MembershipFeeHelpers

  @impl true
  def render(assigns) do
    ~H"""
    <div id={@id}>
      <.section_box title={gettext("Membership Fees")}>
        <%!-- Membership Fee Type Display --%>
        <div class="mb-6">
          <label class="label">
            <span class="label-text font-semibold">{gettext("Membership Fee Type")}</span>
          </label>
          <%= if @member.membership_fee_type do %>
            <div class="flex items-center gap-2">
              <span class="font-medium">{@member.membership_fee_type.name}</span>
              <span class="text-base-content/60">
                ({MembershipFeeHelpers.format_currency(@member.membership_fee_type.amount)}, {MembershipFeeHelpers.format_interval(
                  @member.membership_fee_type.interval
                )})
              </span>
            </div>
          <% else %>
            <span class="text-base-content/60 italic">
              {gettext("No membership fee type assigned")}
            </span>
          <% end %>
        </div>

        <%!-- Vereinfacht: contact info when synced, or warning when API is configured but no contact --%>
        <%= if Mv.Config.vereinfacht_configured?() do %>
          <%= if @member.vereinfacht_contact_id do %>
            <div class="mb-4">
              <label class="label">
                <span class="label-text font-semibold">{gettext("Vereinfacht")}</span>
              </label>
              <div class="flex flex-col gap-1">
                <span class="font-mono text-sm">
                  {gettext("Contact ID: %{id}", id: @member.vereinfacht_contact_id)}
                </span>
                <.link
                  :if={Mv.Config.vereinfacht_contact_view_url(@member.vereinfacht_contact_id)}
                  href={Mv.Config.vereinfacht_contact_view_url(@member.vereinfacht_contact_id)}
                  target="_blank"
                  rel="noopener noreferrer"
                  class="link link-accent underline inline-flex items-center gap-1"
                >
                  {gettext("View contact in Vereinfacht")}
                  <.icon name="hero-arrow-top-right-on-square" class="inline-block size-4" />
                </.link>
                <div class="mt-2">
                  <span class="text-base-content/70 text-sm">{gettext("Debug:")}</span>
                  <button
                    type="button"
                    phx-click="load_vereinfacht_debug"
                    phx-value-contact_id={@member.vereinfacht_contact_id}
                    class="btn btn-sm btn-ghost ml-1"
                  >
                    {gettext("Load API response")}
                  </button>
                </div>
                <%= if @vereinfacht_debug_response do %>
                  <div
                    class="mt-2 rounded border border-base-300 bg-base-200 p-3 overflow-x-auto max-h-96 overflow-y-auto"
                    tabindex="0"
                    role="region"
                    aria-label={gettext("Vereinfacht API response")}
                  >
                    <pre class="text-xs whitespace-pre-wrap font-mono"><%= format_vereinfacht_debug_response(@vereinfacht_debug_response) %></pre>
                  </div>
                <% end %>
              </div>
            </div>
          <% else %>
            <div class="mb-4 rounded-lg border border-warning/50 bg-warning/10 p-3">
              <p class="text-warning font-medium flex items-center gap-2">
                <.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
                {gettext("No Vereinfacht contact exists for this member.")}
              </p>
              <p class="text-sm text-base-content/70 mt-1">
                {gettext(
                  "Sync this member from Settings (Vereinfacht section) or save the member again to create the contact."
                )}
              </p>
            </div>
          <% end %>
        <% end %>

        <%!-- Action Buttons (only when user has permission) --%>
        <div class="flex gap-2 mb-4">
          <.button
            :if={@member.membership_fee_type != nil and @can_create_cycle}
            phx-click="regenerate_cycles"
            phx-target={@myself}
            class={["btn btn-sm btn-outline", if(@regenerating, do: "btn-disabled", else: "")]}
            title={gettext("Generate cycles from the last existing cycle to today")}
          >
            <.icon name="hero-arrow-path" class="size-4" />
            {if(@regenerating, do: gettext("Regenerating..."), else: gettext("Regenerate Cycles"))}
          </.button>
          <.button
            :if={Enum.any?(@cycles) and @can_destroy_cycle}
            phx-click="delete_all_cycles"
            phx-target={@myself}
            class="btn btn-sm btn-error btn-outline"
            title={gettext("Delete all cycles")}
          >
            <.icon name="hero-trash" class="size-4" />
            {gettext("Delete All Cycles")}
          </.button>
          <.button
            :if={@member.membership_fee_type != nil and @can_create_cycle}
            phx-click="open_create_cycle_modal"
            phx-target={@myself}
            class="btn btn-sm btn-primary"
            title={gettext("Create a new cycle manually")}
          >
            <.icon name="hero-plus" class="size-4" />
            {gettext("Create Cycle")}
          </.button>
        </div>

        <%!-- Cycles Table --%>
        <%= if Enum.any?(@cycles) do %>
          <.table
            id="membership-fee-cycles"
            rows={@cycles}
            row_id={fn cycle -> "cycle-#{cycle.id}" end}
          >
            <:col :let={cycle} label={gettext("Cycle")}>
              {MembershipFeeHelpers.format_cycle_range(
                cycle.cycle_start,
                cycle.membership_fee_type.interval
              )}
            </:col>

            <:col :let={cycle} label={gettext("Interval")}>
              <span class="badge badge-outline">
                {MembershipFeeHelpers.format_interval(cycle.membership_fee_type.interval)}
              </span>
            </:col>

            <:col :let={cycle} label={gettext("Amount")}>
              <%= if @can_update_cycle do %>
                <span
                  class="font-mono cursor-pointer hover:text-primary"
                  phx-click="edit_cycle_amount"
                  phx-value-cycle_id={cycle.id}
                  phx-target={@myself}
                  title={gettext("Click to edit amount")}
                >
                  {MembershipFeeHelpers.format_currency(cycle.amount)}
                </span>
              <% else %>
                <span class="font-mono">{MembershipFeeHelpers.format_currency(cycle.amount)}</span>
              <% end %>
            </:col>

            <:col :let={cycle} label={gettext("Status")}>
              <% badge = MembershipFeeHelpers.status_color(cycle.status) %>
              <% icon = MembershipFeeHelpers.status_icon(cycle.status) %>
              <span class={["badge", badge]}>
                <.icon name={icon} class="size-4" />
                {format_status_label(cycle.status)}
              </span>
            </:col>

            <:action :let={cycle}>
              <div class="flex gap-1">
                <%= if @can_update_cycle do %>
                  <button
                    :if={cycle.status != :paid}
                    type="button"
                    phx-click="mark_cycle_status"
                    phx-value-cycle_id={cycle.id}
                    phx-value-status="paid"
                    phx-target={@myself}
                    class="btn btn-sm btn-success"
                    title={gettext("Mark as paid")}
                  >
                    <.icon name="hero-check-circle" class="size-4" />
                    {gettext("Paid")}
                  </button>
                  <button
                    :if={cycle.status != :suspended}
                    type="button"
                    phx-click="mark_cycle_status"
                    phx-value-cycle_id={cycle.id}
                    phx-value-status="suspended"
                    phx-target={@myself}
                    class="btn btn-sm btn-outline btn-warning"
                    title={gettext("Mark as suspended")}
                  >
                    <.icon name="hero-pause-circle" class="size-4" />
                    {gettext("Suspended")}
                  </button>
                  <button
                    :if={cycle.status != :unpaid}
                    type="button"
                    phx-click="mark_cycle_status"
                    phx-value-cycle_id={cycle.id}
                    phx-value-status="unpaid"
                    phx-target={@myself}
                    class="btn btn-sm btn-error"
                    title={gettext("Mark as unpaid")}
                  >
                    <.icon name="hero-x-circle" class="size-4" />
                    {gettext("Unpaid")}
                  </button>
                <% end %>
                <%= if @can_destroy_cycle do %>
                  <button
                    type="button"
                    phx-click="delete_cycle"
                    phx-value-cycle_id={cycle.id}
                    phx-target={@myself}
                    class="btn btn-sm btn-error btn-outline"
                    title={gettext("Delete cycle")}
                  >
                    <.icon name="hero-trash" class="size-4" />
                    {gettext("Delete")}
                  </button>
                <% end %>
              </div>
            </:action>
          </.table>
        <% else %>
          <div class="alert alert-info">
            <.icon name="hero-information-circle" class="size-5" />
            <span>
              {gettext(
                "No membership fee cycles found. Cycles will be generated automatically when a membership fee type is assigned."
              )}
            </span>
          </div>
        <% end %>
      </.section_box>

      <%!-- Edit Cycle Amount Modal --%>
      <%= if @editing_cycle do %>
        <dialog id="edit-cycle-amount-modal" class="modal modal-open">
          <div class="modal-box">
            <h3 class="text-lg font-bold">{gettext("Edit Cycle Amount")}</h3>
            <form phx-submit="save_cycle_amount" phx-target={@myself}>
              <input type="hidden" name="cycle_id" value={@editing_cycle.id} />
              <div class="form-control w-full mt-4">
                <label class="label">
                  <span class="label-text">{gettext("Amount")}</span>
                </label>
                <input
                  type="text"
                  inputmode="decimal"
                  name="amount"
                  step="0.01"
                  min="0"
                  value={Decimal.to_string(@editing_cycle.amount) |> String.replace(".", ",")}
                  class="input input-bordered w-full"
                  required
                />
              </div>
              <div class="modal-action">
                <button type="button" phx-click="cancel_edit_amount" phx-target={@myself} class="btn">
                  {gettext("Cancel")}
                </button>
                <button type="submit" class="btn btn-primary">{gettext("Save")}</button>
              </div>
            </form>
          </div>
        </dialog>
      <% end %>

      <%!-- Delete Cycle Confirmation Modal --%>
      <%= if @deleting_cycle do %>
        <dialog id="delete-cycle-modal" class="modal modal-open">
          <div class="modal-box">
            <h3 class="text-lg font-bold">{gettext("Delete Cycle")}</h3>
            <p class="py-4">
              {gettext("Are you sure you want to delete this cycle?")}
            </p>
            <p class="text-sm text-base-content/70 mb-4">
              {MembershipFeeHelpers.format_cycle_range(
                @deleting_cycle.cycle_start,
                @deleting_cycle.membership_fee_type.interval
              )} - {MembershipFeeHelpers.format_currency(@deleting_cycle.amount)}
            </p>
            <div class="modal-action">
              <button phx-click="cancel_delete_cycle" phx-target={@myself} class="btn">
                {gettext("Cancel")}
              </button>
              <button
                phx-click="confirm_delete_cycle"
                phx-value-cycle_id={@deleting_cycle.id}
                phx-target={@myself}
                class="btn btn-error"
              >
                {gettext("Delete")}
              </button>
            </div>
          </div>
        </dialog>
      <% end %>

      <%!-- Delete All Cycles Confirmation Modal --%>
      <%= if @deleting_all_cycles do %>
        <dialog id="delete-all-cycles-modal" class="modal modal-open">
          <div class="modal-box">
            <h3 class="text-lg font-bold text-error">{gettext("Delete All Cycles")}</h3>
            <div class="alert alert-warning mt-4">
              <.icon name="hero-exclamation-triangle" class="size-5" />
              <div>
                <h4 class="font-bold">{gettext("Warning")}</h4>
                <p>
                  {gettext("You are about to delete all %{count} cycles for this member.",
                    count: length(@cycles)
                  )}
                </p>
                <p class="mt-2">
                  {gettext("This action cannot be undone.")}
                </p>
              </div>
            </div>
            <div class="form-control w-full mt-4">
              <label class="label">
                <span class="label-text">
                  {gettext("Type '%{confirmation}' to confirm", confirmation: gettext("Yes"))}
                </span>
              </label>
              <input
                type="text"
                phx-keyup="update_delete_all_confirmation"
                phx-target={@myself}
                value={@delete_all_confirmation || ""}
                class="input input-bordered w-full"
                placeholder={gettext("Yes")}
              />
            </div>
            <div class="modal-action">
              <button phx-click="cancel_delete_all_cycles" phx-target={@myself} class="btn">
                {gettext("Cancel")}
              </button>
              <button
                phx-click="confirm_delete_all_cycles"
                phx-target={@myself}
                class="btn btn-error"
                disabled={
                  String.trim(String.downcase(@delete_all_confirmation)) !=
                    String.downcase(gettext("Yes"))
                }
              >
                {gettext("Delete All")}
              </button>
            </div>
          </div>
        </dialog>
      <% end %>

      <%!-- Create Cycle Modal --%>
      <%= if @creating_cycle do %>
        <dialog id="create-cycle-modal" class="modal modal-open">
          <div class="modal-box">
            <h3 class="text-lg font-bold">{gettext("Create Cycle")}</h3>
            <form phx-submit="create_cycle" phx-target={@myself}>
              <div class="form-control w-full mt-4">
                <label class="label" for="create-cycle-date">
                  <span class="label-text">{gettext("Date")}</span>
                </label>
                <input
                  type="date"
                  id="create-cycle-date"
                  name="date"
                  value={@create_cycle_date || ""}
                  phx-change="update_create_cycle_date"
                  phx-target={@myself}
                  class="input input-bordered w-full"
                  required
                  aria-label={gettext("Date")}
                />
                <label class="label">
                  <span class="label-text-alt">
                    {gettext(
                      "The cycle period will be calculated based on this date and the interval."
                    )}
                  </span>
                </label>
              </div>
              <%= if @create_cycle_date do %>
                <div class="form-control w-full mt-4">
                  <label class="label">
                    <span class="label-text">{gettext("Cycle Period")}</span>
                  </label>
                  <div class="text-sm text-base-content/70">
                    {format_create_cycle_period(
                      @create_cycle_date,
                      @member.membership_fee_type.interval
                    )}
                  </div>
                </div>
              <% end %>
              <div class="form-control w-full mt-4">
                <label class="label" for="create-cycle-amount">
                  <span class="label-text">{gettext("Amount")}</span>
                </label>
                <input
                  type="text"
                  inputmode="decimal"
                  id="create-cycle-amount"
                  name="amount"
                  step="0.01"
                  min="0"
                  value={
                    Decimal.to_string(@member.membership_fee_type.amount) |> String.replace(".", ",")
                  }
                  class="input input-bordered w-full"
                  required
                  aria-label={gettext("Amount")}
                />
              </div>
              <%= if @create_cycle_error do %>
                <div class="alert alert-error mt-4">
                  <.icon name="hero-exclamation-circle" class="size-5" />
                  <span>{@create_cycle_error}</span>
                </div>
              <% end %>
              <div class="modal-action">
                <button type="button" phx-click="cancel_create_cycle" phx-target={@myself} class="btn">
                  {gettext("Cancel")}
                </button>
                <button type="submit" class="btn btn-primary">{gettext("Create")}</button>
              </div>
            </form>
          </div>
        </dialog>
      <% end %>
    </div>
    """
  end

  @impl true
  def update(assigns, socket) do
    member = assigns.member
    actor = assigns.current_user

    # Load cycles if not already loaded
    cycles =
      case member.membership_fee_cycles do
        nil -> []
        cycles when is_list(cycles) -> cycles
        _ -> []
      end

    # Sort cycles by cycle_start descending (newest first)
    cycles = Enum.sort_by(cycles, & &1.cycle_start, {:desc, Date})

    # Get available fee types (filtered to same interval if member has a type)
    available_fee_types = get_available_fee_types(member, actor)

    # Permission flags for cycle actions (so read_only does not see create/update/destroy UI)
    can_create_cycle = can?(actor, :create, MembershipFeeCycle)
    can_destroy_cycle = can?(actor, :destroy, MembershipFeeCycle)
    can_update_cycle = can?(actor, :update, MembershipFeeCycle)

    {:ok,
     socket
     |> assign(assigns)
     |> assign(:cycles, cycles)
     |> assign(:available_fee_types, available_fee_types)
     |> assign(:can_create_cycle, can_create_cycle)
     |> assign(:can_destroy_cycle, can_destroy_cycle)
     |> assign(:can_update_cycle, can_update_cycle)
     |> assign_new(:interval_warning, fn -> nil end)
     |> assign_new(:editing_cycle, fn -> nil end)
     |> assign_new(:deleting_cycle, fn -> nil end)
     |> assign_new(:deleting_all_cycles, fn -> false end)
     |> assign_new(:delete_all_confirmation, fn -> "" end)
     |> assign_new(:creating_cycle, fn -> false end)
     |> assign_new(:create_cycle_date, fn -> nil end)
     |> assign_new(:create_cycle_error, fn -> nil end)
     |> assign_new(:regenerating, fn -> false end)
     |> assign_new(:vereinfacht_debug_response, fn -> nil end)}
  end

  @impl true
  def handle_event("change_membership_fee_type", %{"value" => ""}, socket) do
    # Remove membership fee type
    actor = current_actor(socket)

    case update_member_fee_type(socket.assigns.member, nil, actor) do
      {:ok, updated_member} ->
        send(self(), {:member_updated, updated_member})

        {:noreply,
         socket
         |> assign(:member, updated_member)
         |> assign(:cycles, [])
         |> assign(
           :available_fee_types,
           get_available_fee_types(updated_member, actor)
         )
         |> assign(:interval_warning, nil)
         |> put_flash(:info, gettext("Membership fee type removed"))}

      {:error, error} ->
        {:noreply, put_flash(socket, :error, format_error(error))}
    end
  end

  def handle_event("change_membership_fee_type", %{"value" => fee_type_id}, socket) do
    member = socket.assigns.member
    actor = current_actor(socket)
    new_fee_type = Ash.get!(MembershipFeeType, fee_type_id, domain: MembershipFees, actor: actor)

    # Check if interval matches
    interval_warning =
      if member.membership_fee_type &&
           member.membership_fee_type.interval != new_fee_type.interval do
        gettext(
          "Warning: Changing from %{old_interval} to %{new_interval} is not allowed. Please select a membership fee type with the same interval.",
          old_interval: MembershipFeeHelpers.format_interval(member.membership_fee_type.interval),
          new_interval: MembershipFeeHelpers.format_interval(new_fee_type.interval)
        )
      else
        nil
      end

    if interval_warning do
      {:noreply, assign(socket, :interval_warning, interval_warning)}
    else
      case update_member_fee_type(member, fee_type_id, actor) do
        {:ok, updated_member} ->
          # Reload member with cycles
          updated_member =
            updated_member
            |> Ash.load!(
              [
                :membership_fee_type,
                membership_fee_cycles: [:membership_fee_type]
              ],
              actor: actor
            )

          cycles =
            Enum.sort_by(
              updated_member.membership_fee_cycles || [],
              & &1.cycle_start,
              {:desc, Date}
            )

          send(self(), {:member_updated, updated_member})

          {:noreply,
           socket
           |> assign(:member, updated_member)
           |> assign(:cycles, cycles)
           |> assign(
             :available_fee_types,
             get_available_fee_types(updated_member, actor)
           )
           |> assign(:interval_warning, nil)
           |> put_flash(:info, gettext("Membership fee type updated. Cycles regenerated."))}

        {:error, error} ->
          {:noreply, put_flash(socket, :error, format_error(error))}
      end
    end
  end

  def handle_event("mark_cycle_status", %{"cycle_id" => cycle_id, "status" => status_str}, socket) do
    status = String.to_existing_atom(status_str)
    cycle = find_cycle(socket.assigns.cycles, cycle_id)

    action =
      case status do
        :paid -> :mark_as_paid
        :unpaid -> :mark_as_unpaid
        :suspended -> :mark_as_suspended
      end

    actor = current_actor(socket)

    case Ash.update(cycle, action: action, domain: MembershipFees, actor: actor) do
      {:ok, updated_cycle} ->
        updated_cycles = replace_cycle(socket.assigns.cycles, updated_cycle)

        {:noreply,
         socket
         |> assign(:cycles, updated_cycles)
         |> put_flash(:info, gettext("Cycle status updated"))}

      {:error, %Ash.Error.Invalid{} = error} ->
        error_msg =
          Enum.map_join(error.errors, ", ", fn e -> e.message end)

        {:noreply,
         socket
         |> put_flash(
           :error,
           gettext("Failed to update cycle status: %{errors}", errors: error_msg)
         )}

      {:error, error} ->
        {:noreply,
         socket
         |> put_flash(:error, format_error(error))}
    end
  end

  def handle_event("regenerate_cycles", _params, socket) do
    # Server-side authorization: do not rely on UI hiding the button (e.g. read_only could trigger via DevTools).
    actor = current_actor(socket)

    if can?(actor, :create, MembershipFeeCycle) do
      socket = assign(socket, :regenerating, true)
      member = socket.assigns.member

      case CycleGenerator.generate_cycles_for_member(member.id) do
        {:ok, _new_cycles, _notifications} ->
          actor = current_actor(socket)

          updated_member =
            member
            |> Ash.load!(
              [
                :membership_fee_type,
                membership_fee_cycles: [:membership_fee_type]
              ],
              actor: actor
            )

          cycles =
            Enum.sort_by(
              updated_member.membership_fee_cycles || [],
              & &1.cycle_start,
              {:desc, Date}
            )

          send(self(), {:member_updated, updated_member})

          {:noreply,
           socket
           |> assign(:member, updated_member)
           |> assign(:cycles, cycles)
           |> assign(:regenerating, false)
           |> put_flash(:info, gettext("Cycles regenerated successfully"))}

        {:error, error} ->
          {:noreply,
           socket
           |> assign(:regenerating, false)
           |> put_flash(:error, format_error(error))}
      end
    else
      {:noreply,
       socket
       |> assign(:regenerating, false)
       |> put_flash(:error, format_error(%Ash.Error.Forbidden{}))}
    end
  end

  def handle_event("edit_cycle_amount", %{"cycle_id" => cycle_id}, socket) do
    cycle = find_cycle(socket.assigns.cycles, cycle_id)

    # Load cycle with membership_fee_type for display
    actor = current_actor(socket)
    cycle = Ash.load!(cycle, :membership_fee_type, actor: actor)

    {:noreply, assign(socket, :editing_cycle, cycle)}
  end

  def handle_event("cancel_edit_amount", _params, socket) do
    {:noreply, assign(socket, :editing_cycle, nil)}
  end

  def handle_event("save_cycle_amount", %{"cycle_id" => cycle_id, "amount" => amount_str}, socket) do
    cycle = find_cycle(socket.assigns.cycles, cycle_id)

    # Normalize comma to dot for decimal parsing (German locale support)
    normalized_amount_str = String.replace(amount_str, ",", ".")

    case Decimal.parse(normalized_amount_str) do
      {amount, _} when is_struct(amount, Decimal) ->
        actor = current_actor(socket)

        case cycle
             |> Ash.Changeset.for_update(:update, %{amount: amount})
             |> Ash.update(domain: MembershipFees, actor: actor) do
          {:ok, updated_cycle} ->
            updated_cycles = replace_cycle(socket.assigns.cycles, updated_cycle)

            {:noreply,
             socket
             |> assign(:cycles, updated_cycles)
             |> assign(:editing_cycle, nil)
             |> put_flash(:info, gettext("Cycle amount updated"))}

          {:error, error} ->
            {:noreply,
             socket
             |> put_flash(:error, format_error(error))}
        end

      :error ->
        {:noreply, put_flash(socket, :error, gettext("Invalid amount format"))}
    end
  end

  def handle_event("delete_cycle", %{"cycle_id" => cycle_id}, socket) do
    cycle = find_cycle(socket.assigns.cycles, cycle_id)

    # Load cycle with membership_fee_type for display
    actor = current_actor(socket)
    cycle = Ash.load!(cycle, :membership_fee_type, actor: actor)

    {:noreply, assign(socket, :deleting_cycle, cycle)}
  end

  def handle_event("cancel_delete_cycle", _params, socket) do
    {:noreply, assign(socket, :deleting_cycle, nil)}
  end

  def handle_event("confirm_delete_cycle", %{"cycle_id" => cycle_id}, socket) do
    cycle = find_cycle(socket.assigns.cycles, cycle_id)
    actor = current_actor(socket)

    case Ash.destroy(cycle, domain: MembershipFees, actor: actor) do
      :ok ->
        updated_cycles = Enum.reject(socket.assigns.cycles, &(&1.id == cycle_id))

        {:noreply,
         socket
         |> assign(:cycles, updated_cycles)
         |> assign(:deleting_cycle, nil)
         |> put_flash(:info, gettext("Cycle deleted"))}

      {:ok, _destroyed} ->
        # Handle case where return_destroyed? is true
        updated_cycles = Enum.reject(socket.assigns.cycles, &(&1.id == cycle_id))

        {:noreply,
         socket
         |> assign(:cycles, updated_cycles)
         |> assign(:deleting_cycle, nil)
         |> put_flash(:info, gettext("Cycle deleted"))}

      {:error, error} ->
        {:noreply,
         socket
         |> assign(:deleting_cycle, nil)
         |> put_flash(:error, format_error(error))}
    end
  end

  def handle_event("delete_all_cycles", _params, socket) do
    {:noreply,
     socket
     |> assign(:deleting_all_cycles, true)
     |> assign(:delete_all_confirmation, "")}
  end

  def handle_event("cancel_delete_all_cycles", _params, socket) do
    {:noreply,
     socket
     |> assign(:deleting_all_cycles, false)
     |> assign(:delete_all_confirmation, "")}
  end

  def handle_event("update_delete_all_confirmation", %{"value" => value}, socket) do
    {:noreply, assign(socket, :delete_all_confirmation, value)}
  end

  def handle_event("confirm_delete_all_cycles", _params, socket) do
    # Validate confirmation (case-insensitive, trimmed)
    confirmation = String.trim(String.downcase(socket.assigns.delete_all_confirmation))
    expected = String.downcase(gettext("Yes"))

    if confirmation == expected do
      member = socket.assigns.member
      actor = current_actor(socket)
      cycles = socket.assigns.cycles

      reset_modal = fn s ->
        s
        |> assign(:deleting_all_cycles, false)
        |> assign(:delete_all_confirmation, "")
      end

      if can?(actor, :destroy, MembershipFeeCycle) do
        do_delete_all_cycles(socket, member, actor, cycles, reset_modal)
      else
        {:noreply,
         socket
         |> reset_modal.()
         |> put_flash(:error, format_error(%Ash.Error.Forbidden{}))}
      end
    else
      {:noreply,
       socket
       |> assign(:deleting_all_cycles, false)
       |> assign(:delete_all_confirmation, "")
       |> put_flash(:error, gettext("Confirmation text does not match"))}
    end
  end

  def handle_event("open_create_cycle_modal", _params, socket) do
    {:noreply,
     socket
     |> assign(:creating_cycle, true)
     |> assign(:create_cycle_date, nil)
     |> assign(:create_cycle_error, nil)}
  end

  def handle_event("cancel_create_cycle", _params, socket) do
    {:noreply,
     socket
     |> assign(:creating_cycle, false)
     |> assign(:create_cycle_date, nil)
     |> assign(:create_cycle_error, nil)}
  end

  def handle_event("update_create_cycle_date", %{"date" => date_str}, socket) do
    date =
      case Date.from_iso8601(date_str) do
        {:ok, date} -> date
        _ -> nil
      end

    {:noreply,
     socket
     |> assign(:create_cycle_date, date)
     |> assign(:create_cycle_error, nil)}
  end

  def handle_event("create_cycle", %{"date" => date_str, "amount" => amount_str}, socket) do
    member = socket.assigns.member

    # Normalize comma to dot for decimal parsing (German locale support)
    normalized_amount_str = String.replace(amount_str, ",", ".")

    amount =
      case Decimal.parse(normalized_amount_str) do
        {d, _} when is_struct(d, Decimal) -> {:ok, d}
        :error -> {:error, :invalid_amount}
      end

    with {:ok, date} <- Date.from_iso8601(date_str),
         {:ok, amount} <- amount,
         cycle_start <-
           CalendarCycles.calculate_cycle_start(date, member.membership_fee_type.interval),
         :ok <- validate_cycle_not_exists(socket.assigns.cycles, cycle_start) do
      attrs = %{
        cycle_start: cycle_start,
        amount: amount,
        status: :unpaid,
        member_id: member.id,
        membership_fee_type_id: member.membership_fee_type_id
      }

      actor = current_actor(socket)

      case Ash.create(MembershipFeeCycle, attrs, domain: MembershipFees, actor: actor) do
        {:ok, _new_cycle} ->
          # Reload member with cycles
          updated_member =
            member
            |> Ash.load!(
              [
                :membership_fee_type,
                membership_fee_cycles: [:membership_fee_type]
              ],
              actor: actor
            )

          cycles =
            Enum.sort_by(
              updated_member.membership_fee_cycles || [],
              & &1.cycle_start,
              {:desc, Date}
            )

          send(self(), {:member_updated, updated_member})

          {:noreply,
           socket
           |> assign(:member, updated_member)
           |> assign(:cycles, cycles)
           |> assign(:creating_cycle, false)
           |> assign(:create_cycle_date, nil)
           |> assign(:create_cycle_error, nil)
           |> put_flash(:info, gettext("Cycle created successfully"))}

        {:error, error} ->
          {:noreply,
           socket
           |> assign(:create_cycle_error, format_error(error))}
      end
    else
      :error ->
        {:noreply,
         socket
         |> assign(:create_cycle_error, gettext("Invalid date format"))}

      {:error, :invalid_amount} ->
        {:noreply,
         socket
         |> assign(:create_cycle_error, gettext("Invalid amount format"))}

      {:error, :cycle_exists} ->
        {:noreply,
         socket
         |> assign(
           :create_cycle_error,
           gettext("A cycle for this period already exists")
         )}
    end
  end

  # Helper functions

  defp do_delete_all_cycles(socket, member, actor, cycles, reset_modal) do
    result =
      Enum.reduce_while(cycles, {:ok, 0}, fn cycle, {:ok, count} ->
        case Ash.destroy(cycle, domain: MembershipFees, actor: actor) do
          :ok -> {:cont, {:ok, count + 1}}
          {:ok, _} -> {:cont, {:ok, count + 1}}
          {:error, error} -> {:halt, {:error, error}}
        end
      end)

    case result do
      {:ok, deleted_count} when deleted_count > 0 ->
        updated_member =
          member
          |> Ash.load!(
            [:membership_fee_type, membership_fee_cycles: [:membership_fee_type]],
            actor: actor
          )

        updated_cycles =
          Enum.sort_by(
            updated_member.membership_fee_cycles || [],
            & &1.cycle_start,
            {:desc, Date}
          )

        send(self(), {:member_updated, updated_member})

        {:noreply,
         socket
         |> assign(:member, updated_member)
         |> assign(:cycles, updated_cycles)
         |> reset_modal.()
         |> put_flash(:info, gettext("All cycles deleted"))}

      {:ok, _} ->
        {:noreply,
         socket
         |> reset_modal.()
         |> put_flash(:info, gettext("No cycles to delete"))}

      {:error, error} ->
        {:noreply,
         socket
         |> reset_modal.()
         |> put_flash(:error, format_error(error))}
    end
  end

  defp get_available_fee_types(member, actor) do
    all_types =
      MembershipFeeType
      |> Ash.Query.sort(name: :asc)
      |> Ash.read!(domain: MembershipFees, actor: actor)

    # If member has a fee type, filter to same interval
    if member.membership_fee_type do
      Enum.filter(all_types, fn type ->
        type.interval == member.membership_fee_type.interval
      end)
    else
      all_types
    end
  end

  defp update_member_fee_type(member, fee_type_id, actor) do
    attrs = %{membership_fee_type_id: fee_type_id}

    member
    |> Ash.Changeset.for_update(:update_member, attrs, domain: Membership)
    |> Ash.update(domain: Membership, actor: actor)
  end

  defp find_cycle(cycles, cycle_id) do
    case Enum.find(cycles, &(&1.id == cycle_id)) do
      nil -> raise "Cycle not found: #{cycle_id}"
      cycle -> cycle
    end
  end

  defp replace_cycle(cycles, updated_cycle) do
    Enum.map(cycles, fn cycle ->
      if cycle.id == updated_cycle.id, do: updated_cycle, else: cycle
    end)
  end

  defp format_status_label(:paid), do: gettext("Paid")
  defp format_status_label(:unpaid), do: gettext("Unpaid")
  defp format_status_label(:suspended), do: gettext("Suspended")

  defp format_error(%Ash.Error.Invalid{} = error) do
    Enum.map_join(error.errors, ", ", fn e -> e.message end)
  end

  defp format_error(%Ash.Error.Forbidden{}) do
    gettext("You are not allowed to perform this action.")
  end

  defp format_error(error) when is_binary(error), do: error
  defp format_error(_error), do: gettext("An error occurred")

  defp validate_cycle_not_exists(cycles, cycle_start) do
    if Enum.any?(cycles, &(&1.cycle_start == cycle_start)) do
      {:error, :cycle_exists}
    else
      :ok
    end
  end

  defp format_create_cycle_period(date, interval) when is_struct(date, Date) do
    cycle_start = CalendarCycles.calculate_cycle_start(date, interval)
    cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval)

    MembershipFeeHelpers.format_cycle_range(cycle_start, interval) <>
      " (#{Calendar.strftime(cycle_start, "%d.%m.%Y")} - #{Calendar.strftime(cycle_end, "%d.%m.%Y")})"
  end

  defp format_create_cycle_period(_date, _interval), do: ""

  defp format_vereinfacht_debug_response({:ok, body}) when is_map(body) do
    Jason.encode!(body, pretty: true)
  end

  defp format_vereinfacht_debug_response({:error, {:http, status, detail}})
       when is_binary(detail) do
    "Error: HTTP #{status} – #{detail}"
  end

  defp format_vereinfacht_debug_response({:error, {:http, status, _}}) do
    "Error: HTTP #{status}"
  end

  defp format_vereinfacht_debug_response({:error, reason}) do
    "Error: " <> inspect(reason)
  end

  # Helper component for section box
  attr :title, :string, required: true
  slot :inner_block, required: true

  defp section_box(assigns) do
    ~H"""
    <section class="mb-6">
      <h2 class="text-lg font-semibold mb-3">{@title}</h2>
      <div class="border border-base-300 rounded-lg p-4 bg-base-100">
        {render_slot(@inner_block)}
      </div>
    </section>
    """
  end
end
