"""User management inside the Public Portal — for holders of a portal role
with the "user_management" feature (Portal Roles › User management).

A manager works only with the portal users of their own organization
(vendor), may give only the roles their role lets them assign
("user_management_roles"), and can't change themselves or anyone holding a
role outside that set — so nobody can grant themselves (or take from
someone above them) more than the administrator allowed."""

import uuid

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, EmailStr, Field
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.database import get_db
from app.core.permissions import resolve_portal_grants
from app.core.security import AuthContext, get_auth_context
from app.crud import account_emails, licensing, password_policy, sessions
from app.crud.audit import write_audit_log
from app.models.identity import Role, Tenant, User, UserRole
from app.schemas.identity import AccountStatus

router = APIRouter(prefix="/portal/users", tags=["portal-users"])


class PortalUserRead(BaseModel):
    id: uuid.UUID
    name: str
    email: str
    phone: str | None
    status: str
    role_ids: list[uuid.UUID]
    last_login_at: object | None = None
    # False for the manager themselves and for anyone holding a role they can't assign.
    manageable: bool


class PortalUserCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    email: EmailStr
    password: str = Field(min_length=1, max_length=256)  # the workspace's password policy decides
    phone: str | None = Field(default=None, max_length=30)
    role_ids: list[uuid.UUID] = []


class PortalUserUpdate(BaseModel):
    name: str | None = Field(default=None, min_length=1, max_length=200)
    phone: str | None = Field(default=None, max_length=30)
    status: AccountStatus | None = None
    role_ids: list[uuid.UUID] | None = None


class PortalPasswordSet(BaseModel):
    password: str = Field(min_length=1, max_length=256)  # the workspace's password policy decides


class PortalRoleOption(BaseModel):
    id: uuid.UUID
    name: str


async def _assignable(db: AsyncSession, ctx: AuthContext) -> set[str]:
    """The role ids this manager may give; 403 for anyone else."""
    if ctx.vendor_id is None or ctx.user_id is None:
        raise HTTPException(status_code=403, detail="Portal user management is for portal accounts — Studio users manage users under Users.")
    grants = await resolve_portal_grants(db, ctx.tenant_id, ctx.user_id)
    if not grants.get("user_management"):
        raise HTTPException(status_code=403, detail="Your portal role doesn't include user management.")
    wanted = {i for i in grants.get("user_management_roles", set())}
    if not wanted:
        return set()
    ids = []
    for i in wanted:
        try:
            ids.append(uuid.UUID(i))
        except ValueError:
            continue
    rows = await db.execute(select(Role.id).where(Role.tenant_id == ctx.tenant_id, Role.scope == "portal", Role.id.in_(ids)))
    return {str(r).lower() for (r,) in rows.all()}


async def _roles_of(db: AsyncSession, user_ids: list[uuid.UUID]) -> dict[uuid.UUID, list[uuid.UUID]]:
    out: dict[uuid.UUID, list[uuid.UUID]] = {u: [] for u in user_ids}
    if user_ids:
        for uid, rid in (await db.execute(select(UserRole.user_id, UserRole.role_id).where(UserRole.user_id.in_(user_ids)))).all():
            out[uid].append(rid)
    return out


def _read(user: User, role_ids: list[uuid.UUID], ctx: AuthContext, assignable: set[str]) -> PortalUserRead:
    return PortalUserRead(
        id=user.id,
        name=user.name,
        email=user.email,
        phone=user.phone,
        status=user.status,
        role_ids=role_ids,
        last_login_at=user.last_login_at,
        manageable=user.id != ctx.user_id and all(str(r).lower() in assignable for r in role_ids),
    )


def _check_roles(requested: list[uuid.UUID], assignable: set[str]) -> None:
    outside = [str(r) for r in requested if str(r).lower() not in assignable]
    if outside:
        raise HTTPException(status_code=403, detail="You can't give one or more of those roles.")


async def _managed(db: AsyncSession, ctx: AuthContext, user_id: uuid.UUID, assignable: set[str]) -> tuple[User, list[uuid.UUID]]:
    user = await db.get(User, user_id)
    # Someone outside the manager's organization doesn't exist, as far as they can tell.
    if user is None or user.tenant_id != ctx.tenant_id or user.vendor_id != ctx.vendor_id:
        raise HTTPException(status_code=404, detail="User not found")
    if user.id == ctx.user_id:
        raise HTTPException(status_code=403, detail="You can't change your own account here.")
    roles = (await _roles_of(db, [user.id]))[user.id]
    if not all(str(r).lower() in assignable for r in roles):
        raise HTTPException(status_code=403, detail="This user holds a role you can't manage.")
    return user, roles


@router.get("/roles", response_model=list[PortalRoleOption])
async def assignable_roles(ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)) -> list[PortalRoleOption]:
    assignable = await _assignable(db, ctx)
    if not assignable:
        return []
    rows = await db.execute(select(Role).where(Role.tenant_id == ctx.tenant_id, Role.id.in_([uuid.UUID(i) for i in assignable])).order_by(Role.name))
    return [PortalRoleOption(id=r.id, name=r.name) for r in rows.scalars().all()]


@router.get("", response_model=list[PortalUserRead])
async def list_portal_users(ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)) -> list[PortalUserRead]:
    assignable = await _assignable(db, ctx)
    users = list(
        (await db.execute(select(User).where(User.tenant_id == ctx.tenant_id, User.vendor_id == ctx.vendor_id).order_by(func.lower(User.name)))).scalars().all()
    )
    roles = await _roles_of(db, [u.id for u in users])
    return [_read(u, roles[u.id], ctx, assignable) for u in users]


@router.post("", response_model=PortalUserRead, status_code=status.HTTP_201_CREATED)
async def create_portal_user(payload: PortalUserCreate, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)) -> PortalUserRead:
    assignable = await _assignable(db, ctx)
    _check_roles(payload.role_ids, assignable)
    taken = (await db.execute(select(User.id).where(User.tenant_id == ctx.tenant_id, func.lower(User.email) == payload.email.lower()))).first()
    if taken:
        raise HTTPException(status_code=409, detail="An account with this email already exists.")
    await licensing.ensure_seat(db, ctx.tenant_id, portal=True)
    user = User(
        tenant_id=ctx.tenant_id,
        vendor_id=ctx.vendor_id,
        name=payload.name.strip(),
        email=payload.email,
        phone=(payload.phone or "").strip() or None,
        status="active",
    )
    password_policy.set_password(await db.get(Tenant, ctx.tenant_id), user, payload.password, by_admin=True)
    db.add(user)
    await db.flush()
    for rid in dict.fromkeys(payload.role_ids):
        db.add(UserRole(user_id=user.id, role_id=rid))
    await db.flush()
    await db.refresh(user)
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "user.create", "user", user.id, {"via": "portal", "role_ids": [str(r) for r in payload.role_ids]})
    return _read(user, list(dict.fromkeys(payload.role_ids)), ctx, assignable)


@router.patch("/{user_id}", response_model=PortalUserRead)
async def update_portal_user(
    user_id: uuid.UUID, payload: PortalUserUpdate, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)
) -> PortalUserRead:
    assignable = await _assignable(db, ctx)
    user, roles = await _managed(db, ctx, user_id, assignable)
    if payload.name is not None:
        user.name = payload.name.strip()
    if payload.phone is not None:
        user.phone = payload.phone.strip() or None
    if payload.status is not None and payload.status != user.status:
        if payload.status == "active":
            await licensing.ensure_seat(db, ctx.tenant_id, portal=True)
        user.status = payload.status
        if payload.status == "inactive":
            await sessions.sign_out_everywhere(db, user)
        await write_audit_log(db, ctx.tenant_id, ctx.user_id, "user.status_changed", "user", user.id, {"status": payload.status, "via": "portal"})
    if payload.role_ids is not None:
        _check_roles(payload.role_ids, assignable)
        wanted = list(dict.fromkeys(payload.role_ids))
        for rid in set(roles) - set(wanted):
            link = (await db.execute(select(UserRole).where(UserRole.user_id == user.id, UserRole.role_id == rid))).scalar_one()
            await db.delete(link)
        for rid in [r for r in wanted if r not in roles]:
            db.add(UserRole(user_id=user.id, role_id=rid))
        if set(roles) != set(wanted):
            await write_audit_log(db, ctx.tenant_id, ctx.user_id, "user.roles_changed", "user", user.id, {"role_ids": [str(r) for r in wanted], "via": "portal"})
        roles = wanted
    await db.flush()
    await db.refresh(user)
    return _read(user, roles, ctx, assignable)


@router.post("/{user_id}/set-password", status_code=status.HTTP_204_NO_CONTENT)
async def set_portal_user_password(
    user_id: uuid.UUID, payload: PortalPasswordSet, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)
) -> None:
    assignable = await _assignable(db, ctx)
    user, _ = await _managed(db, ctx, user_id, assignable)
    password_policy.set_password(await db.get(Tenant, ctx.tenant_id), user, payload.password, by_admin=True)
    account_emails.clear_reset(user)
    await sessions.sign_out_everywhere(db, user)
    await db.flush()
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "user.password_reset", "user", user.id, {"via": "portal"})
    await account_emails.queue(db, await db.get(Tenant, ctx.tenant_id), "password_changed", user)
