"""Scout's front door to the BW Auth accounts kit.

Two questions, two calls, and confusing them is the failure mode this whole
milestone can produce:

* **App-wide** — "may this person create projects at all?" → `app_can`.
* **Per-project** — "may this person see *this project's* results?" →
  `project_can`, which resolves the level for that one project first.

Resolution order for a project (the kit's, restated because it is load-bearing):
an explicit per-project grant wins; failing that the account's app-wide level
applies only if `all_instances` is set; otherwise the answer is `None`, meaning
no access to that project at all.

The owner (`rian`) is synthesized by the kit and has no level row, so
`level_def()` returns nothing for them. Every helper here short-circuits on
`is_owner` first — without that the owner silently loses every per-project
permission (D19).
"""

import logging

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

# Scout's own permission strings — canonical values live in constants.py; these
# aliases are what the rest of the app imports.
from app.constants import (
    PERM_PROJECT_MANAGE as PROJECT_MANAGE,
    PERM_PROJECT_MEMBERS as PROJECT_MEMBERS,
    PERM_PROJECTS_CREATE as PROJECTS_CREATE,
    PERM_PROJECTS_DELETE as PROJECTS_DELETE,
    PERM_RESULTS_VIEW as RESULTS_VIEW,
    PERM_REVIEW as REVIEW,
    PERM_VIEW_AS as VIEW_AS,
)
from app.constants import PERMISSION_CATALOG
from app.db import SessionLocal
from app.services.accounts_store import ScoutAccountsStore

log = logging.getLogger(__name__)

# Kit permissions Scout checks by name.
ACCOUNTS_VIEW = bwa.PERM_ACCOUNTS_VIEW
ACCOUNTS_ADD = bwa.PERM_ACCOUNTS_ADD
ACCOUNTS_CHANGE_LEVEL = bwa.PERM_ACCOUNTS_CHANGE_LEVEL
INSTANCES_GRANT = bwa.PERM_INSTANCES_GRANT

AccountsError = bwa.AccountsError

_initialized = False


def init_accounts_kit() -> None:
    """Wire the kit to Scout's storage. Called once at startup."""
    global _initialized
    bwa.init(
        owner=get_settings().scout_owner,
        has_instances=True,
        store=ScoutAccountsStore(SessionLocal),
    )
    _initialized = True


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 effective_level(username: str, project_id: int | None = None) -> str | None:
    """The level this person acts at, app-wide or within one project."""
    if project_id is None:
        return bwa.effective_level(username)
    return bwa.effective_level(username, str(project_id))


def project_can_any(username: str, project_id: int, permissions: list[str]) -> bool:
    """True when the caller's level on THIS project holds ANY of `permissions`.
    Exists because some capabilities are deliberately reachable two ways —
    membership management is allowed to both `scout.project.manage` (the broad
    level) and `scout.project.members` (the narrow one)."""
    if bwa.is_owner(username):
        return True
    level_name = bwa.effective_level(username, str(project_id))
    if not level_name:
        return False
    definition = bwa.level_def(level_name)
    if not definition:
        return False
    held = set(definition["permissions"])
    return any(p in held for p in permissions)


def project_can(username: str, project_id: int, permission: str) -> bool:
    """Per-project permission check: resolve the level for THIS project, then ask
    what that level may do.

    Returns False when the person has no level here at all, which is the same
    answer as "not a member" — callers turn that into a 404, never a 403.
    """
    if bwa.is_owner(username):
        return True
    level_name = bwa.effective_level(username, str(project_id))
    if not level_name:
        return False
    definition = bwa.level_def(level_name)
    return bool(definition and permission in definition["permissions"])


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


def level_names() -> list[str]:
    return [level["name"] for level in bwa.levels()]


def assignable_by(username: str) -> list[str]:
    """Which levels this actor may hand out APP-WIDE (from their app-wide
    level). For anything scoped to one project use `assignable_on` — a scoped
    coordinator's power comes from their per-project level, which this cannot
    see. (Kit feedback T9: bwa.assignable_by has no per-instance variant.)"""
    return bwa.assignable_by(username)


def assignable_on(username: str, project_id: int) -> list[str]:
    """Which levels this actor may hand out ON THIS PROJECT: resolved from
    their EFFECTIVE level there (grant → all_instances → none), so a per-project
    elevated level carries its own assignability. Owner → every level."""
    if bwa.is_owner(username):
        return [level["name"] for level in bwa.levels()]
    level_name = bwa.effective_level(username, str(project_id))
    if not level_name:
        return []
    definition = bwa.level_def(level_name)
    return list(definition["assignable"]) if definition else []


# ------------------------------------------------------------- level admin
# Thin pass-throughs: the kit's mutations enforce the permission model
# themselves (levels.create / levels.edit_permissions / in-use refusal), so
# these exist only to keep routers out of the kit's namespace.


def level_definitions() -> list[dict]:
    """Every level with its usage count, for the admin screen."""
    store = ScoutAccountsStore(SessionLocal)
    result = []
    for definition in bwa.levels():
        entry = dict(definition)
        entry["in_use_by"] = store.count_level_usage(definition["name"])
        result.append(entry)
    return result


def permission_catalog() -> list[dict]:
    return [dict(entry) for entry in PERMISSION_CATALOG]


def create_level(actor: str, name: str, permissions: list[str], assignable: list[str]) -> dict:
    bwa.create_level(actor, name, permissions=permissions, assignable=assignable)
    return bwa.level_def(name)


def update_level(actor: str, name: str, permissions: list[str], assignable: list[str]) -> dict:
    bwa.set_level_def(actor, name, permissions=permissions, assignable=assignable)
    return bwa.level_def(name)


def delete_level(actor: str, name: str) -> None:
    bwa.delete_level(actor, name)


def access_matrix() -> dict:
    """Users × projects with the EFFECTIVE level in each cell — the at-a-glance
    view. Cells are resolved exactly like requests are (grant → all_instances →
    None), so this table is the truth, not a parallel derivation."""
    instances = bwa.instances()
    rows = []
    for person in bwa.members():
        cells = {}
        for instance in instances:
            if instance["id"] in person["grants"]:
                cells[instance["id"]] = person["grants"][instance["id"]]
            elif person["all_instances"]:
                cells[instance["id"]] = person["level"]
            else:
                cells[instance["id"]] = None
        rows.append(
            {
                "username": person["username"],
                "level": person["level"],
                "all_instances": person["all_instances"],
                "cells": cells,
            }
        )
    return {"projects": instances, "users": rows}


# ------------------------------------------------------- central credentials
# Invites and resets go through the gateway: Scout never sees a link or a
# token, and the server scopes both to users Scout has reported.


def invite(username: str, email: str, first: str = "", last: str = "") -> dict:
    """Create/invite a BW account and email them a set-password link. Raises
    BWAuthError; the caller maps it to a structured response."""
    return bw_auth.invite_user(username, email, first=first, last=last)


def send_reset(username: str) -> None:
    bw_auth.send_reset(username)


def cross_app_access(username: str) -> list[dict]:
    """Everywhere this user has BW-app access, per the apps' own reports."""
    return bw_auth.user_access(username)


def account_url(return_to: str | None = None) -> str:
    """The central manage-account page (password change, Google link)."""
    base = get_settings().bw_auth.rstrip("/") + "/account"
    if return_to:
        from urllib.parse import quote

        base += "?return=" + quote(return_to, safe="")
    return base


def bw_status() -> dict:
    """Reachability + configuration snapshot for the Auth admin tab."""
    settings = get_settings()
    status = {
        "configured": settings.has_bw_client,
        "client_id": settings.bw_client_id if settings.has_bw_client else None,
        "auth_host": settings.bw_auth,
        "domain_mode": settings.domain_mode,
        "owner": settings.scout_owner,
        "reachable": None,
    }
    if settings.has_bw_client:
        import urllib.request

        try:
            with urllib.request.urlopen(settings.bw_auth.rstrip("/") + "/health", timeout=4) as r:
                status["reachable"] = r.status == 200
        except Exception:  # noqa: BLE001 - a health probe may fail any way it likes
            status["reachable"] = False
    return status


BWAuthError = bw_auth.BWAuthError


# ------------------------------------------------------------------ reporting
# Display-only mirrors to auth.bowden.works. Scout stays the authority (D24);
# none of this grants anything anywhere, and a failure must never break a
# request — hence the broad catches.


def report_access(username: str) -> None:
    """Publish one person's Scout access (app-wide level + per-project levels)."""
    if not get_settings().has_bw_client:
        return
    try:
        person = bwa.member(username)
        if person is None:
            bw_auth.report_access(username, None)
            return
        labels = {i["id"]: i["label"] for i in bwa.instances()}
        entries = []
        if person["all_instances"]:
            entries = [
                {"id": i["id"], "label": i["label"], "level": person["level"]}
                for i in bwa.instances()
            ]
        for instance_id, level in person["grants"].items():
            entries = [e for e in entries if e["id"] != instance_id]
            entries.append(
                {"id": instance_id, "label": labels.get(instance_id, instance_id), "level": level}
            )
        bw_auth.report_access(person["username"], person["level"], entries)
    except Exception as exc:  # noqa: BLE001 - visibility must never break a request
        log.warning("report_access failed for %s: %s", username, exc)


def report_revoked(username: str) -> None:
    """Clear someone's Scout access centrally (deactivated or removed)."""
    if not get_settings().has_bw_client:
        return
    try:
        bw_auth.report_access(username, None)
    except Exception as exc:  # noqa: BLE001
        log.warning("report_access(None) failed for %s: %s", username, exc)


def report_project_catalog() -> None:
    """Republish the project catalog. The call replaces the whole list, so it
    runs after a project is created or renamed."""
    if not get_settings().has_bw_client:
        return
    try:
        bw_auth.report_instances(bwa.instances())
    except Exception as exc:  # noqa: BLE001
        log.warning("report_instances failed: %s", exc)


def sync_all() -> None:
    """One-shot backfill of the catalog and every member. Used after the
    migration; the kit swallows its own failures here."""
    if not get_settings().has_bw_client:
        return
    bwa.sync_reports(bw_auth)
