"""Scouting projects and their membership.

Every route here is protected: the framework-level default-deny requires a
session, and each handler additionally resolves what this particular caller may
do. Two different questions are asked, and they are not interchangeable —
creating a project is an app-wide permission, while managing one is resolved
against that specific project (see `app/services/levels.py`).
"""

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

from app.constants import DEFAULT_LEVEL
from app.db import get_db
from app.models.account import Account
from app.models.project import Project
from app.models.schemas import (
    MemberAdd,
    MemberLevelUpdate,
    MemberOut,
    ProjectCreate,
    ProjectOut,
    ProjectUpdate,
)
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,
    project_with_permission,
    readable_project,
    require_project_creator,
    visible_projects,
)

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


def _to_out(project: Project, account: Account, member_count: int = 0) -> ProjectOut:
    """Serialize a project together with what THIS caller may do to it, so the
    UI never has to re-derive policy from a level name."""
    out = ProjectOut.model_validate(project)
    out.member_count = member_count
    out.my_level = levels.effective_level(account.username, project.id)
    out.can_manage = levels.project_can(account.username, project.id, levels.PROJECT_MANAGE)
    out.can_manage_members = levels.project_can_any(
        account.username, project.id, [levels.PROJECT_MANAGE, levels.PROJECT_MEMBERS]
    )
    out.can_view_results = levels.project_can(account.username, project.id, levels.RESULTS_VIEW)
    out.can_delete = levels.project_can(account.username, project.id, levels.PROJECTS_DELETE)
    return out


def _member_out(member, person: Account) -> MemberOut:
    return MemberOut(
        username=member.username,
        display_name=person.display_name,
        email=person.email,
        level=member.level,
        account_level=person.level,
        all_instances=person.all_instances,
        active=person.active,
        added_by=member.added_by,
        added_at=member.added_at,
    )


def _require_assignable(actor: Account, level_name: str, project_id: int) -> None:
    """Refuse a level this actor may not hand out ON THIS PROJECT.

    Assignability resolves from the actor's EFFECTIVE level here — a scoped
    coordinator's power comes from their per-project level, which the app-wide
    `assignable_by` cannot see (that gap broke the coordinator flow; the test
    suite caught it). An `admin` still cannot assign `admin`: the kit's
    assignable lists are the single policy either way.
    """
    if level_name not in levels.assignable_on(actor.username, project_id):
        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[ProjectOut])
def list_projects(
    account: Account = Depends(current_account), db: Session = Depends(get_db)
) -> list[ProjectOut]:
    projects = visible_projects(db, account)
    counts = projects_service.member_counts(db, [p.id for p in projects])
    return [_to_out(p, account, counts.get(p.id, 0)) for p in projects]


@router.post("", response_model=ProjectOut, status_code=status.HTTP_201_CREATED)
def create_project(
    payload: ProjectCreate,
    admin: Account = Depends(require_project_creator),
    db: Session = Depends(get_db),
) -> ProjectOut:
    project = projects_service.create(
        db,
        name=payload.name,
        client_name=payload.client_name,
        client_website=payload.client_website,
        brief=payload.brief,
        status=payload.status,
        created_by=admin.username,
    )
    # The project catalog is republished wholesale, so central visibility picks
    # up the new project without a separate per-project call.
    levels.report_project_catalog()
    return _to_out(project, admin, 0)


@router.get("/{project_id}", response_model=ProjectOut)
def get_project(
    project_id: int,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> ProjectOut:
    project = readable_project(db, project_id, account)
    counts = projects_service.member_counts(db, [project.id])
    return _to_out(project, account, counts.get(project.id, 0))


@router.patch("/{project_id}", response_model=ProjectOut)
def update_project(
    project_id: int,
    payload: ProjectUpdate,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> ProjectOut:
    project = project_with_permission(db, project_id, account, levels.PROJECT_MANAGE)
    projects_service.update(db, project, **payload.model_dump(exclude_unset=True))
    if "name" in payload.model_dump(exclude_unset=True):
        levels.report_project_catalog()  # the label changed
    counts = projects_service.member_counts(db, [project.id])
    return _to_out(project, account, counts.get(project.id, 0))


@router.get("/{project_id}/members", response_model=list[MemberOut])
def list_members(
    project_id: int,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> list[MemberOut]:
    project = readable_project(db, project_id, account)
    return [
        _member_out(member, person)
        for member, person in projects_service.members(db, project.id)
    ]


@router.post("/{project_id}/members", response_model=MemberOut, status_code=status.HTTP_201_CREATED)
def add_member(
    project_id: int,
    payload: MemberAdd,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> MemberOut:
    """Grant someone a level on this project, by BW username.

    The username is resolved against BW first: that both proves the account
    exists and gives Scout a real name to show, so someone can be added before
    they have ever signed in to Scout.
    """
    project = project_with_any_permission(
        db, project_id, account, [levels.PROJECT_MANAGE, levels.PROJECT_MEMBERS]
    )
    username = payload.username.strip().lower()
    level_name = (payload.level or DEFAULT_LEVEL).strip()
    _require_assignable(account, level_name, project.id)

    person = accounts_service.get(db, username)
    if person is None:
        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`, then add them again."
                    ),
                },
            ) from exc
        # The app-wide level stays at the default; project access comes from the
        # grant below. Someone is only staff if an admin says so on the People
        # screen, never as a side effect of being added to a project.
        person = accounts_service.provision(
            db, identity, level=DEFAULT_LEVEL, added_by=account.username
        )

    if not person.active:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "error_code": "ACCOUNT_INACTIVE",
                "summary": f"'{username}' is deactivated in Scout.",
                "details": "Reactivate the account on the People screen, then add them again.",
            },
        )

    member = projects_service.add_member(
        db, project.id, person.username, account.username, level=level_name
    )
    levels.report_access(person.username)
    return _member_out(member, person)


@router.patch("/{project_id}/members/{username}", response_model=MemberOut)
def set_member_level(
    project_id: int,
    username: str,
    payload: MemberLevelUpdate,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> MemberOut:
    """Change what someone may do on THIS project, leaving their standing on
    every other project untouched."""
    project = project_with_any_permission(
        db, project_id, account, [levels.PROJECT_MANAGE, levels.PROJECT_MEMBERS]
    )
    target = username.strip().lower()
    level_name = payload.level.strip()
    _require_assignable(account, level_name, project.id)

    if levels.is_owner(target):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "error_code": "OWNER_IMMUTABLE",
                "summary": "The app owner's access cannot be changed.",
            },
        )
    if target == account.username:
        # An explicit grant overrides the app-wide level, so an admin who set
        # their own grant to `reviewer` here would lock themselves out of
        # managing this project and be unable to undo it.
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "error_code": "CANNOT_CHANGE_OWN_LEVEL",
                "summary": "You cannot change your own level on a project.",
                "details": "Ask another admin, or the app owner.",
            },
        )

    member = projects_service.set_member_level(db, project.id, target, level_name)
    if member is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={
                "error_code": "MEMBER_NOT_FOUND",
                "summary": "That person is not on this project.",
            },
        )
    person = accounts_service.get(db, target)
    levels.report_access(target)
    return _member_out(member, person)


@router.delete("/{project_id}/members/{username}", status_code=status.HTTP_204_NO_CONTENT)
def remove_member(
    project_id: int,
    username: str,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> None:
    project = project_with_any_permission(
        db, project_id, account, [levels.PROJECT_MANAGE, levels.PROJECT_MEMBERS]
    )
    target = username.strip().lower()
    if levels.is_owner(target):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "error_code": "OWNER_IMMUTABLE",
                "summary": "The app owner cannot be removed from a project.",
            },
        )
    if not projects_service.remove_member(db, project.id, target):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={
                "error_code": "MEMBER_NOT_FOUND",
                "summary": "That person is not on this project.",
            },
        )
    levels.report_access(target)


@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_project(
    project_id: int,
    force: bool = False,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> None:
    """Delete a project, per-project gated on `scout.projects.delete` (D31).

    Refused while reviews exist unless `?force=true` — collected client
    feedback should not vanish on a misclick; the UI asks twice.
    """
    from sqlalchemy import func, select

    from app.models.option import Review

    project = project_with_permission(db, project_id, account, levels.PROJECTS_DELETE)
    review_count = db.scalar(
        select(func.count()).select_from(Review).where(Review.project_id == project.id)
    )
    if review_count and not force:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "error_code": "PROJECT_HAS_REVIEWS",
                "summary": f"This project has {review_count} review answer(s).",
                "details": "Deleting discards collected client feedback. "
                "Repeat with force=true to confirm.",
            },
        )
    former_members = projects_service.delete_project(db, project)
    for username in former_members:
        levels.report_access(username)
    levels.report_project_catalog()
