defmodule Mv.Membership.Group do
  @moduledoc """
  Ash resource representing a group that members can belong to.

  ## Overview
  Groups allow organizing members into categories (e.g., "Board Members", "Active Members").
  Each member can belong to multiple groups, and each group can contain multiple members.

  ## Attributes
  - `name` - Unique group name (required, max 100 chars, case-insensitive uniqueness)
  - `slug` - URL-friendly identifier (required, max 100 chars, auto-generated from name, immutable)
  - `description` - Optional description (max 500 chars)

  ## Relationships
  - `has_many :member_groups` - Relationship to MemberGroup join table
  - `many_to_many :members` - Relationship to Members through MemberGroup

  ## Constraints
  - Name must be unique (case-insensitive, using LOWER(name) in database)
  - Slug must be unique (case-sensitive, exact match)
  - Name cannot be null
  - Slug cannot be null

  ## Calculations
  - `member_count` - Returns the number of members in this group

  ## Examples
      # Create a new group
      Group.create!(%{name: "Board Members", description: "Members of the board"})
      # => %Group{name: "Board Members", slug: "board-members", ...}

      # Update group (slug remains unchanged)
      group = Group.get_by_slug!("board-members")
      Group.update!(group, %{description: "Updated description"})
      # => %Group{slug: "board-members", ...}  # slug unchanged!
  """
  use Ash.Resource,
    domain: Mv.Membership,
    data_layer: AshPostgres.DataLayer,
    authorizers: [Ash.Policy.Authorizer]

  require Ash.Query
  alias Mv.Helpers
  alias Mv.Helpers.SystemActor
  require Logger

  postgres do
    table "groups"
    repo Mv.Repo
  end

  actions do
    defaults [:read, :destroy]

    create :create do
      accept [:name, :description]
      change Mv.Membership.Changes.GenerateSlug
      validate string_length(:slug, min: 1)
    end

    update :update do
      accept [:name, :description]
      require_atomic? false
    end
  end

  policies do
    policy action_type([:read, :create, :update, :destroy]) do
      description "Check permissions from role (all can read; normal_user and admin can create/update/destroy)"
      authorize_if Mv.Authorization.Checks.HasPermission
    end
  end

  validations do
    validate present(:name)

    # Case-insensitive name uniqueness validation
    validate fn changeset, context ->
      name = Ash.Changeset.get_attribute(changeset, :name)
      current_id = Ash.Changeset.get_attribute(changeset, :id)

      if name do
        check_name_uniqueness(name, current_id, context)
      else
        :ok
      end
    end
  end

  attributes do
    uuid_v7_primary_key :id

    attribute :name, :string do
      allow_nil? false
      public? true

      constraints max_length: 100,
                  trim?: true
    end

    attribute :slug, :string do
      allow_nil? false
      public? true
      writable? false

      constraints max_length: 100,
                  trim?: true
    end

    attribute :description, :string do
      allow_nil? true
      public? true

      constraints max_length: 500,
                  trim?: true
    end

    timestamps()
  end

  relationships do
    has_many :member_groups, Mv.Membership.MemberGroup
    many_to_many :members, Mv.Membership.Member, through: Mv.Membership.MemberGroup
  end

  aggregates do
    count :member_count, :member_groups
  end

  identities do
    identity :unique_slug, [:slug]
  end

  # Private helper function for case-insensitive name uniqueness check
  # Uses context actor if available (respects policies), falls back to system actor
  defp check_name_uniqueness(name, exclude_id, context) do
    # Use context actor if available (respects user permissions), otherwise fall back to system actor
    actor =
      case context do
        %{actor: actor} when not is_nil(actor) -> actor
        _ -> SystemActor.get_system_actor()
      end

    query =
      Mv.Membership.Group
      |> Ash.Query.filter(fragment("LOWER(?) = LOWER(?)", name, ^name))
      |> Helpers.query_exclude_id(exclude_id)

    opts = Helpers.ash_actor_opts(actor)

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

      {:ok, _} ->
        {:error, field: :name, message: "has already been taken", value: name}

      {:error, reason} ->
        Logger.warning(
          "Name uniqueness validation query failed for group name '#{name}': #{inspect(reason)}. Allowing operation to proceed (fail-open)."
        )

        :ok
    end
  end
end
