defmodule MvWeb.Authorization do
  @moduledoc """
  UI-level authorization helpers for LiveView templates.

  These functions check if the current user has permission to perform actions
  or access pages. They use the same PermissionSets module as the backend policies,
  ensuring UI and backend authorization are consistent.

  ## Usage in Templates

      <!-- Conditional button rendering -->
      <%= if can?(@current_user, :create, Mv.Membership.Member) do %>
        <.link patch={~p"/members/new"}>New Member</.link>
      <% end %>

      <!-- Record-level check -->
      <%= if can?(@current_user, :update, @member) do %>
        <.button>Edit</.button>
      <% end %>

      <!-- Page access check -->
      <%= if can_access_page?(@current_user, "/admin/roles") do %>
        <.link navigate="/admin/roles">Manage Roles</.link>
      <% end %>

  ## Performance

  All checks are pure function calls using the hardcoded PermissionSets module.
  No database queries, < 1 microsecond per check.
  """

  alias Mv.Authorization.PermissionSets
  alias MvWeb.Plugs.CheckPagePermission

  @doc """
  Checks if user has permission for an action on a resource.

  This function has two variants:
  1. Resource atom: Checks if user has permission for action on resource type
  2. Record struct: Checks if user has permission for action on specific record (with scope checking)

  ## Examples

      # Resource-level check (atom)
      iex> admin = %{role: %{permission_set_name: "admin"}}
      iex> can?(admin, :create, Mv.Membership.Member)
      true

      iex> mitglied = %{role: %{permission_set_name: "own_data"}}
      iex> can?(mitglied, :create, Mv.Membership.Member)
      false

      # Record-level check (struct with scope)
      iex> user = %{id: "user-123", role: %{permission_set_name: "own_data"}}
      iex> member = %Member{id: "member-456", user: %User{id: "user-123"}}
      iex> can?(user, :update, member)
      true
  """
  @spec can?(map() | nil, atom(), atom() | struct()) :: boolean()
  def can?(nil, _action, _resource), do: false

  def can?(user, action, resource) when is_atom(action) and is_atom(resource) do
    with %{role: %{permission_set_name: ps_name}} when not is_nil(ps_name) <- user,
         {:ok, ps_atom} <- PermissionSets.permission_set_name_to_atom(ps_name),
         permissions <- PermissionSets.get_permissions(ps_atom) do
      resource_name = get_resource_name(resource)

      Enum.any?(permissions.resources, fn perm ->
        perm.resource == resource_name and perm.action == action and perm.granted
      end)
    else
      _ -> false
    end
  end

  def can?(user, action, %resource{} = record) when is_atom(action) do
    with %{role: %{permission_set_name: ps_name}} when not is_nil(ps_name) <- user,
         {:ok, ps_atom} <- PermissionSets.permission_set_name_to_atom(ps_name),
         permissions <- PermissionSets.get_permissions(ps_atom) do
      resource_name = get_resource_name(resource)

      # Find matching permission
      matching_perm =
        Enum.find(permissions.resources, fn perm ->
          perm.resource == resource_name and perm.action == action and perm.granted
        end)

      case matching_perm do
        nil -> false
        perm -> check_scope(perm.scope, user, record, resource_name)
      end
    else
      _ -> false
    end
  end

  @doc """
  Checks if user can access a specific page.

  Nil-safe: returns false when user is nil (e.g. unauthenticated or layout
  assigns regression), so callers do not need to guard.

  ## Examples

      iex> admin = %{role: %{permission_set_name: "admin"}}
      iex> can_access_page?(admin, "/admin/roles")
      true

      iex> can_access_page?(nil, "/members")
      false

      iex> mitglied = %{role: %{permission_set_name: "own_data"}}
      iex> can_access_page?(mitglied, "/members")
      false
  """
  @spec can_access_page?(map() | nil, String.t() | Phoenix.VerifiedRoutes.unverified_path()) ::
          boolean()
  def can_access_page?(nil, _page_path), do: false

  def can_access_page?(user, page_path) do
    # Delegate to plug logic so UI uses same rules (reserved "new", own/linked path checks).
    page_path_str = if is_binary(page_path), do: page_path, else: to_string(page_path)
    CheckPagePermission.user_can_access_page?(user, page_path_str, router: MvWeb.Router)
  end

  # Check if scope allows access to record
  defp check_scope(:all, _user, _record, _resource_name), do: true

  defp check_scope(:own, user, record, _resource_name) do
    record.id == user.id
  end

  defp check_scope(:linked, user, record, resource_name) do
    case resource_name do
      "Member" -> check_member_linked(user, record)
      "CustomFieldValue" -> check_custom_field_value_linked(user, record)
      _ -> check_fallback_linked(user, record)
    end
  end

  defp check_member_linked(user, record) do
    # Member has_one :user (inverse of User belongs_to :member)
    # Check if member.user.id == user.id (user must be preloaded)
    case Map.get(record, :user) do
      %{id: user_id} -> user_id == user.id
      _ -> false
    end
  end

  defp check_custom_field_value_linked(user, record) do
    # Need to traverse: custom_field_value.member.user.id
    # Note: In UI, custom_field_value should have member.user preloaded
    case Map.get(record, :member) do
      %{user: %{id: member_user_id}} -> member_user_id == user.id
      _ -> false
    end
  end

  defp check_fallback_linked(user, record) do
    # Fallback: try user_id or user relationship
    case Map.get(record, :user_id) do
      nil -> check_user_relationship_linked(user, record)
      user_id -> user_id == user.id
    end
  end

  defp check_user_relationship_linked(user, record) do
    # Try user relationship
    case Map.get(record, :user) do
      %{id: user_id} -> user_id == user.id
      _ -> false
    end
  end

  # Extract resource name from module
  defp get_resource_name(resource) when is_atom(resource) do
    resource |> Module.split() |> List.last()
  end
end
