"""bw_view_as.py — standard "View As" (impersonation) for BW-Auth (Pattern B) apps.

Drop this in alongside bw_auth.py. It gives you the MECHANISM (session state,
effective-user resolution, a themed banner + picker, and the security guards) and
leaves you exactly TWO app-specific hooks to implement:

    can_view_as(real_user, target_user) -> bool      # your authorization policy
    viewable_targets(real_user) -> list[dict]         # who to show in the picker

"Super Admins can view anyone" / "Managers can view their clients" is *your* policy —
you express it in those two functions (Pattern B keeps roles + relationships in the
app). Everything else — starting/stopping, the un-hideable banner, read-only-by-
default, no-privilege-escalation, and reporting the impersonation to the BW hub so the
owner can see it — is standard and improved in ONE place (here). Update this file, re-
copy it into your apps, and the improvement rolls out.

State lives in YOUR app session (a dict-like you pass in). Your app already stores
`session['user']` (the real BW username); this adds `session['bw_acting_as']`. Use
`effective_username(session)` everywhere you'd use the current user for DATA and
PERMISSIONS; use `real_username(session)` only for the "return to yourself" control and
audit. Reads follow the effective user; WRITES are blocked by default while
impersonating (`mode='readonly'`) unless the app explicitly starts `mode='act'`.

Security model (this is a sharp tool — treat it like one):
  * Authorization is ALWAYS re-checked server-side in start(); never trust a client-
    supplied target.
  * Read-only by default. `mode='act'` (take real actions as the target) is an explicit
    per-call opt-in; gate your write handlers on can_write(session).
  * No privilege escalation: pass rank_of and start() refuses an equal/higher target,
    so "view as" can never let you do something you couldn't already do.
  * Audit every start/stop in your app, and call bw_auth.report_impersonation(...) so
    the hub shows the owner who is currently viewing as whom, across all apps.
"""
import html

ACTING_KEY = 'bw_acting_as'
MODE_KEY = 'bw_acting_mode'        # 'readonly' (default) | 'act'


class ViewAsError(Exception):
    """start() was refused — show the message to the user, keep the real session."""


def start(session, target, *, can_view_as, rank_of=None, mode='readonly'):
    """Begin impersonation, AUTHORIZED SERVER-SIDE.

    Args:
        session:      your app's session (a mutable mapping).
        target:       the BW username to view as (from the picker — re-checked here).
        can_view_as:  can_view_as(real, target) -> bool. YOUR policy. REQUIRED.
        rank_of:      optional rank_of(username) -> int. If given, refuses a target of
                      EQUAL or HIGHER rank (no escalation). Strongly recommended.
        mode:         'readonly' (default; writes blocked) or 'act' (real actions).

    Returns the (normalized) target on success. Raises ViewAsError if not signed in /
    self / unauthorized / would escalate.
    """
    real = (session.get('user') or '').strip().lower()
    if not real:
        raise ViewAsError('You are not signed in.')
    target = (target or '').strip().lower()
    if not target or target == real:
        raise ViewAsError('Pick a different user to view as.')
    if not can_view_as(real, target):
        raise ViewAsError('You are not allowed to view as this user.')
    if rank_of is not None and rank_of(target) >= rank_of(real):
        raise ViewAsError('You cannot view as a user with equal or higher privileges.')
    if mode not in ('readonly', 'act'):
        mode = 'readonly'
    session[ACTING_KEY] = target
    session[MODE_KEY] = mode
    return target


def stop(session):
    """Return to the real user. Idempotent."""
    session.pop(ACTING_KEY, None)
    session.pop(MODE_KEY, None)


def real_username(session):
    """The actually-signed-in BW user — owns the 'return' control + audit."""
    return session.get('user')


def effective_username(session):
    """The user the app should act as for DATA + PERMISSIONS (the impersonated user
    while viewing-as, else the real user). Use THIS everywhere in app logic."""
    return session.get(ACTING_KEY) or session.get('user')


def is_impersonating(session):
    return bool(session.get(ACTING_KEY))


def mode(session):
    return session.get(MODE_KEY) or 'readonly'


def can_write(session):
    """False while impersonating in read-only mode — gate your write/POST handlers on
    this so a 'view as' can't accidentally take actions as someone else."""
    return (not is_impersonating(session)) or session.get(MODE_KEY) == 'act'


# --- the standard, un-hideable UI (theme-consistent; override the CSS if you like) ---

BANNER_CSS = """
.bw-viewas-banner{position:fixed;left:0;right:0;bottom:0;z-index:2147483647;
 font-family:system-ui,-apple-system,sans-serif;font-size:14px;padding:10px 16px;
 text-align:center;color:#1f2937;background:#fde68a;border-top:2px solid #d97706;
 box-shadow:0 -2px 10px rgba(0,0,0,.18)}
.bw-viewas-banner a{color:#92400e;font-weight:700;text-decoration:underline;margin-left:10px}
.bw-viewas-banner .ro{opacity:.75;font-weight:600;margin-left:6px}
.bw-viewas-picker{display:inline-flex;gap:8px;align-items:center;font:inherit}
body:has(.bw-viewas-banner){padding-bottom:52px}  /* keep the banner off the content */
"""


def banner_html(session, *, stop_url='/view-as/stop', label=None):
    """The always-visible impersonation banner (render it on every page). Returns ''
    when not impersonating. `label` optionally maps a username to a display name."""
    if not is_impersonating(session):
        return ''
    who = session.get(ACTING_KEY)
    shown = html.escape(str(label(who) if label else who))
    ro = '<span class="ro">(read-only)</span>' if mode(session) != 'act' else ''
    return (f'<div class="bw-viewas-banner" role="status">Viewing as <b>{shown}</b> {ro}'
            f'<a href="{html.escape(stop_url)}">Return to yourself</a></div>')


def picker_html(targets, *, start_url='/view-as/start', csrf=None):
    """A minimal 'View As' picker built from viewable_targets(real). Render it only for
    a user allowed to impersonate (your template decides). Each target is a dict
    {'username': ..., 'label': ...}. POSTs {target} (+ optional csrf) to start_url."""
    if not targets:
        return ''
    opts = ''.join(
        f'<option value="{html.escape(t["username"])}">'
        f'{html.escape(t.get("label") or t["username"])}</option>' for t in targets)
    csrf_field = (f'<input type="hidden" name="csrf" value="{html.escape(csrf)}">'
                  if csrf else '')
    return (f'<form class="bw-viewas-picker" method="POST" action="{html.escape(start_url)}">'
            f'{csrf_field}<label>View as: <select name="target">{opts}</select></label>'
            f'<button type="submit">Go</button></form>')


# Tiny self-check: the mechanism + guards, no I/O.
def _selfcheck():
    s = {'user': 'rian'}
    allow_all = lambda real, tgt: True
    rank = {'rian': 100, 'adi': 50, 'jane': 10}.get
    start(s, 'jane', can_view_as=allow_all, rank_of=rank)
    assert effective_username(s) == 'jane' and real_username(s) == 'rian'
    assert is_impersonating(s) and not can_write(s)          # read-only default
    stop(s); assert effective_username(s) == 'rian' and not is_impersonating(s)
    start(s, 'adi', can_view_as=allow_all, rank_of=rank, mode='act')
    assert can_write(s)                                       # act mode allows writes
    stop(s)
    for bad, why in [('rian', 'self'), ('', 'empty')]:
        try:
            start(s, bad, can_view_as=allow_all); raise AssertionError(f'accepted {why}')
        except ViewAsError:
            pass
    try:  # no escalation: adi (50) can't view rian (100)
        start({'user': 'adi'}, 'rian', can_view_as=allow_all, rank_of=rank)
        raise AssertionError('allowed escalation')
    except ViewAsError:
        pass
    try:  # policy refusal
        start(s, 'jane', can_view_as=lambda r, t: False); raise AssertionError('ignored policy')
    except ViewAsError:
        pass
    print('bw_view_as self-check OK: effective/real, read-only default, act mode, '
          'no-self, no-escalation, policy-refusal')


if __name__ == '__main__':
    _selfcheck()
