"""bw_admin_api.py — the standard BW-Auth admin API as a mountable FastAPI router.

Every Pattern B app was hand-writing the same ~15 admin endpoints (levels CRUD,
accounts list/invite/level/remove/reset, per-instance grants, the access matrix,
directory search, cross-app access, view-as) plus the same `/api/me` capability
block. This ships them once as a router factory the app mounts:

    import bw_accounts, bw_auth, bw_view_as
    from bw_admin_api import build_router

    router = build_router(
        bwa=bw_accounts, bw_auth=bw_auth, bw_view_as=bw_view_as,
        optional_user=optional_user_dep,     # () -> str | None  (your session)
        instance_obj=lambda iid: db_get_project(iid),   # -> obj|None (404 vs 403)
        session_of=lambda req: req.state.session,        # for view-as start/stop
        view_as_policy=my_can_view_as,        # (real, target) -> bool
        rank_of=bw_view_as.permission_weighted_rank_of(bw_accounts),
        prefix="/api/bw",
    )
    app.include_router(router)

HOOKS the app injects (nothing else is app-specific):
  * optional_user      — a FastAPI dependency `(request) -> str | None` returning the
                         EFFECTIVE signed-in BW username (the impersonated one while a
                         View As is active) or None. YOUR session owns identity; the
                         pack never logs anyone in. Auth-required routes 401 on None.
  * real_user          — optional `(request) -> str | None`; the REAL username while a
                         View As is active (defaults to optional_user). Audit/identity
                         follow this. Same request-scoped shape as optional_user.
  * instance_obj(iid)  — optional; returns the instance object or None. Drives the
                         404-vs-403 asymmetry: unknown id → 404, known-but-forbidden
                         → 403 (a 403 on a bad id leaks that the id exists).
  * session_of(req)    — optional; the app's mutable session mapping, for view-as
                         start/stop (only wired if given AND bw_view_as passed).
  * view_as_policy     — (real, target) -> bool; who may impersonate whom.
  * rank_of            — optional no-escalation rank (default the kit's weighted one).
  * start_hook/stop_hook — optional; the app's OWN view-as start(request, target, mode)
                         and stop(request). When given, the /view-as routes call these
                         instead of bw_view_as.start/stop directly — so app policy
                         (e.g. act-mode owner-only) AND central impersonation reporting
                         run. Without them the routes fall back to the raw drop-in
                         (no app policy, no hub reporting).

THE /api/me CONTRACT (the SPA's half of Pattern B — see PATTERN-B.md appendix):
server-decided booleans + fields the UI consumes verbatim; the UI must NEVER switch
on a level NAME. Shape is documented in `me_payload()` below.

Errors are the kit's: `{error_code, summary}` with an HTTP status mapped from the
code (FORBIDDEN/NOT_ASSIGNABLE/OWNER_IMMUTABLE→403, NO_SUCH_*→404, EXISTS→409,
else 400). FastAPI-only; the model/guards live in bw_accounts (framework-agnostic).
"""
from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request

_STATUS = {
    "FORBIDDEN": 403, "NOT_ASSIGNABLE": 403, "OWNER_IMMUTABLE": 403, "ESCALATION": 403,
    "NO_SUCH_LEVEL": 404, "NO_SUCH_MEMBER": 404, "NO_SUCH_INSTANCE": 404,
    "TARGET_NOT_FOUND": 404, "EXISTS": 409, "NO_INSTANCES": 400, "BAD_INPUT": 400,
    "NOT_INITIALIZED": 500,
}


def _raise(exc):
    """Map a bw_accounts.AccountsError / bw_view_as.ViewAsError to an HTTPException."""
    code = getattr(exc, "code", "BAD_INPUT")
    raise HTTPException(status_code=_STATUS.get(code, 400),
                        detail={"error_code": code, "summary": str(exc)})


def me_payload(bwa, username, *, real=None, bw_view_as=None, session=None,
               view_as_policy=None, account_url=None):
    """THE standard /api/me capability block, as a dict. `username` is the EFFECTIVE
    user (impersonated if a View As is active), `real` the real one. UIs consume
    these fields and never re-derive policy from level names."""
    if not username:
        # Every capability key the authenticated branch returns, all False — a
        # symmetric shape so the SPA never sees a missing key (undefined vs false).
        return {"authenticated": False, "is_owner": False, "is_staff": False,
                "can_manage_accounts": False, "can_add_accounts": False,
                "can_change_levels": False, "can_create_levels": False,
                "can_edit_levels": False, "can_create_instances": False,
                "can_view_as_others": False, "assignable_levels": [],
                "account_url": account_url,
                "impersonating": False, "viewing_as": None, "viewing_as_label": None,
                "real_user": None, "can_write": True, "view_as_mode": None}
    real = real or username
    out = {
        "authenticated": True,
        "username": username,
        "is_owner": bwa.is_owner(username),
        # Standard capability booleans — the load-bearing UI seam.
        "can_manage_accounts": bwa.can(username, bwa.PERM_ACCOUNTS_VIEW),
        "can_add_accounts": bwa.can(username, bwa.PERM_ACCOUNTS_ADD),
        "can_change_levels": bwa.can(username, bwa.PERM_ACCOUNTS_CHANGE_LEVEL),
        "can_create_levels": bwa.can(username, bwa.PERM_LEVELS_CREATE),
        "can_edit_levels": bwa.can(username, bwa.PERM_LEVELS_EDIT),
        "can_create_instances": bwa.can(username, bwa.PERM_INSTANCES_CREATE),
        "is_staff": bwa.can(username, bwa.PERM_ACCOUNTS_VIEW),
        "assignable_levels": bwa.assignable_by(username),
        "account_url": account_url,
    }
    # view-as block (mode→view_as_mode is the standard field name for the SPA)
    va = {}
    if bw_view_as is not None and session is not None:
        va = bw_view_as.me_fields(session)
        va["view_as_mode"] = va.pop("mode", None)
    out["impersonating"] = va.get("impersonating", False)
    out["viewing_as"] = va.get("viewing_as")
    out["viewing_as_label"] = va.get("viewing_as_label")
    out["real_user"] = va.get("real_user", real)
    out["can_write"] = va.get("can_write", True)
    out["view_as_mode"] = va.get("view_as_mode")
    out["can_view_as_others"] = bool(
        bw_view_as is not None and view_as_policy is not None and real
        and bwa.can(real, bwa.PERM_ACCOUNTS_VIEW_AS))
    return out


def build_router(*, bwa, optional_user, bw_auth=None, bw_view_as=None,
                 real_user=None, instance_obj=None, session_of=None,
                 view_as_policy=None, rank_of=None, target_valid=None,
                 start_hook=None, stop_hook=None,
                 account_url=None, prefix="/api/bw", audit=None):
    """Return an APIRouter with the standard admin surface. See module docstring
    for the hooks. Mount with `app.include_router(build_router(...))`."""
    router = APIRouter(prefix=prefix, tags=["bw-admin"])
    real_user = real_user or optional_user

    def _require_user(user):
        if not user:
            raise HTTPException(status_code=401,
                                detail={"error_code": "NOT_SIGNED_IN",
                                        "summary": "Sign in required."})
        return user

    def _central_new_account(username, body):
        """Create the BW account for a username that has NONE, and say how.

        Returns (note, password). Default is the emailed set-password invite.
        `delivery: "password"` is the MANUAL HANDOFF: nothing is emailed, and a
        generated password comes back ONCE for the operator to paste into their
        own message. Only ever reached for a NEW account — the caller has
        already established the username has no BW account, and the server
        refuses password delivery for an existing one regardless."""
        if not (body.get("email") or "").strip():
            raise HTTPException(status_code=400,
                                detail={"error_code": "EMAIL_REQUIRED",
                                        "summary": "A new account needs an email "
                                                   "(the invite goes there)."})
        manual = (body.get("delivery") or "email").strip().lower() == "password"
        try:
            if manual:
                fn = getattr(bw_auth, "create_user_with_password", None)
                if fn is None:      # app vendored an older bw_auth.py
                    raise HTTPException(
                        status_code=400,
                        detail={"error_code": "DELIVERY_UNSUPPORTED",
                                "summary": "Re-vendor bw_auth.py to create "
                                           "accounts with a generated password."})
                r = fn(username, body.get("email", ""),
                       body.get("first", ""), body.get("last", ""))
                return ("account created with a password", r.get("password") or "")
            r = bw_auth.invite_user(username, body.get("email", ""),
                                    body.get("first", ""), body.get("last", ""))
            return (f"invite emailed to {r.get('emailed_to', 'them')}", "")
        except HTTPException:
            raise
        except Exception as exc:
            raise HTTPException(status_code=400,
                                detail={"error_code": "INVITE_FAILED",
                                        "summary": str(exc)})

    def _instance_or_404(iid):
        if instance_obj is not None and instance_obj(iid) is None:
            raise HTTPException(status_code=404,
                                detail={"error_code": "NO_SUCH_INSTANCE",
                                        "summary": "No such instance."})

    # -- /api/me --------------------------------------------------------------
    @router.get("/me")
    def me(request: Request, user: str | None = Depends(optional_user)):
        real = real_user(request) if real_user else user
        session = session_of(request) if (session_of and user) else None
        return me_payload(bwa, user, real=real, bw_view_as=bw_view_as,
                          session=session, view_as_policy=view_as_policy,
                          account_url=account_url)

    # -- levels ---------------------------------------------------------------
    @router.get("/levels")
    def list_levels(user: str | None = Depends(optional_user)):
        _require_user(user)
        return {"levels": bwa.levels(),
                "permission_catalog": bwa.known_permissions(),
                "assignable_by_me": bwa.assignable_by(user),
                "can_edit": bwa.can(user, bwa.PERM_LEVELS_EDIT),
                "can_create": bwa.can(user, bwa.PERM_LEVELS_CREATE)}

    @router.post("/levels")
    def create_level(user: str | None = Depends(optional_user),
                     body: dict = Body(...)):
        try:
            name = bwa.create_level(_require_user(user), body.get("name", ""),
                                    body.get("permissions") or [],
                                    body.get("assignable") or [], audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        return {"success": True, "name": name}

    @router.post("/levels/adopt")
    def adopt_level(user: str | None = Depends(optional_user),
                    body: dict = Body(...)):
        try:
            name = bwa.adopt_template(_require_user(user), body.get("name", ""),
                                      audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        return {"success": True, "name": name}

    @router.patch("/levels/{name}")
    def edit_level(name: str, user: str | None = Depends(optional_user),
                   body: dict = Body(...)):
        try:
            bwa.set_level_def(_require_user(user), name,
                              permissions=body.get("permissions"),
                              assignable=body.get("assignable"), audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        return {"success": True}

    @router.delete("/levels/{name}")
    def delete_level(name: str, user: str | None = Depends(optional_user)):
        try:
            bwa.delete_level(_require_user(user), name, audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        return {"success": True}

    # -- accounts -------------------------------------------------------------
    @router.get("/accounts")
    def list_accounts(user: str | None = Depends(optional_user)):
        _require_user(user)
        if not bwa.can(user, bwa.PERM_ACCOUNTS_VIEW):
            _raise(bwa.AccountsError("You cannot view accounts.", "FORBIDDEN"))
        return {"members": bwa.members(),
                "assignable_by_me": bwa.assignable_by(user)}

    @router.get("/users/check")
    def check_user(username: str = Query("", min_length=0),
                   instance: str = Query(""),
                   user: str | None = Depends(optional_user)):
        """The add-person flow's branch point: is `username` already a member of
        this app / an existing BW account / brand new? Exact-match only — never a
        listing — and gated on the same accounts.add permission as inviting
        (resolved against `instance` when given, so in-instance managers can use
        it), so it discloses nothing the invite response wouldn't (the central
        userinfo call is rate-limited + audit-logged server-side)."""
        actor = _require_user(user)
        if not bwa.can(actor, bwa.PERM_ACCOUNTS_ADD, instance or None):
            _raise(bwa.AccountsError("You cannot add accounts.", "FORBIDDEN"))
        u = (username or "").strip().lower()
        if not u:
            raise HTTPException(status_code=400,
                                detail={"error_code": "BAD_INPUT",
                                        "summary": "A username is required."})
        m = bwa.member(u)
        if m:
            # App-wide flow: any member is "already here". Instance flow:
            # "already here" means already ON that instance — a member without
            # access to it lands in the add-existing case instead.
            already = (not instance) or bool(m.get("all_instances")
                                             or instance in (m.get("grants") or {}))
            profile = None
            if not already and bw_auth is not None:
                try:
                    profile = bw_auth.userinfo(u)
                except Exception:
                    profile = None
            return {"username": u, "member": already, "exists": True,
                    "checked": True, "profile": profile}
        if bw_auth is None:
            return {"username": u, "member": False, "exists": False,
                    "checked": False, "profile": None}
        try:
            profile = bw_auth.userinfo(u)
            return {"username": u, "member": False, "exists": True,
                    "checked": True, "profile": profile}
        except Exception:
            return {"username": u, "member": False, "exists": False,
                    "checked": True, "profile": None}

    @router.post("/accounts/invite")
    def invite(user: str | None = Depends(optional_user), body: dict = Body(...)):
        """Add a person. Two shapes behind one endpoint:
          * an EXISTING BW account → membership only, no credential email; with
            notify=true a plain "you've been given access" courtesy email.
          * a NEW username → email required; central creates the account and
            emails the set-password invite, then membership is added."""
        actor = _require_user(user)
        username = (body.get("username") or "").strip().lower()
        level = body.get("level") or ""
        notify = bool(body.get("notify"))
        try:
            # Guard BEFORE any central side effect (composite-flow rule, T8).
            if not bwa.can(actor, bwa.PERM_ACCOUNTS_ADD):
                raise bwa.AccountsError("You cannot add accounts.", "FORBIDDEN")
            if level not in bwa.assignable_by(actor):
                raise bwa.AccountsError(f"You cannot assign '{level}'.", "NOT_ASSIGNABLE")
            if bwa.member(username):
                raise bwa.AccountsError(f"'{username}' is already a member.", "EXISTS")
        except bwa.AccountsError as e:
            _raise(e)
        note = "added (no central invite wired)"
        password = ""
        if bw_auth is not None:
            exists = False
            try:
                bw_auth.userinfo(username)
                exists = True
            except Exception:
                exists = False
            if exists:
                note = "existing account added"
            else:
                note, password = _central_new_account(username, body)
        try:
            bwa.add_member(actor, username, level, audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        # The optional courtesy email for an existing account, AFTER membership is
        # real. Best-effort: a failed notification never undoes the add.
        if bw_auth is not None and notify and note == "existing account added":
            try:
                notify_fn = getattr(bw_auth, "notify_added", None)
                if notify_fn is not None:
                    notify_fn(username)
                    note += " · notified by email"
                else:
                    note += " · notification unavailable"
            except Exception:
                note += " · notification could not be sent"
        _sync(bw_auth, bwa)
        out = {"success": True, "username": username, "note": note}
        if password:
            # Returned ONCE, straight to the admin's screen. Never stored here.
            out["password"] = password
        return out

    @router.post("/accounts/{username}/level")
    def set_account_level(username: str, user: str | None = Depends(optional_user),
                          body: dict = Body(...)):
        try:
            bwa.set_member_level(_require_user(user), username,
                                 body.get("level") or "", audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        _sync(bw_auth, bwa)
        return {"success": True}

    @router.post("/accounts/{username}/all-instances")
    def set_all_instances(username: str, user: str | None = Depends(optional_user),
                          body: dict = Body(...)):
        try:
            bwa.set_all_instances(_require_user(user), username,
                                  bool(body.get("value")), audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        _sync(bw_auth, bwa)
        return {"success": True}

    @router.delete("/accounts/{username}")
    def remove_account(username: str, user: str | None = Depends(optional_user)):
        actor = _require_user(user)
        try:
            bwa.remove_member(actor, username, audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        if bw_auth is not None:
            try:
                bw_auth.report_access(username, None)   # clear central visibility
            except Exception:
                pass
        _sync(bw_auth, bwa)
        return {"success": True}

    @router.post("/accounts/{username}/send-reset")
    def send_reset(username: str, user: str | None = Depends(optional_user)):
        actor = _require_user(user)
        if not bwa.can(actor, bwa.PERM_ACCOUNTS_RESET_PW):
            _raise(bwa.AccountsError("You cannot send reset links.", "FORBIDDEN"))
        if bw_auth is None:
            raise HTTPException(status_code=400,
                                detail={"error_code": "BAD_INPUT",
                                        "summary": "bw_auth not wired."})
        try:
            bw_auth.send_reset(username)
        except Exception as exc:
            raise HTTPException(status_code=400,
                                detail={"error_code": "RESET_FAILED",
                                        "summary": str(exc)})
        return {"success": True}

    @router.post("/accounts/{username}/new-password")
    def new_password(username: str, user: str | None = Depends(optional_user)):
        """OWNER ONLY: replace a member's password with a generated one, returned
        ONCE (manual handoff — the owner pastes it into their own email).

        Two locks, deliberately independent:
          * here — only the app OWNER may do it, never a delegated admin. Handing
            someone another person's working password is acting-as, which this kit
            reserves for the owner (the same rule as act-mode View As). `user` is
            the EFFECTIVE user, so this is also refused mid-View-As.
          * central — the gateway only allows an external account whose entire
            footprint is this app, so even a compromised app cannot use this to
            open another app's door."""
        actor = _require_user(user)
        if not bwa.is_owner(actor):
            _raise(bwa.AccountsError("Only the app owner can set a new password.",
                                     "FORBIDDEN"))
        u = (username or "").strip().lower()
        if not bwa.member(u):
            _raise(bwa.AccountsError(f"'{u}' is not a member.", "NO_SUCH_MEMBER"))
        fn = getattr(bw_auth, "reset_password_generated", None) if bw_auth else None
        if fn is None:
            raise HTTPException(status_code=400,
                                detail={"error_code": "DELIVERY_UNSUPPORTED",
                                        "summary": "Re-vendor bw_auth.py to generate "
                                                   "new passwords."})
        try:
            r = fn(u)
        except Exception as exc:
            raise HTTPException(status_code=400,
                                detail={"error_code": "RESET_FAILED", "summary": str(exc)})
        if not r.get("password"):
            raise HTTPException(status_code=400,
                                detail={"error_code": "RESET_FAILED",
                                        "summary": r.get("summary") or "Reset failed."})
        # Returned once, straight to the owner's screen. Never stored here.
        return {"success": True, "username": u, "password": r["password"],
                "note": "New password set. They've been signed out everywhere."}

    # -- instances ------------------------------------------------------------
    @router.get("/instances")
    def list_instances(user: str | None = Depends(optional_user)):
        _require_user(user)
        return {"instances": bwa.instances()}

    @router.post("/instances/{iid}/members")
    def grant(iid: str, user: str | None = Depends(optional_user),
              body: dict = Body(...)):
        actor = _require_user(user)
        _instance_or_404(iid)
        try:
            bwa.grant_instance(actor, (body.get("username") or "").strip().lower(),
                               iid, body.get("level") or "", audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        _sync(bw_auth, bwa)
        return {"success": True}

    @router.post("/instances/{iid}/members/{username}/level")
    def grant_level(iid: str, username: str,
                    user: str | None = Depends(optional_user), body: dict = Body(...)):
        actor = _require_user(user)
        _instance_or_404(iid)
        try:
            bwa.grant_instance(actor, username, iid, body.get("level") or "",
                               audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        _sync(bw_auth, bwa)
        return {"success": True}

    @router.delete("/instances/{iid}/members/{username}")
    def revoke(iid: str, username: str, user: str | None = Depends(optional_user)):
        actor = _require_user(user)
        _instance_or_404(iid)
        try:
            bwa.revoke_instance(actor, username, iid, audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        _sync(bw_auth, bwa)
        return {"success": True}

    # -- the in-instance member surface (the scoped manager's view) -----------
    def _instance_or_404_strict(iid, actor):
        """404 when the instance is unknown OR the actor can't see it at all —
        never confirm an invisible id (asymmetry rule). Owner and app-wide
        accounts.view holders see every instance."""
        _instance_or_404(iid)
        inst = next((i for i in bwa.instances() if i["id"] == iid), None)
        if inst is None:
            raise HTTPException(status_code=404,
                                detail={"error_code": "NO_SUCH_INSTANCE",
                                        "summary": "No such instance."})
        visible = (bwa.is_owner(actor)
                   or bwa.can(actor, bwa.PERM_ACCOUNTS_VIEW)
                   or bwa.effective_level(actor, iid) is not None)
        if not visible:
            raise HTTPException(status_code=404,
                                detail={"error_code": "NO_SUCH_INSTANCE",
                                        "summary": "No such instance."})
        return inst

    @router.get("/instances/{iid}/members")
    def instance_members(iid: str, user: str | None = Depends(optional_user)):
        """Who's on THIS instance, with per-instance capabilities for the caller
        — the payload an instance page's members widget renders. Anyone with
        access to the instance may see its roster; the can_* booleans (decided
        against THIS instance, T9) drive the editing controls."""
        actor = _require_user(user)
        inst = _instance_or_404_strict(iid, actor)
        rows = []
        for m in bwa.members():
            if iid in (m.get("grants") or {}):
                rows.append({"username": m["username"],
                             "level": m["grants"][iid], "via": "grant"})
            elif m.get("all_instances"):
                rows.append({"username": m["username"],
                             "level": m["level"], "via": "all"})
        return {"instance": inst, "members": rows,
                "can_manage": bwa.can(actor, bwa.PERM_INSTANCES_GRANT, iid),
                "can_add": bwa.can(actor, bwa.PERM_ACCOUNTS_ADD, iid),
                "assignable": bwa.assignable_by(actor, iid)}

    @router.post("/instances/{iid}/invite")
    def instance_invite(iid: str, user: str | None = Depends(optional_user),
                        body: dict = Body(...)):
        """Add a person to THIS instance — the manager-inside-a-project flow.
        Same two shapes as /accounts/invite, but every guard resolves against
        the instance (T9), so a scoped manager needs NO app-wide permission:
          * existing BW account → membership (if new to the app, at this level
            with all_instances=False) + the grant; notify=true sends the
            courtesy email.
          * new username → email required; central set-password invite, then
            membership + grant."""
        actor = _require_user(user)
        _instance_or_404_strict(iid, actor)
        username = (body.get("username") or "").strip().lower()
        level = body.get("level") or ""
        notify = bool(body.get("notify"))
        try:
            # Guards BEFORE any central side effect (T8), resolved per-instance.
            existing_member = bwa.member(username)
            if existing_member:
                if not bwa.can(actor, bwa.PERM_INSTANCES_GRANT, iid):
                    raise bwa.AccountsError("You cannot grant on this instance.",
                                            "FORBIDDEN")
            else:
                if not bwa.can(actor, bwa.PERM_ACCOUNTS_ADD, iid):
                    raise bwa.AccountsError("You cannot add people here.",
                                            "FORBIDDEN")
            if level not in bwa.assignable_by(actor, iid):
                raise bwa.AccountsError(f"You cannot assign '{level}'.",
                                        "NOT_ASSIGNABLE")
            if existing_member and iid in (existing_member.get("grants") or {}):
                raise bwa.AccountsError(f"'{username}' is already on this instance.",
                                        "EXISTS")
        except bwa.AccountsError as e:
            _raise(e)
        # An existing APP member needs no central involvement — they're known;
        # this is just a grant. Only a username new to the app consults central.
        note = "added"
        password = ""
        can_notify = bool(existing_member)
        if bw_auth is not None and not existing_member:
            exists_centrally = False
            try:
                bw_auth.userinfo(username)
                exists_centrally = True
            except Exception:
                exists_centrally = False
            if exists_centrally:
                note = "existing account added"
                can_notify = True
            else:
                note, password = _central_new_account(username, body)
        try:
            bwa.add_member_to_instance(actor, username, iid, level, audit=audit)
        except bwa.AccountsError as e:
            _raise(e)
        if bw_auth is not None and notify and can_notify:
            try:
                notify_fn = getattr(bw_auth, "notify_added", None)
                if notify_fn is not None:
                    notify_fn(username)
                    note += " · notified by email"
            except Exception:
                note += " · notification could not be sent"
        _sync(bw_auth, bwa)
        out = {"success": True, "username": username, "note": note}
        if password:
            out["password"] = password      # once, to the admin's screen only
        return out

    # -- matrix + directory + self -------------------------------------------
    @router.get("/access-matrix")
    def matrix(user: str | None = Depends(optional_user)):
        _require_user(user)
        if not bwa.can(user, bwa.PERM_ACCOUNTS_VIEW):
            _raise(bwa.AccountsError("You cannot view accounts.", "FORBIDDEN"))
        return bwa.access_matrix()

    @router.get("/users/search")
    def search_users(q: str = Query("", min_length=0),
                     limit: int = Query(8, ge=1, le=20),
                     instance: str = Query(""),
                     user: str | None = Depends(optional_user)):
        # `instance` scopes the GATE (accounts.add resolved against that
        # instance, T9) so an in-instance manager can use the picker; the
        # directory's privacy scope itself stays server-enforced per app.
        actor = _require_user(user)
        if not bwa.can(actor, bwa.PERM_ACCOUNTS_ADD, instance or None):
            _raise(bwa.AccountsError("You cannot search the directory.", "FORBIDDEN"))
        if bw_auth is None:
            return {"users": []}
        try:
            return {"users": bw_auth.search_users(q, limit=limit)}
        except Exception as exc:
            raise HTTPException(status_code=400,
                                detail={"error_code": "SEARCH_FAILED",
                                        "summary": str(exc)})

    @router.get("/my-access")
    def my_access(user: str | None = Depends(optional_user)):
        u = _require_user(user)
        if bw_auth is None:
            return {"access": []}
        try:
            return {"access": bw_auth.user_access(u)}
        except Exception:
            return {"access": []}

    @router.post("/my/send-reset")
    def my_send_reset(user: str | None = Depends(optional_user)):
        u = _require_user(user)
        if bw_auth is not None:
            try:
                bw_auth.send_reset(u)
            except Exception:
                pass
        return {"success": True}

    @router.post("/sync")
    def sync(user: str | None = Depends(optional_user)):
        actor = _require_user(user)
        if not bwa.is_owner(actor):
            _raise(bwa.AccountsError("Owner only.", "FORBIDDEN"))
        _sync(bw_auth, bwa)
        return {"success": True}

    # -- view-as (only if wired) ---------------------------------------------
    if bw_view_as is not None and session_of is not None and view_as_policy is not None:
        @router.get("/view-as/targets")
        def va_targets(user: str | None = Depends(optional_user)):
            u = _require_user(user)
            if not bwa.can(u, bwa.PERM_ACCOUNTS_VIEW_AS):
                _raise(bwa.AccountsError("You cannot view as others.", "FORBIDDEN"))
            # candidates = members this actor may impersonate under the policy
            out = [m["username"] for m in bwa.members()
                   if m["username"] != u and view_as_policy(u, m["username"])]
            return {"targets": out}

        @router.post("/view-as/start")
        def va_start(request: Request, user: str | None = Depends(optional_user),
                     body: dict = Body(...)):
            real = real_user(request) if real_user else user
            _require_user(real)
            try:
                if start_hook is not None:
                    # The app's own start(): applies app policy (e.g. act-mode
                    # owner-only) AND reports the impersonation to central.
                    start_hook(request, body.get("target", ""),
                               body.get("mode", "readonly"))
                else:
                    bw_view_as.start(session_of(request), body.get("target", ""),
                                     can_view_as=view_as_policy, rank_of=rank_of,
                                     target_valid=target_valid,
                                     mode=body.get("mode", "readonly"), audit=audit)
            except bw_view_as.ViewAsError as e:
                _raise(e)
            return {"success": True}

        @router.post("/view-as/stop")
        def va_stop(request: Request, user: str | None = Depends(optional_user)):
            if stop_hook is not None:
                stop_hook(request)   # app's own stop(): clears the central row too
            else:
                bw_view_as.stop(session_of(request), audit=audit)
            return {"success": True}

    return router


def _sync(bw_auth, bwa):
    """Best-effort central re-report after a mutation; never breaks the response."""
    if bw_auth is None:
        return
    try:
        bwa.sync_reports(bw_auth)
    except Exception:
        pass
