"""
Edge ACLs and plugins: worked examples.

`EdgeAclProvider.apply_to(table)` returns a new table that applies ACLs at the
next downstream operation, picking up the *requesting* user's auth context.

ACLs are therefore applied at several points:

  1. Direct fetch           - the classic viewer flow; the server applies ACLs
                              for the viewer when the table is fetched directly.
  2. Plugin request         - a plugin (deephaven.ui, deephaven plotly-express,
                              WebClientData2, ...) ships the table to the client
                              as a server-side reference. A plugin declares an
                              `authorization_export_behavior`: when it is
                              `"transform"` (as deephaven.ui does), the server runs
                              every reference the plugin exports through the
                              authorization transform - the same one applied when a
                              table is fetched directly - in the *requesting* user's
                              context. A plugin that leaves it `"unset"` (e.g.
                              plotly-express) does not transform its exported
                              references under the default permissive policy, so on
                              that path the table's own ACL is the only protection.
  3. Downstream operations  - any operation on an ACL-bearing table
                              (`.where`, `.sum_by`, `by=` in a plot, ...) applies
                              ACLs for whatever auth context is active when that
                              operation runs.

The most important subtlety, demonstrated repeatedly below, is *whose context
is active when the downstream operation runs*:

  - Inside a `@ui.component`, the component re-renders per viewer, so table
    operations performed in the component body run in the VIEWER's context.
    ACLs are therefore applied for the viewer.

  - At instantiation time (e.g. building a plot at the top level), the
    operation runs in the QUERY OWNER's context. ACLs are applied for the
    owner (and an owner/admin sees everything). The result is "baked in" and
    later object-visibility ACLs do NOT re-filter it for the viewer.

Run this as a Persistent Query owned by an admin/owner; "alice" is a viewer.
"""

from deephaven.plot import express as dx
import deephaven.ui as ui
from deephaven_enterprise.edge_acl import EdgeAclProvider
from deephaven_enterprise import acl_generator

# ---------------------------------------------------------------------------
# Sample data: the dx "stocks" data set.
#   Columns: Timestamp, Sym, Exchange, Size, Price, Dollars, Side
#   Sym values: CAT, DOG, FISH, BIRD, LIZARD
# We will permit alice to see only DOG and FISH.
# ---------------------------------------------------------------------------
stocks = dx.data.stocks(ticking=True)


# ===========================================================================
# Example 1 - raw_table: no ACL applied.
# ===========================================================================
#
# raw_table carries no EdgeAclProvider. The query owner / an admin can open it
# directly. alice CANNOT: because ACLs have been applied somewhere in this
# query (see acl_table below), `requireAclsToExport()` is true, and a table
# with no ACL marker is denied to viewers. When alice tries to fetch
# raw_table directly, she gets a TableAccessException and the
# table simply does not open for her.
raw_table = stocks


# ===========================================================================
# Example 2 - acl_table: an Edge ACL permitting alice to see DOG and FISH.
# ===========================================================================
#
# `apply_to` returns a new table. When alice opens it, the server applies the
# row ACL for alice, so she sees only the rows where Sym is DOG or FISH.
# The owner/admin opening it sees everything (admins are not subject to edge ACLs).
#
# ACLs are keyed by group. Here we grant the filter to a group named "alice".
alice_acl = (
    EdgeAclProvider.builder()
    .row_acl("alice", acl_generator.where("Sym in `DOG`, `FISH`"))
    .build()
)
acl_table = alice_acl.apply_to(raw_table)


# ===========================================================================
# Example 3 - a dh.ui dashboard showing raw_table and acl_table side by side.
# ===========================================================================
#
# A dh.ui dashboard exports its tables to the client as plugin references.
# deephaven.ui declares authorization_export_behavior="transform", so each table
# the dashboard exports is run through the authorization transform in the viewer's
# context - a dashboard table is subject to the same authorization as opening it
# directly:
#
#   - acl_table  : a table with an ACL, so the transform applies alice's row ACL ->
#                  she sees only DOG and FISH.
#
#   - raw_table  : has no ACL marker, and ACLs are required to export (an ACL is
#                  applied in this query), so the transform DENIES it for alice -
#                  exactly as when she opens it directly (Example 1).
#                  The transform raises a TableAccessException, so alice's
#                  raw_table panel shows an error rather than any data.
#
# (The owner/admin is exempt from edge ACLs and sees both panels in full.)
example3_dashboard = ui.dashboard(
    ui.row(
        ui.panel(ui.table(raw_table), title="raw_table - denied to alice"),
        ui.panel(ui.table(acl_table), title="acl_table - filtered to DOG, FISH"),
    )
)
EdgeAclProvider.add_full_access(example3_dashboard)


# ===========================================================================
# Example 4 - a @ui.component that sums the trade sizes.
# ===========================================================================
#
# We sum the Size column inside a @ui.component and show four result tables. The
# component body runs at RENDER time in the VIEWER's context, so the sum_by occurs
# under alice's context and its row ACL is applied before the sum - acl_total is
# correctly the sum over alice's DOG + FISH rows.
#
# raw_total and acl_total are *derived* tables: aggregating a table with ACLs
# yields a plain result that no longer carries an ACL marker. When
# deephaven.ui exports them through the transform, ACLs are required to export and
# neither is marked, so the transform DENIES both (raising a TableAccessException):
#
#   - raw_total  : derived from an un-ACL'd table; denied.
#   - acl_total  : correctly filtered to DOG + FISH, yet still unmarked - so also
#                  denied, even though its data is exactly what alice may see.
#
# To show a derived result, re-mark it with an ACL the transform accepts. The last
# two panels re-mark each result for the CURRENT user with
# apply_full_access_for_current_user(). That grants full access to the viewing user
# only and does NO filtering - it simply marks the result exportable to that user -
# so whatever you mark is shown. Both are legitimate, deliberate choices by the
# query writer:
#
#   - shown_acl_total : alice's own DOG + FISH total. Safe because acl_total was computed
#                       in her context and holds only her rows.
#   - shown_raw_total : the grand total over ALL syms. A valid choice to share as an
#                       AGGREGATE even though alice may not see the unsummarized rows.
#
# IMPORTANT: apply_full_access_for_current_user() asserts the data is safe to provide
# to the viewer; it adds no protection of its own. The QUERY WRITER is responsible for
# ensuring each result is properly sanitized for the user before marking it - whether
# by filtering it to the user's rows or by otherwise transforming the data.
@ui.component
def sum_sizes_component():
    raw_total = raw_table.view("Size").sum_by()
    acl_total = acl_table.view("Size").sum_by()
    # Mark each result exportable to the current viewer only. The writer decides each
    # is safe to share: acl_total holds only alice's rows, and raw_total is a grand
    # total that exposes no row-level data.
    shown_acl_total = EdgeAclProvider.apply_full_access_for_current_user(acl_total)
    shown_raw_total = EdgeAclProvider.apply_full_access_for_current_user(raw_total)
    return ui.row(
        ui.panel(ui.table(raw_total), title="raw total - denied (unmarked)"),
        ui.panel(ui.table(acl_total), title="acl total - DOG+FISH, denied (unmarked)"),
        ui.panel(ui.table(shown_acl_total), title="acl total - full access for alice"),
        ui.panel(
            ui.table(shown_raw_total),
            title="grand total - full access for alice (aggregate)",
        ),
    )


example4_dashboard = ui.dashboard(sum_sizes_component())
EdgeAclProvider.add_full_access(example4_dashboard)


# ===========================================================================
# Example 5 - an ACL that only grants access to "bob"; alice gets an error.
# ===========================================================================
#
# bob_acl grants a row filter to the group "bob" and nobody else. alice is not
# in bob's group. bob_only_table has ACLs, so when deephaven.ui exports
# it for alice the authorization transform looks up her row filters, finds none,
# and treats that as "no access" - raising a TableAccessException.
#
# Unlike opening it directly - where the denial is caught and obfuscated
# (typically surfaced as a generic not-found, so it does not reveal the table) -
# the exception raised while exporting a deephaven.ui reference propagates out as an
# Internal Server Error in alice's UI. This is the same denial as Example 3's
# raw_table.
bob_acl = (
    EdgeAclProvider.builder()
    .row_acl("bob", acl_generator.where("Sym in `CAT`"))
    .build()
)
bob_only_table = bob_acl.apply_to(stocks)


@ui.component
def bob_only_component():
    # For alice the transform denies this reference (no permitted rows) ->
    # TableAccessException -> Internal Server Error. For bob (or the owner/admin)
    # it renders normally.
    return ui.table(bob_only_table)


example5_dashboard = ui.dashboard(bob_only_component())
EdgeAclProvider.add_full_access(example5_dashboard)


# ===========================================================================
# Example 6 - a dx line plot with `by="Sym"`; object ACL does NOT re-filter.
# ===========================================================================
#
# Applying an Edge ACL to a table does not automatically make it safe for use
# with widgets or other components. We build a line plot from
# acl_table with `by="Sym"`. The `by` partitions the table NOW, at
# instantiation time, which runs in the QUERY OWNER's context. The owner
# is an admin, so sees the raw, unfiltered table; the plot
# is built over ALL syms and that result is baked into the figure.
#
# We then add an OBJECT ACL to make the plot visible to alice. Object ACLs
# only control *visibility* of the object - they do not (and cannot) re-filter
# data that was already computed. So alice sees a plot containing CAT, DOG,
# FISH, BIRD, and LIZARD, not just DOG and FISH.
#
# Importantly, an object ACL is not a substitute for an edge ACL applied in the
# viewer's context.
#
# To give alice a correctly filtered plot, build the plot INSIDE a
# @ui.component (so the `by` runs in her context, like Example 4) rather than at
# the top level - see Example 7.
line_plot = dx.line(acl_table, x="Timestamp", y="Price", by="Sym")
EdgeAclProvider.add_full_access(line_plot)


# ===========================================================================
# Example 7 - the same dx line plot, but built INSIDE a @ui.component.
# ===========================================================================
#
# This is the mitigation for Example 6:
# wrapping the plot construction in a @ui.component makes the plot do the right
# thing for edge ACLs *without the plot needing to know anything about ACLs*.
#
# The component body runs at RENDER time in the VIEWER's context. So when
# dx.line applies `by="Sym"`, the downstream operation happens under alice's
# auth context and the ACL is applied for her. alice's
# figure contains only DOG and FISH; the owner/admin still sees every sym.
#
# Contrast with Example 6, where the identical dx.line call at the top level
# operated in the owner's (admin) context and baked an unfiltered figure that
# no later object ACL could re-filter.
#
# The broader point: many widgets (Deephaven Express plots among them) were never
# designed with ACLs in mind. By composing them with deephaven.ui so that they
# execute in the user's context, when the widget accesses a table with ACLs it
# inherits correct, per-viewer filtering for free. This lets you retrofit
# ACL-correct behavior onto widgets that have no ACL awareness of their own.
@ui.component
def acl_line_plot_component():
    # dx.line's `by="Sym"` operates on acl_table here, in the viewer's context, so
    # the row ACL is applied before the figure is built. For alice this yields
    # a plot of DOG and FISH only.
    plot = dx.line(acl_table, x="Timestamp", y="Price", by="Sym")
    EdgeAclProvider.add_full_access(plot)
    return plot


example7_dashboard = ui.dashboard(acl_line_plot_component())
EdgeAclProvider.add_full_access(example7_dashboard)


# ===========================================================================
# Example 8a - reading row data from a derived top-level table: DENIED.
# ===========================================================================
#
# ui.use_row_data (like the other data-reading hooks) applies the authorization
# transform to the table it is handed, the same as exporting a table directly.
# The data it returns is only as safe as the table passed into it.
#
# Here, acl_table.tail(1) is evaluated at the top level, in the QUERY OWNER's
# context. The result is a plain derived table: it carries no EdgeAclProvider
# (ACLs are not propagated through downstream operations). Because ACLs have been
# applied somewhere in this query, requireAclsToExport() is true, and deephaven.ui
# denies the unmarked table for alice when use_row_data attempts to read it -
# she gets an error.
@ui.component
def ui_table_row(table):
    row_data = ui.use_row_data(table)
    if row_data is None:
        return ui.heading("No data yet.")
    return ui.heading(f"Row data is {row_data}. Value of Sym is {row_data['Sym']}")


example8a_dashboard = ui.dashboard(ui_table_row(acl_table.tail(1)))
EdgeAclProvider.add_full_access(example8a_dashboard)


# ===========================================================================
# Example 8b - reading row data with use_row_data; tail done inside the component.
# ===========================================================================
#
# The table with ACLs is passed in and .tail(1) is done INSIDE the component,
# at RENDER time, in the VIEWER's context. Because the tail operation is performed
# under alice's context, .tail(1) only sees her permitted rows. The result is then
# marked exportable to the current user with apply_full_access_for_current_user
# so that use_row_data can read from it. CORRECT.
#
# This demonstrates the same concept as Examples 4, 6, and 7, now for
# data-extraction hooks: perform downstream operations (here .tail) INSIDE the
# per-user component, not at the top level, so the ACL is applied in the viewer's
# context before any data is read out.
@ui.component
def ui_last_table_row(table):
    tailed_data = EdgeAclProvider.apply_full_access_for_current_user(table.tail(1))
    row_data = ui.use_row_data(tailed_data)
    if row_data is None:
        return ui.heading("No data yet.")
    return ui.heading(f"Last row data is {row_data}. Value of Sym is {row_data['Sym']}")


example8b_dashboard = ui.dashboard(ui_last_table_row(acl_table))
EdgeAclProvider.add_full_access(example8b_dashboard)


# ===========================================================================
# Example 9 - use_table_listener; the listener attaches to the table in the
#             viewer's context, so it only ever receives ACL-permitted rows.
# ===========================================================================
#
# This component subscribes to its table argument with ui.use_table_listener and,
# on each update, toasts the added rows whose Size exceeds 5000, including the Sym
# and Size - i.e. it exfiltrates row data, much like use_row_data in Example 8.
# (Sym is the column the row ACL filters on.)
#
# The subscription is created INSIDE the @ui.component, at RENDER time, in the
# VIEWER's context, so the listener attaches to a table filtered for that viewer:
# alice's listener only ever sees DOG/FISH rows, and the toast
# only shows data she is permitted to see.
@ui.component
def toast_table(t):
    render_queue = ui.use_render_queue()

    def listener_function(update, is_replay):
        # added() rows have already passed through the per-viewer
        # filter, so this only reads rows the viewer is permitted to see.
        added = update.added()
        syms = added["Sym"]
        sizes = added["Size"]
        for i in range(len(sizes)):
            # Only toast notable trades; include the size in the message.
            if sizes[i] > 5000:
                render_queue(
                    lambda sym=syms[i], size=sizes[i]: ui.toast(
                        f"{sym} traded size {size}", timeout=5000
                    )
                )

    ui.use_table_listener(t, listener_function, [t])
    return t


example9_toast_table = toast_table(acl_table)
EdgeAclProvider.add_full_access(example9_toast_table)
