"""The audit sink every account mutation calls, and the kit's hooks into it.

Sources of truth: this module, `app/models/accounts.py` (`AuditLog`), `docs/ACCOUNTS.md`.
Design: `.logs/planning/accounts-2026-09.md` §4.10.

`record()` fills the real account, the acting-as account, the session hash and the address
from the request's session (a contextvar the loader sets) whenever the caller does not say,
so the kit hook's rows carry the same two identities as every other row. It writes on its own
short connection and never raises: a failing audit must not fail the mutation that already
succeeded, which is the kit's contract too. Never a password, a token, or a typed unknown
username (it may be a password typed into the wrong box).

The one policy hook on the kit lives here: on `remove_member` the sink runs the same path as
`accounts disable`, because the kit's own remove only flips `account_members.active`, which
neither login nor the session loader reads, while the People panel's Remove button tells the
owner it revokes access.
"""

from __future__ import annotations

import logging

from sqlalchemy import select

from app import db as appdb
from app.models import Account, AuditLog
from app.services import sessions

log = logging.getLogger(__name__)


def _account_id(db, username: str | None) -> int | None:
    if not username:
        return None
    return db.scalar(select(Account.id).where(Account.username == username.strip().lower()))


def record(action: str, *, entity_type: str | None = None, entity_key: str | None = None,
           detail: dict | None = None, account_id: int | None = None,
           acting_as_id: int | None = None, session_hash: str | None = None,
           ip: str | None = None) -> None:
    """One row. Identities default to the request's session; a CLI or a route may pass them."""
    try:
        current = sessions.current.get()
        with appdb.SessionLocal() as db:
            if current is not None:
                if account_id is None:
                    account_id = current.account_id
                if acting_as_id is None and current.acting_as:
                    acting_as_id = _account_id(db, current.acting_as)
                session_hash = session_hash or current.token_hash
                ip = ip or current.ip
            db.add(AuditLog(
                account_id=account_id, acting_as_id=acting_as_id, session_hash=session_hash,
                action=action[:60], entity_type=entity_type, entity_key=(entity_key or "")[:120] or None,
                detail=detail, ip=(ip or "")[:64] or None,
            ))
            db.commit()
    except Exception:  # noqa: BLE001 - telemetry must never break the action it records
        log.exception("audit row not written: %s", action)


def kit_sink(action: str, actor: str, target: str, detail: str = "") -> None:
    """The `audit=` hook handed to `bw_accounts.init`: `add_member`, `set_member_level`,
    `create_level`, ... Target is a username for member actions, a level or instance name
    otherwise. On `remove_member` the account is also disabled and signed out everywhere."""
    entity = "level" if action.endswith("_level") or action.startswith("create_level") or action in ("set_level_def", "delete_level") else "account"
    if action in ("create_instance",):
        entity = "instance"
    record(f"kit.{action}", entity_type=entity, entity_key=target,
           detail={"actor": actor, "detail": detail} if detail else {"actor": actor})
    if action == "remove_member":
        from app.services import directory

        directory.disable_account(target, via="panel", reason="removed")


def view_as_sink(action: str, real: str, target: str, mode: str) -> None:
    """The `audit=` hook handed to `bw_view_as.verify`: only the fail-closed auto-stop reaches
    it (explicit start and stop are recorded by `services/view_as`), so the row says so."""
    record(action, entity_type="account", entity_key=target,
           detail={"real": real, "mode": mode, "auto": True})
