defmodule Mv.Vereinfacht do
  @moduledoc """
  Business logic for Vereinfacht accounting software integration.

  - `sync_member/1` – Sync a single member to the API (create or update contact).
    Used by Member create/update (SyncContact) and by User actions that update
    the linked member's email via Ecto (e.g. user email change).
  - `sync_members_without_contact/0` – Bulk sync of members without a contact ID.
  """
  require Ash.Query
  import Ash.Expr
  alias Mv.Vereinfacht.Client
  alias Mv.Membership.Member
  alias Mv.Helpers.SystemActor
  alias Mv.Helpers

  @doc """
  Tests the connection to the Vereinfacht API using the current configuration.

  Delegates to `Mv.Vereinfacht.Client.test_connection/3` with the values from
  `Mv.Config` (ENV variables take priority over database settings).

  ## Returns
  - `{:ok, :connected}` – credentials are valid and API is reachable
  - `{:error, :not_configured}` – URL, API key or club ID is missing
  - `{:error, {:http, status, message}}` – API returned an error (e.g. 401, 403)
  - `{:error, {:request_failed, reason}}` – network/transport error
  """
  @spec test_connection() :: {:ok, :connected} | {:error, term()}
  def test_connection do
    Client.test_connection(
      Mv.Config.vereinfacht_api_url(),
      Mv.Config.vereinfacht_api_key(),
      Mv.Config.vereinfacht_club_id()
    )
  end

  @doc """
  Syncs a single member to Vereinfacht (create or update finance contact).

  If the member has no `vereinfacht_contact_id`, creates a contact and updates
  the member with the new ID. If they already have an ID, updates the contact.
  Uses system actor for any Ash update. Does nothing if Vereinfacht is not configured.

  Returns:
  - `:ok` – Contact was updated.
  - `{:ok, member}` – Contact was created and member was updated with the new ID.
  - `{:error, reason}` – API or update failed.
  """
  @spec sync_member(struct()) :: :ok | {:ok, struct()} | {:error, term()}
  def sync_member(member) do
    if Mv.Config.vereinfacht_configured?() do
      do_sync_member(member)
    else
      :ok
    end
  end

  defp do_sync_member(member) do
    if present_contact_id?(member.vereinfacht_contact_id) do
      sync_existing_contact(member)
    else
      ensure_contact_then_save(member)
    end
  end

  defp sync_existing_contact(member) do
    case Client.update_contact(member.vereinfacht_contact_id, member) do
      {:ok, _} -> :ok
      {:error, reason} -> {:error, reason}
    end
  end

  defp ensure_contact_then_save(member) do
    case get_or_create_contact_id(member) do
      {:ok, contact_id} -> save_contact_id(member, contact_id)
      {:error, _} = err -> err
    end
  end

  # Before create: find by email to avoid duplicate contacts (idempotency).
  # When an existing contact is found, update it with current member data.
  defp get_or_create_contact_id(member) do
    email = member |> Map.get(:email) |> to_string() |> String.trim()

    if email == "" do
      Client.create_contact(member)
    else
      case Client.find_contact_by_email(email) do
        {:ok, existing_id} -> update_existing_contact_and_return_id(existing_id, member)
        {:error, :not_found} -> Client.create_contact(member)
        {:error, _} = err -> err
      end
    end
  end

  defp update_existing_contact_and_return_id(contact_id, member) do
    case Client.update_contact(contact_id, member) do
      {:ok, _} -> {:ok, contact_id}
      {:error, _} = err -> err
    end
  end

  defp save_contact_id(member, contact_id) do
    system_actor = SystemActor.get_system_actor()
    opts = Helpers.ash_actor_opts(system_actor)

    case Ash.update(member, %{vereinfacht_contact_id: contact_id}, [
           {:action, :set_vereinfacht_contact_id} | opts
         ]) do
      {:ok, updated} -> {:ok, updated}
      {:error, reason} -> {:error, reason}
    end
  end

  defp present_contact_id?(nil), do: false
  defp present_contact_id?(""), do: false
  defp present_contact_id?(s) when is_binary(s), do: String.trim(s) != ""
  defp present_contact_id?(_), do: false

  @doc """
  Formats an API/request error reason into a short user-facing message.

  Used by SyncContact (flash) and GlobalSettingsLive (sync result list).
  """
  @spec format_error(term()) :: String.t()
  def format_error({:http, _status, detail}) when is_binary(detail), do: "Vereinfacht: " <> detail
  def format_error({:http, status, _}), do: "Vereinfacht: API error (HTTP #{status})."

  def format_error({:request_failed, _}),
    do: "Vereinfacht: Request failed (e.g. connection error)."

  def format_error({:invalid_response, _}), do: "Vereinfacht: Invalid API response."
  def format_error(other), do: "Vereinfacht: " <> inspect(other)

  @doc """
  Creates Vereinfacht contacts for all members that do not yet have a
  `vereinfacht_contact_id`. Uses system actor for reads and updates.

  Returns `{:ok, %{synced: count, errors: list}}` where errors is a list of
  `{member_id, reason}`. Does nothing if Vereinfacht is not configured.
  """
  @spec sync_members_without_contact() ::
          {:ok, %{synced: non_neg_integer(), errors: [{String.t(), term()}]}}
          | {:error, :not_configured}
  def sync_members_without_contact do
    if Mv.Config.vereinfacht_configured?() do
      system_actor = SystemActor.get_system_actor()
      opts = Helpers.ash_actor_opts(system_actor)

      query =
        Member
        |> Ash.Query.filter(
          expr(is_nil(^ref(:vereinfacht_contact_id)) or ^ref(:vereinfacht_contact_id) == "")
        )

      case Ash.read(query, opts) do
        {:ok, members} ->
          do_sync_members(members, opts)

        {:error, _} = err ->
          err
      end
    else
      {:error, :not_configured}
    end
  end

  defp do_sync_members(members, opts) do
    {synced, errors} =
      Enum.reduce(members, {0, []}, fn member, {acc_synced, acc_errors} ->
        {inc, new_errors} = sync_one_member(member, opts)
        {acc_synced + inc, acc_errors ++ new_errors}
      end)

    {:ok, %{synced: synced, errors: errors}}
  end

  defp sync_one_member(member, _opts) do
    case sync_member(member) do
      :ok -> {1, []}
      {:ok, _} -> {1, []}
      {:error, reason} -> {0, [{member.id, reason}]}
    end
  end
end
