defmodule MvWeb.RoleLive.Index do
  @moduledoc """
  LiveView for displaying and managing the role list.

  ## Features
  - List all roles with name, description, permission_set_name, is_system_role
  - Create new roles
  - Navigate to role details and edit forms
  - Delete non-system roles

  ## Events
  - `delete` - Remove a role from the database (only non-system roles)

  ## Security
  Only admins can access this page (enforced by authorization).
  """
  use MvWeb, :live_view

  alias Mv.Accounts
  alias Mv.Authorization

  require Ash.Query

  import MvWeb.RoleLive.Helpers,
    only: [format_error: 1, permission_set_badge_class: 1, opts_with_actor: 3]

  @impl true
  def mount(_params, _session, socket) do
    actor = socket.assigns[:current_user]
    roles = load_roles(actor)
    user_counts = load_user_counts(roles, actor)

    {:ok,
     socket
     |> assign(:page_title, gettext("Listing Roles"))
     |> assign(:roles, roles)
     |> assign(:user_counts, user_counts)}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    case Authorization.get_role(id, actor: socket.assigns.current_user) do
      {:ok, role} ->
        handle_delete_role(role, id, socket)

      {:error, %Ash.Error.Query.NotFound{}} ->
        {:noreply,
         put_flash(
           socket,
           :error,
           gettext("Role not found.")
         )}

      {:error, error} ->
        error_message = format_error(error)

        {:noreply,
         put_flash(
           socket,
           :error,
           gettext("Failed to delete role: %{error}", error: error_message)
         )}
    end
  end

  defp handle_delete_role(role, id, socket) do
    if role.is_system_role do
      {:noreply,
       put_flash(
         socket,
         :error,
         gettext("System roles cannot be deleted.")
       )}
    else
      user_count = recalculate_user_count(role, socket.assigns.current_user)

      if user_count > 0 do
        {:noreply,
         put_flash(
           socket,
           :error,
           gettext(
             "Cannot delete role. %{count} user(s) are still assigned to this role. Please assign them to another role first.",
             count: user_count
           )
         )}
      else
        perform_role_deletion(role, id, socket)
      end
    end
  end

  defp perform_role_deletion(role, id, socket) do
    case Authorization.destroy_role(role, actor: socket.assigns.current_user) do
      :ok ->
        updated_roles = Enum.reject(socket.assigns.roles, &(&1.id == id))
        updated_counts = Map.delete(socket.assigns.user_counts, id)

        {:noreply,
         socket
         |> assign(:roles, updated_roles)
         |> assign(:user_counts, updated_counts)
         |> put_flash(:info, gettext("Role deleted successfully."))}

      {:error, error} ->
        error_message = format_error(error)

        {:noreply,
         put_flash(
           socket,
           :error,
           gettext("Failed to delete role: %{error}", error: error_message)
         )}
    end
  end

  @spec load_roles(map() | nil) :: [Mv.Authorization.Role.t()]
  defp load_roles(actor) do
    opts = MvWeb.LiveHelpers.ash_actor_opts(actor)

    case Authorization.list_roles(opts) do
      {:ok, roles} -> Enum.sort_by(roles, & &1.name)
      {:error, _} -> []
    end
  end

  # Loads all user counts for roles using DB-side aggregation for better performance
  @spec load_user_counts([Mv.Authorization.Role.t()], map() | nil) :: %{
          Ecto.UUID.t() => non_neg_integer()
        }
  defp load_user_counts(roles, _actor) do
    role_ids = Enum.map(roles, & &1.id)

    # Use Ecto directly for efficient GROUP BY COUNT query
    # This is much more performant than loading all users and counting in Elixir
    # Note: We bypass Ash here for performance, but this is a simple read-only query
    import Ecto.Query

    query =
      from u in Accounts.User,
        where: u.role_id in ^role_ids,
        group_by: u.role_id,
        select: {u.role_id, count(u.id)}

    results = Mv.Repo.all(query)

    results
    |> Enum.into(%{}, fn {role_id, count} -> {role_id, count} end)
  end

  # Gets user count from preloaded assigns map
  @spec get_user_count(Mv.Authorization.Role.t(), %{Ecto.UUID.t() => non_neg_integer()}) ::
          non_neg_integer()
  defp get_user_count(role, user_counts) do
    Map.get(user_counts, role.id, 0)
  end

  # Recalculates user count for a specific role (used before deletion)
  @spec recalculate_user_count(Mv.Authorization.Role.t(), map() | nil) :: non_neg_integer()
  defp recalculate_user_count(role, actor) do
    opts = opts_with_actor([], actor, Mv.Accounts)

    case Ash.count(Accounts.User |> Ash.Query.filter(role_id == ^role.id), opts) do
      {:ok, count} -> count
      _ -> 0
    end
  end
end
