defmodule MvWeb.Plugs.CheckPagePermission do
  @moduledoc """
  Plug that checks if the current user has permission to access the requested page.

  Runs in the router pipeline before LiveView mounts. Uses PermissionSets page list
  and matches the current route template (or request path) against allowed patterns.

  ## How It Works

  1. Public paths (e.g. /auth, /register) are exempt and pass through.
  2. Extracts page path from conn via `Phoenix.Router.route_info/4` (route template
     like "/members/:id") or falls back to `conn.request_path`.
  3. Gets current user from `conn.assigns[:current_user]`.
  4. Gets user's permission_set_name from role and calls `PermissionSets.get_permissions/1`.
  5. Matches requested path against allowed patterns (exact, dynamic `:param`, wildcard "*").
  6. If unauthorized: redirects to "/sign-in" (no user) or "/users/:id" (user profile) with flash error and halts.

  ## Pattern Matching

  - Exact: "/members" == "/members"
  - Dynamic: "/members/:id" matches "/members/123"
  - Wildcard: "*" matches everything (admin)
  - Reserved: the segment "new" is never matched by `:id` or `:slug` (e.g. `/members/new` and `/groups/new` require an explicit page permission).
  """

  import Plug.Conn
  import Phoenix.Controller
  alias Mv.Authorization.PermissionSets
  require Logger

  def init(opts), do: opts

  def call(conn, _opts) do
    if public_path?(conn.request_path) do
      conn
    else
      # Ensure role is loaded (load_from_session does not load it; required for permission check)
      user =
        conn.assigns[:current_user]
        |> Mv.Authorization.Actor.ensure_loaded()

      conn = Plug.Conn.assign(conn, :current_user, user)
      page_path = get_page_path(conn)
      request_path = conn.request_path

      if has_page_permission?(user, page_path, request_path) do
        conn
      else
        log_page_access_denied(user, page_path)

        redirect_to = redirect_target(user)

        conn
        |> fetch_session()
        |> fetch_flash()
        |> put_flash(:error, "You don't have permission to access this page.")
        |> redirect(to: redirect_to)
        |> halt()
      end
    end
  end

  @doc """
  Returns the redirect URL for an unauthorized user (for LiveView push_redirect).
  """
  def redirect_target_for_user(nil), do: "/sign-in"

  def redirect_target_for_user(user) when is_map(user) or is_struct(user) do
    id = Map.get(user, :id) || Map.get(user, "id")
    if id, do: "/users/#{to_string(id)}", else: "/sign-in"
  end

  def redirect_target_for_user(_), do: "/sign-in"

  defp redirect_target(user), do: redirect_target_for_user(user)

  @doc """
  Returns true if the path is public (no auth/permission check).
  Used by LiveView hook to skip redirect on sign-in etc.
  """
  def public_path?(path) when is_binary(path) do
    path in ["/register", "/reset", "/set_locale", "/sign-in", "/sign-out"] or
      String.starts_with?(path, "/auth") or
      String.starts_with?(path, "/confirm") or
      String.starts_with?(path, "/password-reset")
  end

  defp get_page_path(conn) do
    router = conn.private[:phoenix_router]
    get_page_path_from_router(router, conn.method, conn.request_path, conn.host)
  end

  @doc """
  Returns whether the user is allowed to access the given request path.
  Used by the plug and by LiveView on_mount/handle_params for client-side navigation.

  Options: `:router` (default MvWeb.Router), `:host` (default "localhost").
  """
  def user_can_access_page?(user, request_path, opts \\ []) do
    router = Keyword.get(opts, :router, MvWeb.Router)
    host = Keyword.get(opts, :host, "localhost")
    page_path = get_page_path_from_router(router, "GET", request_path, host)
    has_page_permission?(user, page_path, request_path)
  end

  defp get_page_path_from_router(router, method, request_path, host) do
    case Phoenix.Router.route_info(router, method, request_path, host) do
      %{route: route} -> route
      _ -> request_path
    end
  end

  defp has_page_permission?(nil, _page_path, _request_path), do: false

  defp has_page_permission?(user, page_path, request_path) do
    with ps_name when is_binary(ps_name) <- permission_set_name_from_user(user),
         {:ok, ps_atom} <- PermissionSets.permission_set_name_to_atom(ps_name),
         permissions <- PermissionSets.get_permissions(ps_atom) do
      page_matches?(permissions.pages, page_path, request_path, user)
    else
      _ -> false
    end
  end

  defp permission_set_name_from_user(user) when is_map(user) or is_struct(user) do
    get_in(user, [Access.key(:role), Access.key(:permission_set_name)]) ||
      get_in(user, [Access.key("role"), Access.key("permission_set_name")])
  end

  defp permission_set_name_from_user(_), do: nil

  defp user_id_from_user(user) when is_map(user) or is_struct(user) do
    id = Map.get(user, :id) || Map.get(user, "id")
    if id, do: to_string(id), else: nil
  end

  defp user_id_from_user(_), do: nil

  # Reserved path segments that must not match a single :id param (e.g. /members/new, /users/new).
  @reserved_id_segments ["new"]

  # For "/users/:id" with own_data we only allow when the id in the path equals the current user's id.
  # For "/members/:id" we reject when the segment is reserved (e.g. "new") so /members/new is not allowed.
  defp page_matches?(allowed_pages, requested_path, request_path, user) do
    Enum.any?(allowed_pages, fn pattern ->
      pattern_match?(pattern, requested_path, request_path, user)
    end)
  end

  defp pattern_match?("*", _requested_path, _request_path, _user), do: true

  defp pattern_match?(pattern, _requested_path, request_path, user)
       when pattern == "/users/:id" do
    match_dynamic_route?(pattern, request_path) and
      path_param_equals(pattern, request_path, "id", user_id_from_user(user))
  end

  defp pattern_match?(pattern, _requested_path, request_path, user)
       when pattern in ["/users/:id/edit", "/users/:id/show/edit"] do
    match_dynamic_route?(pattern, request_path) and
      path_param_equals(pattern, request_path, "id", user_id_from_user(user))
  end

  defp pattern_match?(pattern, _requested_path, request_path, user)
       when pattern == "/members/:id" do
    match_dynamic_route?(pattern, request_path) and
      path_param_not_reserved(pattern, request_path, "id", @reserved_id_segments) and
      members_show_allowed?(pattern, request_path, user)
  end

  defp pattern_match?(pattern, _requested_path, request_path, user)
       when pattern in ["/members/:id/edit", "/members/:id/show/edit"] do
    match_dynamic_route?(pattern, request_path) and
      members_edit_allowed?(pattern, request_path, user)
  end

  defp pattern_match?(pattern, _requested_path, request_path, _user)
       when pattern == "/groups/:slug" do
    match_dynamic_route?(pattern, request_path) and
      path_param_not_reserved(pattern, request_path, "slug", @reserved_id_segments)
  end

  defp pattern_match?(pattern, requested_path, _request_path, _user)
       when pattern == requested_path do
    true
  end

  defp pattern_match?(pattern, _requested_path, request_path, _user) do
    if String.contains?(pattern, ":") do
      match_dynamic_route?(pattern, request_path)
    else
      false
    end
  end

  defp path_param_not_reserved(pattern, request_path, param_name, reserved)
       when is_list(reserved) do
    segments = String.split(request_path, "/", trim: true)
    idx = param_index(pattern, param_name)

    if idx < 0 do
      false
    else
      value = Enum.at(segments, idx)
      value not in reserved
    end
  end

  defp path_param_equals(pattern, request_path, param_name, expected_value)
       when is_binary(expected_value) do
    segments = String.split(request_path, "/", trim: true)
    idx = param_index(pattern, param_name)

    if idx < 0 do
      false
    else
      value = Enum.at(segments, idx)
      value == expected_value
    end
  end

  defp path_param_equals(_, _, _, _), do: false

  # For own_data: only allow show/edit when :id is the user's linked member. For other permission sets: allow when not reserved.
  defp members_show_allowed?(pattern, request_path, user) do
    if permission_set_name_from_user(user) == "own_data" do
      path_param_equals(pattern, request_path, "id", user_member_id(user))
    else
      true
    end
  end

  defp members_edit_allowed?(pattern, request_path, user) do
    if permission_set_name_from_user(user) == "own_data" do
      path_param_equals(pattern, request_path, "id", user_member_id(user))
    else
      path_param_not_reserved(pattern, request_path, "id", @reserved_id_segments)
    end
  end

  defp user_member_id(user) when is_map(user) or is_struct(user) do
    member_id = Map.get(user, :member_id) || Map.get(user, "member_id")

    if is_nil(member_id) do
      load_member_id_for_user(user)
    else
      to_string(member_id)
    end
  end

  defp user_member_id(_), do: nil

  defp load_member_id_for_user(user) do
    id = user_id_from_user(user)

    if id do
      case Ash.get(Mv.Accounts.User, id, load: [:member], domain: Mv.Accounts, authorize?: false) do
        {:ok, loaded} when not is_nil(loaded.member_id) -> to_string(loaded.member_id)
        _ -> nil
      end
    else
      nil
    end
  end

  defp param_index(pattern, param_name) do
    pattern
    |> String.split("/", trim: true)
    |> Enum.find_index(fn seg ->
      String.starts_with?(seg, ":") and String.trim_leading(seg, ":") == param_name
    end)
    |> case do
      nil -> -1
      i -> i
    end
  end

  defp match_dynamic_route?(pattern, path) do
    pattern_segments = String.split(pattern, "/", trim: true)
    path_segments = String.split(path, "/", trim: true)

    if length(pattern_segments) == length(path_segments) do
      Enum.zip(pattern_segments, path_segments)
      |> Enum.all?(fn {pattern_seg, path_seg} ->
        String.starts_with?(pattern_seg, ":") or pattern_seg == path_seg
      end)
    else
      false
    end
  end

  defp log_page_access_denied(user, page_path) do
    user_id =
      if user do
        Map.get(user, :id) || Map.get(user, "id") || "nil"
      else
        "nil"
      end

    role_name =
      if user do
        get_in(user, [Access.key(:role), Access.key(:name)]) ||
          get_in(user, [Access.key("role"), Access.key("name")]) || "nil"
      else
        "nil"
      end

    Logger.info("""
    Page access denied:
      User: #{user_id}
      Role: #{role_name}
      Page: #{page_path}
    """)
  end
end
