"""UserGroup management — Org admin+ or platform admin.

Groups are GROUP-kind ``Principal`` rows whose ``parent_principal_id``
points at their owning ORG-principal. Group memberships live in the
unified ``principal_memberships`` table with ``role=NULL`` (groups
don't have role tiers).
"""

from datetime import datetime, timezone
from typing import List, Optional

from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlmodel import select

from gpustack.api.exceptions import (
    AlreadyExistsException,
    ForbiddenException,
    InvalidException,
    NotFoundException,
)
from gpustack.schemas.principals import (
    OrgRole,
    Principal,
    PrincipalMembership,
    PrincipalType,
)
from gpustack.schemas.user_groups import (
    UserGroupCreate,
    UserGroupListParams,
    UserGroupMembershipPublic,
    UserGroupPublic,
    UserGroupUpdate,
    UserGroupsPublic,
)
from gpustack.schemas.users import User
from gpustack.server.deps import SessionDep, TenantContextDep

router = APIRouter()


class GroupMembershipCreate(BaseModel):
    user_id: int


def _can_manage_groups(ctx, org_id: int) -> bool:
    if ctx.is_platform_admin:
        return True
    if ctx.current_principal_id != org_id:
        return False
    return ctx.org_role == OrgRole.ADMIN


async def _load_org(session, org_id: int) -> Principal:
    org = await Principal.one_by_id(session, org_id)
    if not org or org.deleted_at is not None or org.kind != PrincipalType.ORG:
        raise NotFoundException(message="Organization not found")
    return org


async def _load_group(session, org_id: int, group_id: int) -> Principal:
    group = await Principal.one_by_id(session, group_id)
    if (
        not group
        or group.deleted_at is not None
        or group.kind != PrincipalType.GROUP
        or group.parent_principal_id != org_id
    ):
        raise NotFoundException(message="Group not found")
    return group


def _group_to_public(group: Principal) -> UserGroupPublic:
    return UserGroupPublic.from_principal(group)


# ---- groups ----------------------------------------------------------------


@router.get("/organizations/{org_id}/groups", response_model=UserGroupsPublic)
async def list_groups(
    session: SessionDep,
    ctx: TenantContextDep,
    org_id: int,
    params: UserGroupListParams = Depends(),
    search: Optional[str] = None,
):
    await _load_org(session, org_id)
    if not ctx.is_platform_admin and ctx.current_principal_id != org_id:
        raise ForbiddenException(message="Not a member of this organization")

    fuzzy_fields = {"name": search} if search else {}
    page = await Principal.paginated_by_query(
        session=session,
        fields={
            "kind": PrincipalType.GROUP,
            "parent_principal_id": org_id,
            "deleted_at": None,
        },
        fuzzy_fields=fuzzy_fields,
        page=params.page,
        per_page=params.perPage,
        order_by=params.order_by,
    )
    page.items = [_group_to_public(g) for g in page.items]
    return page


@router.post("/organizations/{org_id}/groups", response_model=UserGroupPublic)
async def create_group(
    session: SessionDep,
    ctx: TenantContextDep,
    org_id: int,
    body: UserGroupCreate,
):
    await _load_org(session, org_id)
    if not _can_manage_groups(ctx, org_id):
        raise ForbiddenException(message="Insufficient permission to manage groups")

    existing = await Principal.one_by_fields(
        session,
        {
            "kind": PrincipalType.GROUP,
            "parent_principal_id": org_id,
            "name": body.name,
            "deleted_at": None,
        },
    )
    if existing:
        raise AlreadyExistsException(
            message=f"Group '{body.name}' already exists in this organization"
        )

    try:
        group = Principal(
            kind=PrincipalType.GROUP,
            parent_principal_id=org_id,
            name=body.name,
            description=body.description,
        )
        created = await Principal.create(session, group)
    except Exception as e:
        raise InvalidException(message=f"Failed to create group: {e}")
    return _group_to_public(created)


@router.put("/organizations/{org_id}/groups/{group_id}", response_model=UserGroupPublic)
async def update_group(
    session: SessionDep,
    ctx: TenantContextDep,
    org_id: int,
    group_id: int,
    body: UserGroupUpdate,
):
    group = await _load_group(session, org_id, group_id)
    if not _can_manage_groups(ctx, org_id):
        raise ForbiddenException(message="Insufficient permission to manage groups")

    try:
        await group.update(session, body.model_dump(exclude_unset=True))
    except Exception as e:
        raise InvalidException(message=f"Failed to update group: {e}")
    return _group_to_public(group)


@router.delete("/organizations/{org_id}/groups/{group_id}")
async def delete_group(
    session: SessionDep, ctx: TenantContextDep, org_id: int, group_id: int
):
    group = await _load_group(session, org_id, group_id)
    if not _can_manage_groups(ctx, org_id):
        raise ForbiddenException(message="Insufficient permission to manage groups")

    try:
        await group.delete(session)
    except Exception as e:
        raise InvalidException(message=f"Failed to delete group: {e}")


# ---- group members ---------------------------------------------------------


async def _resolve_user(session, user_id: int) -> Optional[User]:
    user = await User.one_by_id(session, user_id)
    if not user or user.is_system or user.deleted_at is not None:
        return None
    return user


@router.get(
    "/organizations/{org_id}/groups/{group_id}/members",
    response_model=List[UserGroupMembershipPublic],
)
async def list_group_members(
    session: SessionDep,
    ctx: TenantContextDep,
    org_id: int,
    group_id: int,
):
    await _load_group(session, org_id, group_id)
    if not ctx.is_platform_admin and ctx.current_principal_id != org_id:
        raise ForbiddenException(message="Not a member of this organization")

    stmt = select(PrincipalMembership).where(
        PrincipalMembership.parent_principal_id == group_id,
        PrincipalMembership.deleted_at.is_(None),
    )
    rows = list((await session.exec(stmt)).all())
    member_ids = {r.member_principal_id for r in rows}
    user_by_principal: dict[int, User] = {}
    if member_ids:
        result = await session.exec(
            select(User).where(User.principal_id.in_(member_ids))
        )
        user_by_principal = {u.principal_id: u for u in result.all()}
    out: List[UserGroupMembershipPublic] = []
    for r in rows:
        u = user_by_principal.get(r.member_principal_id)
        out.append(
            UserGroupMembershipPublic(
                user_id=getattr(u, "id", 0),
                group_id=group_id,
                created_at=r.created_at,
                username=getattr(u, "username", None),
                full_name=getattr(u, "full_name", None),
            )
        )
    return out


@router.post(
    "/organizations/{org_id}/groups/{group_id}/members",
    response_model=UserGroupMembershipPublic,
)
async def add_group_member(
    session: SessionDep,
    ctx: TenantContextDep,
    org_id: int,
    group_id: int,
    body: GroupMembershipCreate,
):
    await _load_group(session, org_id, group_id)
    if not _can_manage_groups(ctx, org_id):
        raise ForbiddenException(message="Insufficient permission to manage groups")

    user = await _resolve_user(session, body.user_id)
    if not user:
        raise NotFoundException(message="User not found")

    # User must be an active member of the group's org first.
    org_membership_stmt = select(PrincipalMembership.id).where(
        PrincipalMembership.parent_principal_id == org_id,
        PrincipalMembership.member_principal_id == user.principal_id,
        PrincipalMembership.deleted_at.is_(None),
    )
    if (await session.exec(org_membership_stmt)).first() is None:
        raise InvalidException(
            message=(
                f"User {body.user_id} is not a member of " f"organization {org_id}"
            )
        )

    existing_stmt = select(PrincipalMembership).where(
        PrincipalMembership.parent_principal_id == group_id,
        PrincipalMembership.member_principal_id == user.principal_id,
        PrincipalMembership.deleted_at.is_(None),
    )
    if (await session.exec(existing_stmt)).first() is not None:
        raise AlreadyExistsException(
            message=f"User {body.user_id} is already in group {group_id}"
        )

    try:
        now = datetime.now(timezone.utc).replace(tzinfo=None)
        link = PrincipalMembership(
            parent_principal_id=group_id,
            member_principal_id=user.principal_id,
            role=None,
            created_at=now,
            updated_at=now,
        )
        session.add(link)
        await session.commit()
        await session.refresh(link)
    except Exception as e:
        await session.rollback()
        raise InvalidException(message=f"Failed to add group member: {e}")
    return UserGroupMembershipPublic(
        user_id=user.id,
        group_id=group_id,
        created_at=link.created_at,
        username=user.username,
        full_name=user.full_name,
    )


@router.delete("/organizations/{org_id}/groups/{group_id}/members/{user_id}")
async def remove_group_member(
    session: SessionDep,
    ctx: TenantContextDep,
    org_id: int,
    group_id: int,
    user_id: int,
):
    await _load_group(session, org_id, group_id)
    if not _can_manage_groups(ctx, org_id):
        raise ForbiddenException(message="Insufficient permission to manage groups")

    user = await _resolve_user(session, user_id)
    if not user:
        raise NotFoundException(message="Group membership not found")

    stmt = select(PrincipalMembership).where(
        PrincipalMembership.parent_principal_id == group_id,
        PrincipalMembership.member_principal_id == user.principal_id,
        PrincipalMembership.deleted_at.is_(None),
    )
    link = (await session.exec(stmt)).first()
    if not link:
        raise NotFoundException(message="Group membership not found")

    try:
        await link.delete(session, soft=True)
    except Exception as e:
        await session.rollback()
        raise InvalidException(message=f"Failed to remove group member: {e}")
