"""Who the caller is, and what they are allowed to touch.

Access resolves through the BW Auth accounts kit (see `app/services/levels.py`):
an app-wide level decides what someone may do across Scout, and a per-project
level decides what they may do inside one project. Membership plus level is the
access boundary, and it is enforced here — never in a router, and never in the
frontend.
"""

from fastapi import Depends, HTTPException, Request, status
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.db import get_db
from app.models.account import Account
from app.models.project import Project, ProjectMember
from app.services import levels
from app.services import view_as as view_as_service

SESSION_USER_KEY = "user"

_NOT_FOUND = {"error_code": "PROJECT_NOT_FOUND", "summary": "Project not found."}


def session_username(request: Request) -> str | None:
    """The EFFECTIVE username for this request.

    Runs the view-as re-authorization first (fail-closed: an impersonation that
    is no longer allowed auto-stops here), then returns the impersonated user
    while a view-as is active, else the real one. Everything downstream —
    capabilities, visibility, membership — therefore answers for the effective
    user, which is the entire point of View As. Audit and identity use
    `real_username`, never this.
    """
    value = request.session.get(SESSION_USER_KEY)
    if not (isinstance(value, str) and value):
        return None
    effective = view_as_service.verify_and_effective(request)
    return effective.lower() if effective else None


def real_username(request: Request) -> str | None:
    """The actually-signed-in user, for audit lines and view-as controls."""
    value = request.session.get(SESSION_USER_KEY)
    return value.lower() if isinstance(value, str) and value else None


def optional_account(request: Request, db: Session = Depends(get_db)) -> Account | None:
    """The signed-in account, or None. Used only by the public /api/me."""
    username = session_username(request)
    if not username:
        return None
    account = db.get(Account, username)
    if account is None or not account.active:
        return None
    return account


def current_account(request: Request, db: Session = Depends(get_db)) -> Account:
    """The signed-in account. 401 when there is no app session, or when the
    account was deactivated after the session was issued — deactivation takes
    effect on the next request, not at the next login."""
    username = session_username(request)
    if not username:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail={"error_code": "NOT_AUTHENTICATED", "summary": "Sign in to continue."},
        )
    account = db.get(Account, username)
    if account is None or not account.active:
        request.session.clear()
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail={
                "error_code": "ACCOUNT_INACTIVE",
                "summary": "This account no longer has access to Scout.",
            },
        )
    return account


def require_permission(permission: str):
    """Dependency factory for an APP-WIDE permission. Per-project questions go
    through `project_with_permission` instead — the two are not interchangeable
    (see levels.py)."""

    def dependency(account: Account = Depends(current_account)) -> Account:
        if not levels.app_can(account.username, permission):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail={
                    "error_code": "PERMISSION_REQUIRED",
                    "summary": "You do not have permission to do that.",
                    "details": f"This action needs the '{permission}' permission.",
                },
            )
        return account

    return dependency


# The app-wide gates, named once so routers read declaratively. There is
# deliberately no general "require_admin": anything scoped to one project must
# resolve its permission against THAT project via `project_with_permission`,
# never against an app-wide level.
require_project_creator = require_permission(levels.PROJECTS_CREATE)
require_accounts_admin = require_permission(levels.ACCOUNTS_VIEW)
require_accounts_add = require_permission("accounts.add")
require_level_changer = require_permission("accounts.change_level")
require_accounts_delete = require_permission("accounts.delete")


def is_member(db: Session, project_id: int, username: str) -> bool:
    return db.get(ProjectMember, (project_id, username)) is not None


def visible_projects(db: Session, account: Account) -> list[Project]:
    """Every project this person has any level on.

    Someone with `all_instances` (staff) sees everything; everyone else sees the
    projects they hold an explicit grant on.
    """
    stmt = select(Project).order_by(Project.created_at.desc())
    if levels.is_owner(account.username) or account.all_instances:
        return list(db.scalars(stmt))
    stmt = stmt.join(ProjectMember, ProjectMember.project_id == Project.id).where(
        ProjectMember.username == account.username
    )
    return list(db.scalars(stmt))


def readable_project(db: Session, project_id: int, account: Account) -> Project:
    """Load a project the caller may see at all, or 404.

    A project the caller cannot see returns 404 rather than 403 on purpose: a 403
    would confirm that the id exists, which leaks the client list to any signed-in
    user willing to enumerate.
    """
    project = db.get(Project, project_id)
    if project is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=_NOT_FOUND)
    if not levels.has_project_access(account.username, project.id):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=_NOT_FOUND)
    return project


def project_with_permission(
    db: Session, project_id: int, account: Account, permission: str
) -> Project:
    """Load a project this caller may perform `permission` on.

    The two failures are deliberately different: no access at all is a 404 (the
    project's existence stays secret), while access at a level that lacks this
    particular permission is a 403 (they already know it exists).
    """
    return project_with_any_permission(db, project_id, account, [permission])


def project_with_any_permission(
    db: Session, project_id: int, account: Account, permissions: list[str]
) -> Project:
    """Same contract as `project_with_permission`, satisfied by ANY of the given
    permissions — for capabilities deliberately reachable two ways, like
    membership management (`scout.project.manage` OR `scout.project.members`)."""
    project = readable_project(db, project_id, account)
    if not levels.project_can_any(account.username, project.id, permissions):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail={
                "error_code": "PERMISSION_REQUIRED",
                "summary": "You do not have permission to do that on this project.",
                "details": "Your level on this project lacks "
                + " or ".join(f"'{p}'" for p in permissions) + ".",
            },
        )
    return project
