defmodule Mv.Membership do
  @moduledoc """
  Ash Domain for membership management.

  ## Resources
  - `Member` - Club members with personal information and custom field values
  - `CustomFieldValue` - Dynamic custom field values attached to members
  - `CustomField` - Schema definitions for custom fields
  - `Setting` - Global application settings (singleton)
  - `Group` - Groups that members can belong to
  - `MemberGroup` - Join table for many-to-many relationship between Members and Groups

  ## Public API
  The domain exposes these main actions:
  - Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
  - Custom field value management: `create_custom_field_value/1`, `list_custom_field_values/0`, etc.
  - Custom field management: `create_custom_field/1`, `list_custom_fields/0`, `list_required_custom_fields/1`, etc.
  - Settings management: `get_settings/0`, `update_settings/2`, `update_member_field_visibility/2`, `update_single_member_field_visibility/3`
  - Group management: `create_group/1`, `list_groups/0`, `update_group/2`, `destroy_group/1`
  - Member-group associations: `create_member_group/1`, `list_member_groups/0`, `destroy_member_group/1`

  ## Admin Interface
  The domain is configured with AshAdmin for management UI.
  """
  use Ash.Domain,
    extensions: [AshAdmin.Domain, AshPhoenix]

  require Ash.Query
  import Ash.Expr

  admin do
    show? true
  end

  resources do
    resource Mv.Membership.Member do
      define :create_member, action: :create_member
      define :list_members, action: :read
      define :update_member, action: :update_member
      define :destroy_member, action: :destroy
    end

    resource Mv.Membership.CustomFieldValue do
      define :create_custom_field_value, action: :create
      define :list_custom_field_values, action: :read
      define :update_custom_field_value, action: :update
      define :destroy_custom_field_value, action: :destroy
    end

    resource Mv.Membership.CustomField do
      define :create_custom_field, action: :create
      define :list_custom_fields, action: :read
      define :update_custom_field, action: :update
      define :destroy_custom_field, action: :destroy_with_values
      define :prepare_custom_field_deletion, action: :prepare_deletion, args: [:id]
    end

    resource Mv.Membership.Setting do
      # Note: create action exists but is not exposed via code interface
      # It's only used internally as fallback in get_settings/0
      # Settings should be created via seed script
      define :update_settings, action: :update
      define :update_member_field_visibility, action: :update_member_field_visibility

      define :update_single_member_field_visibility,
        action: :update_single_member_field_visibility
    end

    resource Mv.Membership.Group do
      define :create_group, action: :create
      define :list_groups, action: :read
      define :update_group, action: :update
      define :destroy_group, action: :destroy
    end

    resource Mv.Membership.MemberGroup do
      define :create_member_group, action: :create
      define :list_member_groups, action: :read
      define :destroy_member_group, action: :destroy
    end
  end

  # Singleton pattern: Get the single settings record
  @doc """
  Gets the global settings.

  Settings should normally be created via the seed script (`priv/repo/seeds.exs`).
  If no settings exist, this function will create them as a fallback using the
  `ASSOCIATION_NAME` environment variable or "Club Name" as default.

  ## Returns

    - `{:ok, settings}` - The settings record
    - `{:ok, nil}` - No settings exist (should not happen if seeds were run)
    - `{:error, error}` - Error reading settings

  ## Examples

      iex> {:ok, settings} = Mv.Membership.get_settings()
      iex> settings.club_name
      "My Club"

  """
  def get_settings do
    # Try to get the first (and only) settings record
    case Ash.read_one(Mv.Membership.Setting, domain: __MODULE__) do
      {:ok, nil} ->
        # No settings exist - create as fallback (should normally be created via seed script)
        default_club_name = System.get_env("ASSOCIATION_NAME") || "Club Name"

        Mv.Membership.Setting
        |> Ash.Changeset.for_create(:create, %{
          club_name: default_club_name,
          member_field_visibility: %{"exit_date" => false}
        })
        |> Ash.create!(domain: __MODULE__)
        |> then(fn settings -> {:ok, settings} end)

      {:ok, settings} ->
        {:ok, settings}

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

  @doc """
  Updates the global settings.

  ## Parameters

    - `settings` - The settings record to update
    - `attrs` - A map of attributes to update (e.g., `%{club_name: "New Name"}`)

  ## Returns

    - `{:ok, updated_settings}` - Successfully updated settings
    - `{:error, error}` - Validation or update error

  ## Examples

      iex> {:ok, settings} = Mv.Membership.get_settings()
      iex> {:ok, updated} = Mv.Membership.update_settings(settings, %{club_name: "New Club"})
      iex> updated.club_name
      "New Club"

  """
  def update_settings(settings, attrs) do
    settings
    |> Ash.Changeset.for_update(:update, attrs)
    |> Ash.update(domain: __MODULE__)
  end

  @doc """
  Lists only required custom fields.

  This is an optimized version that filters at the database level instead of
  loading all custom fields and filtering in memory. Requires an actor for
  authorization (CustomField read policy). Callers must pass `actor:`; no default.

  ## Options

    - `:actor` - Required. The actor for authorization (e.g. current user).
      All roles can read CustomField; actor must have a valid role.

  ## Returns

    - `{:ok, required_custom_fields}` - List of required custom fields
    - `{:error, :missing_actor}` - When actor is nil (caller must pass actor)
    - `{:error, error}` - Error reading custom fields (e.g. Forbidden)

  ## Examples

      iex> {:ok, required_fields} = Mv.Membership.list_required_custom_fields(actor: actor)
      iex> Enum.all?(required_fields, & &1.required)
      true

      iex> Mv.Membership.list_required_custom_fields(actor: nil)
      {:error, :missing_actor}
  """
  def list_required_custom_fields(actor: actor) when not is_nil(actor) do
    Mv.Membership.CustomField
    |> Ash.Query.filter(expr(required == true))
    |> Ash.read(domain: __MODULE__, actor: actor)
  end

  def list_required_custom_fields(actor: nil), do: {:error, :missing_actor}

  @doc """
  Updates the member field visibility configuration.

  This is a specialized action for updating only the member field visibility settings.
  It validates that all keys are valid member fields and all values are booleans.

  ## Parameters

    - `settings` - The settings record to update
    - `visibility_config` - A map of member field names (strings) to boolean visibility values
      (e.g., `%{"street" => false, "house_number" => false}`)

  ## Returns

    - `{:ok, updated_settings}` - Successfully updated settings
    - `{:error, error}` - Validation or update error

  ## Examples

      iex> {:ok, settings} = Mv.Membership.get_settings()
      iex> {:ok, updated} = Mv.Membership.update_member_field_visibility(settings, %{"street" => false, "house_number" => false})
      iex> updated.member_field_visibility
      %{"street" => false, "house_number" => false}

  """
  def update_member_field_visibility(settings, visibility_config) do
    settings
    |> Ash.Changeset.for_update(:update_member_field_visibility, %{
      member_field_visibility: visibility_config
    })
    |> Ash.update(domain: __MODULE__)
  end

  @doc """
  Atomically updates a single field in the member field visibility configuration.

  This action uses PostgreSQL's jsonb_set function to atomically update a single key
  in the JSONB map, preventing lost updates in concurrent scenarios. This is the
  preferred method for updating individual field visibility settings.

  ## Parameters

    - `settings` - The settings record to update
    - `field` - The member field name as a string (e.g., "street", "house_number")
    - `show_in_overview` - Boolean value indicating visibility

  ## Returns

    - `{:ok, updated_settings}` - Successfully updated settings
    - `{:error, error}` - Validation or update error

  ## Examples

      iex> {:ok, settings} = Mv.Membership.get_settings()
      iex> {:ok, updated} = Mv.Membership.update_single_member_field_visibility(settings, field: "street", show_in_overview: false)
      iex> updated.member_field_visibility["street"]
      false

  """
  def update_single_member_field_visibility(settings,
        field: field,
        show_in_overview: show_in_overview
      ) do
    settings
    |> Ash.Changeset.new()
    |> Ash.Changeset.set_argument(:field, field)
    |> Ash.Changeset.set_argument(:show_in_overview, show_in_overview)
    |> Ash.Changeset.for_update(:update_single_member_field_visibility, %{})
    |> Ash.update(domain: __MODULE__)
  end

  @doc """
  Gets a group by its slug.

  Uses `Ash.Query.filter` to efficiently find a group by its slug.
  The unique index on `slug` ensures efficient lookup performance.
  The slug lookup is case-sensitive (exact match required).

  ## Parameters

    - `slug` - The slug to search for (case-sensitive)
    - `opts` - Options including `:actor` for authorization

  ## Returns

    - `{:ok, group}` - Found group (with members and member_count loaded)
    - `{:ok, nil}` - Group not found
    - `{:error, error}` - Error reading group

  ## Examples

      iex> {:ok, group} = Mv.Membership.get_group_by_slug("board-members", actor: actor)
      iex> group.name
      "Board Members"

      iex> {:ok, nil} = Mv.Membership.get_group_by_slug("non-existent", actor: actor)
      {:ok, nil}

  """
  def get_group_by_slug(slug, opts \\ []) do
    load = Keyword.get(opts, :load, [])

    require Ash.Query

    query =
      Mv.Membership.Group
      |> Ash.Query.filter(slug == ^slug)
      |> Ash.Query.load(load)

    opts
    |> Keyword.delete(:load)
    |> Keyword.put_new(:domain, __MODULE__)
    |> then(&Ash.read_one(query, &1))
  end
end
