"""The app's front door to the BW-Auth accounts kit.

The kit (`bw_accounts`) owns the model — levels, per-level permissions, members,
instances, grants, the owner-is-super-admin rule — and the admin API comes from
`bw_admin_api.build_router`. This module only WIRES the kit to storage and exposes
the handful of resolvers the app's own routes and middleware need. Two questions
that are never interchangeable:

  * **app-wide** — "may this person do X across the app?" → `app_can`.
  * **per-instance** — "may this person do X on THIS instance?" → `instance_can`,
    which resolves their level on that one instance first (grant → all-instances →
    none). Confusing the two is the classic failure; keep them separate.

The default store is the kit's stdlib SQLite store — no ORM, no migrations, one
file under the mounted data dir. An app with its own database swaps this one call
for `bw_store_sqlalchemy.managed(...)` / `.mapped(...)` (see PACKS.md); nothing
else here changes.
"""

import logging

from app import bw_accounts as bwa
from app import bw_config
from app.config import get_settings

log = logging.getLogger(__name__)

AccountsError = bwa.AccountsError

# Kit permission names the app references directly.
PERM_INSTANCES_CREATE = bwa.PERM_INSTANCES_CREATE

# Easel's own permission vocabulary — registered with the kit so the Levels
# editor shows it as enforced. Seeded onto `admin` (bw-app.toml). Covers the
# whole manager side: screens/options, uploads, walkthrough authoring, resolve,
# and presenting ("send") a project.
PERM_PROJECTS_MANAGE = "projects.manage"

_initialized = False


def init_accounts_kit() -> None:
    """Wire the kit to storage and seed the default levels. Called once at
    startup, before anything asks a permission question."""
    global _initialized
    # Managed mode (PACKS.md): the kit's four bw_* tables live in the app's own
    # database, on the app's Base.metadata — so Alembic owns them alongside the
    # domain tables and there is exactly one database to think about.
    from app import bw_store_sqlalchemy as bwstore
    from app.db import get_engine, get_session_factory
    from app.models import Base

    store = bwstore.managed(Base.metadata, get_session_factory())
    bwa.init(
        store=store,
        owner=bw_config.OWNER,
        has_instances=bw_config.HAS_INSTANCES,
        audit=audit,
    )
    bwa.register_enforced(PERM_PROJECTS_MANAGE)
    # SQLite (tests / no-container local runs) creates the schema right here;
    # Postgres NEVER does — Alembic owns the prod schema, and a create_all
    # there would mask a missing migration instead of failing loudly.
    engine = get_engine()
    if engine.dialect.name == "sqlite":
        Base.metadata.create_all(engine)
    _seed_levels()
    _initialized = True


def _seed_levels() -> None:
    """Create the configured seed levels, but only when none exist yet — so this
    never fights a live app whose levels have since been edited in the UI."""
    if bwa.levels():
        return
    for name, spec in bw_config.SEED_LEVELS.items():
        try:
            bwa.create_level(
                bw_config.OWNER, name,
                permissions=list(spec.get("permissions", [])),
                assignable=list(spec.get("assignable", [])),
            )
        except AccountsError as exc:  # pragma: no cover - idempotent seeding
            log.warning("seed level %s skipped: %s", name, exc)


def audit(action: str, actor: str, target: str, detail: str = "") -> None:
    """Best-effort audit sink handed to every kit mutation (G5)."""
    log.info("audit %s actor=%s target=%s %s", action, actor, target, detail)


# ---------------------------------------------------------------- resolvers

def is_owner(username: str) -> bool:
    return bwa.is_owner(username)


def app_can(username: str, permission: str) -> bool:
    """App-wide permission check. The owner always passes."""
    return bwa.can(username, permission)


def instance_can(username: str, instance_id: str, permission: str) -> bool:
    """Per-instance permission check: resolve the caller's level on THIS instance,
    then ask what it may do. False also means 'not a member here' — callers turn
    that into a 404, never a 403."""
    return bwa.can(username, permission, instance_id=instance_id)


def effective_level(username: str, instance_id: str | None = None) -> str | None:
    return bwa.effective_level(username, instance_id)


def has_instance_access(username: str, instance_id: str) -> bool:
    """Whether this person can see the instance at all, at any level."""
    return bwa.is_owner(username) or bwa.effective_level(username, instance_id) is not None


def instance_obj(instance_id: str):
    """The 404-vs-403 hook for `build_router`: the instance dict, or None when the
    id does not exist (so an unknown id is a 404, never a 403 that would confirm
    the id exists)."""
    return next((i for i in bwa.instances() if i["id"] == instance_id), None)


def visible_instances(username: str) -> list[dict]:
    """Every instance this person may see: staff/owner see all, everyone else sees
    the ones they hold a level on. The app's instance-list route renders this."""
    if is_owner(username):
        return list(bwa.instances())
    member = bwa.member(username)
    all_instances = bool(member and member.get("all_instances"))
    out = []
    for inst in bwa.instances():
        if all_instances or has_instance_access(username, inst["id"]):
            out.append(inst)
    return out
