"""The People screen — app-wide management of who exists in Scout.

Scout never creates a BW account and never sets a password; those live with the
server owner (`srv-gw id-user-create` / `id-user-set-password`). What happens
here is deciding who has a Scout account, what level they hold across the app,
and whether that level reaches every project.

Per-project levels are set on the project itself, not here.
"""

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.config import get_settings
from app.constants import DEFAULT_LEVEL
from app.db import get_db
from app.models.account import Account
from app.models.schemas import (
    AccountActiveUpdate,
    AccountLevelUpdate,
    AccountOut,
    InviteRequest,
    InviteResult,
    MemberAdd,
)
from app.services import accounts as accounts_service
from app.services import bw, levels
from app.services import projects as projects_service
from app.services.authz import (
    current_account,
    project_with_any_permission,
    require_accounts_add,
    require_accounts_admin,
    require_accounts_delete,
    require_level_changer,
)

router = APIRouter(prefix="/api/accounts", tags=["accounts"])


def _load(db: Session, username: str) -> Account:
    account = accounts_service.get(db, username)
    if account is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={"error_code": "ACCOUNT_NOT_FOUND", "summary": "No such Scout account."},
        )
    return account


def _guard_owner(username: str) -> None:
    """The owner is the standing super admin and is immutable. The kit enforces
    this too; refusing here gives a clean error instead of a kit exception."""
    if levels.is_owner(username):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "error_code": "OWNER_IMMUTABLE",
                "summary": "The app owner cannot be modified.",
                "details": "The owner is Scout's standing super admin.",
            },
        )


def _guard_assignable(actor: Account, level_name: str) -> None:
    """Refuse a level this actor may not hand out — notably, an admin cannot
    assign `admin`, which is what stops admins minting other admins."""
    if level_name not in levels.assignable_by(actor.username):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail={
                "error_code": "NOT_ASSIGNABLE",
                "summary": f"You cannot assign the '{level_name}' level.",
                "details": "Your own level decides which levels you may hand out.",
            },
        )


@router.get("", response_model=list[AccountOut])
def list_accounts(
    _admin: Account = Depends(require_accounts_admin), db: Session = Depends(get_db)
) -> list[AccountOut]:
    return [AccountOut.model_validate(a) for a in accounts_service.list_all(db)]


@router.post("", response_model=AccountOut, status_code=status.HTTP_201_CREATED)
def create_account(
    payload: MemberAdd,
    admin: Account = Depends(require_accounts_add),
    db: Session = Depends(get_db),
) -> AccountOut:
    """Give an existing BW account access to Scout.

    Resolving the username against BW is what makes this safe to do before the
    person has ever signed in: an unknown username is rejected rather than
    creating a row nobody can ever claim.
    """
    username = payload.username.strip().lower()
    level_name = (payload.level or DEFAULT_LEVEL).strip()
    _guard_assignable(admin, level_name)

    existing = accounts_service.get(db, username)
    if existing is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "error_code": "ACCOUNT_EXISTS",
                "summary": f"'{username}' already has a Scout account.",
            },
        )
    try:
        identity = bw.lookup_identity(username)
    except bw.BWAuthError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={
                "error_code": "BW_ACCOUNT_NOT_FOUND",
                "summary": f"No active BW account called '{username}'.",
                "details": (
                    "Ask the server owner to create it with `srv-gw id-user-create` "
                    "and set a password with `srv-gw id-user-set-password`."
                ),
            },
        ) from exc

    account = accounts_service.provision(
        db, identity, level=level_name, added_by=admin.username
    )
    levels.report_access(account.username)
    return AccountOut.model_validate(account)


@router.patch("/{username}/level", response_model=AccountOut)
def set_level(
    username: str,
    payload: AccountLevelUpdate,
    admin: Account = Depends(require_level_changer),
    db: Session = Depends(get_db),
) -> AccountOut:
    """Set someone's app-wide level, and optionally whether it reaches every
    project without an explicit grant."""
    target = username.strip().lower()
    _guard_owner(target)
    level_name = payload.level.strip()
    _guard_assignable(admin, level_name)

    if target == admin.username and level_name != admin.level:
        # Changing your own level is how an app ends up with nobody able to
        # administer it, and it is also the shape of a privilege-escalation bug.
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "error_code": "CANNOT_CHANGE_OWN_LEVEL",
                "summary": "You cannot change your own level.",
                "details": "Ask another admin, or the app owner.",
            },
        )

    account = _load(db, target)
    accounts_service.set_level(db, account, level_name)
    if payload.all_instances is not None:
        accounts_service.set_all_instances(db, account, payload.all_instances)
    levels.report_access(account.username)
    return AccountOut.model_validate(account)


@router.patch("/{username}/active", response_model=AccountOut)
def set_active(
    username: str,
    payload: AccountActiveUpdate,
    admin: Account = Depends(require_accounts_delete),
    db: Session = Depends(get_db),
) -> AccountOut:
    """Deactivating takes effect on the deactivated person's next request, not at
    their next login — their existing session stops working immediately, and the
    accounts kit stops seeing them as a member at all."""
    target = username.strip().lower()
    _guard_owner(target)
    if target == admin.username and not payload.active:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "error_code": "CANNOT_DEACTIVATE_SELF",
                "summary": "You cannot deactivate your own account.",
            },
        )
    account = _load(db, target)
    accounts_service.set_active(db, account, payload.active)
    if account.active:
        levels.report_access(account.username)
    else:
        levels.report_revoked(account.username)
    return AccountOut.model_validate(account)


@router.get("/me/assignable", response_model=list[str])
def my_assignable_levels(account: Account = Depends(current_account)) -> list[str]:
    """Which levels this caller may hand out, so the UI only offers what the
    server will actually accept."""
    return levels.assignable_by(account.username)


@router.post("/invite", response_model=InviteResult, status_code=status.HTTP_201_CREATED)
def invite_person(
    payload: InviteRequest,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> InviteResult:
    """Create a BW account (if new) and email them a set-password link, then
    provision them in Scout — the full "add someone who doesn't exist yet" flow.

    Two ways in, matching the two kinds of inviter:
    * app-wide `accounts.add` — the People screen;
    * OR membership powers on the target project — the scoped-coordinator flow
      (`project_id` required in that case, and the person lands on that project).

    Scout never sees the invite link; the gateway emails it directly. A BW
    account that already has a password gets no email — they are simply added.
    """
    username = payload.username.strip().lower()
    can_app_wide = levels.app_can(account.username, levels.ACCOUNTS_ADD)
    project = None
    if payload.project_id is not None:
        project = project_with_any_permission(
            db, payload.project_id, account, [levels.PROJECT_MANAGE, levels.PROJECT_MEMBERS]
        )
    if not can_app_wide and project is None:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail={
                "error_code": "PERMISSION_REQUIRED",
                "summary": "You cannot invite people app-wide.",
                "details": "Invite them onto a project you manage instead "
                "(pass project_id), or ask an admin.",
            },
        )

    app_level = (payload.level or "").strip()
    if app_level:
        _guard_assignable(account, app_level)
    project_level = (payload.project_level or "").strip()
    if project is not None:
        project_level = project_level or "reviewer"
        if project_level not in levels.assignable_on(account.username, project.id):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail={
                    "error_code": "NOT_ASSIGNABLE",
                    "summary": f"You cannot assign the '{project_level}' level on this project.",
                    "details": "Your level on this project decides what you may hand out.",
                },
            )

    if not get_settings().has_bw_client:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail={
                "error_code": "BW_AUTH_NOT_CONFIGURED",
                "summary": "Invites need BW Auth, which is not configured.",
            },
        )
    try:
        response = levels.invite(username, payload.email.strip(), payload.first, payload.last)
    except levels.BWAuthError as exc:
        raise HTTPException(
            status_code=status.HTTP_502_BAD_GATEWAY,
            detail={
                "error_code": "BW_INVITE_FAILED",
                "summary": "BW Auth refused the invite.",
                "details": str(exc),
            },
        ) from exc

    person = accounts_service.get(db, username)
    if person is None:
        identity = {
            "username": username,
            "email": payload.email.strip(),
            "first": payload.first,
            "last": payload.last,
        }
        person = accounts_service.provision(
            db, identity, level=app_level or "reviewer", added_by=account.username
        )
    elif app_level and app_level != person.level:
        # An invite must never be a side door around the level route's guards
        # (owner immutability, no self-change). The level applies only when the
        # account is CREATED here; existing accounts change level on the People
        # screen, where those guards live.
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "error_code": "ACCOUNT_EXISTS",
                "summary": f"'{username}' already has a Scout account; the level was not changed.",
                "details": "Change levels on the People screen, not through an invite.",
            },
        )

    if project is not None:
        projects_service.add_member(
            db, project.id, person.username, account.username, level=project_level
        )
    levels.report_access(person.username)

    existing_with_password = bool(response.get("existing_with_password"))
    emailed = bool(response.get("emailed_to")) and not existing_with_password
    if existing_with_password:
        summary = f"{username} already has a BW password — added without an email."
    elif emailed:
        summary = f"Invite emailed. The link lets {username} set a password (48h)."
    else:
        summary = f"{username} invited."
    return InviteResult(
        username=person.username,
        created=bool(response.get("created")),
        emailed=emailed,
        existing_with_password=existing_with_password,
        summary=summary,
    )


@router.post("/{username}/send-reset", status_code=status.HTTP_202_ACCEPTED)
def send_reset(
    username: str,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> dict:
    """Email a password-reset link: to yourself always, to others with the
    `accounts.reset_password` permission. The gateway sends the mail; the
    response never says whether an email actually went out."""
    target = username.strip().lower()
    if target != account.username and not levels.app_can(
        account.username, "accounts.reset_password"
    ):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail={
                "error_code": "PERMISSION_REQUIRED",
                "summary": "You cannot send resets for other people.",
            },
        )
    _load(db, target)  # 404 for a username Scout does not know
    if not get_settings().has_bw_client:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail={
                "error_code": "BW_AUTH_NOT_CONFIGURED",
                "summary": "Resets need BW Auth, which is not configured.",
            },
        )
    try:
        levels.send_reset(target)
    except levels.BWAuthError as exc:
        raise HTTPException(
            status_code=status.HTTP_502_BAD_GATEWAY,
            detail={
                "error_code": "BW_RESET_FAILED",
                "summary": "BW Auth refused the reset.",
                "details": str(exc),
            },
        ) from exc
    return {"sent": True, "summary": "If the account can receive resets, an email is on its way."}
