defmodule Mv.Membership.MemberGroup do
  @moduledoc """
  Ash resource representing the join table for the many-to-many relationship
  between Members and Groups.

  ## Overview
  MemberGroup is a join table that links members to groups. It enables the
  many-to-many relationship where:
  - A member can belong to multiple groups
  - A group can contain multiple members

  ## Attributes
  - `member_id` - Foreign key to Member (required)
  - `group_id` - Foreign key to Group (required)

  ## Relationships
  - `belongs_to :member` - Relationship to Member
  - `belongs_to :group` - Relationship to Group

  ## Constraints
  - Unique constraint on `(member_id, group_id)` - prevents duplicate memberships
  - CASCADE delete: Removing member removes all group associations
  - CASCADE delete: Removing group removes all member associations

  ## Examples
      # Add member to group
      {:ok, member_group} =
        Membership.create_member_group(%{member_id: member.id, group_id: group.id})

      # Remove member from group
      {:ok, [member_group]} =
        Ash.read(
          Mv.Membership.MemberGroup
          |> Ash.Query.filter(member_id == ^member.id and group_id == ^group.id),
          domain: Mv.Membership
        )

      :ok = Membership.destroy_member_group(member_group)
  """
  use Ash.Resource,
    domain: Mv.Membership,
    data_layer: AshPostgres.DataLayer,
    authorizers: [Ash.Policy.Authorizer]

  require Ash.Query

  postgres do
    table "member_groups"
    repo Mv.Repo
  end

  actions do
    defaults [:read, :destroy]

    create :create do
      accept [:member_id, :group_id]
    end
  end

  # Authorization: read uses bypass for :linked (own_data only) then HasPermission for :all;
  # create/destroy use HasPermission (normal_user + admin only).
  # Single check: own_data gets filter via auto_filter; admin does not match, gets :all from HasPermission.
  policies do
    bypass action_type(:read) do
      description "own_data: read only member_groups where member_id == actor.member_id"
      authorize_if Mv.Authorization.Checks.MemberGroupReadLinkedForOwnData
    end

    policy action_type(:read) do
      description "Check read permission from role (read_only/normal_user/admin :all)"
      authorize_if Mv.Authorization.Checks.HasPermission
    end

    policy action_type([:create, :destroy]) do
      description "Check create/destroy from role (normal_user + admin only)"
      authorize_if Mv.Authorization.Checks.HasPermission
    end
  end

  validations do
    validate present(:member_id)
    validate present(:group_id)

    # Prevent duplicate associations
    validate fn changeset, context ->
      member_id = Ash.Changeset.get_attribute(changeset, :member_id)
      group_id = Ash.Changeset.get_attribute(changeset, :group_id)
      current_id = Ash.Changeset.get_attribute(changeset, :id)

      if member_id && group_id do
        check_duplicate_association(member_id, group_id, current_id, context)
      else
        :ok
      end
    end
  end

  attributes do
    uuid_v7_primary_key :id

    attribute :member_id, :uuid do
      allow_nil? false
    end

    attribute :group_id, :uuid do
      allow_nil? false
    end

    timestamps()
  end

  relationships do
    belongs_to :member, Mv.Membership.Member do
      allow_nil? false
    end

    belongs_to :group, Mv.Membership.Group do
      allow_nil? false
    end
  end

  identities do
    identity :unique_member_group, [:member_id, :group_id]
  end

  # Private helper function to check for duplicate associations
  # Uses context actor if available (respects policies), falls back to system actor
  defp check_duplicate_association(member_id, group_id, exclude_id, context) do
    alias Mv.Helpers
    alias Mv.Helpers.SystemActor

    # 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.MemberGroup
      |> Ash.Query.filter(member_id == ^member_id and group_id == ^group_id)
      |> Helpers.query_exclude_id(exclude_id)

    opts = Helpers.ash_actor_opts(actor)

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

      {:ok, _} ->
        {:error, field: :member_id, message: "Member is already in this group", value: member_id}

      {:error, _reason} ->
        # Fail-open: if query fails, allow operation to proceed
        # Database constraint will catch duplicates anyway
        :ok
    end
  end
end
