import uuid

from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.database import get_db
from app.core.permissions import deny_vendor, require_permission
from app.core.security import AuthContext, get_auth_context
from app.crud import account_emails, licensing, mfa, password_policy, sessions, sign_in_policy
from app.crud.audit import write_audit_log
from app.crud.base import CRUDBase
from app.crud.user_picker import UserPickerType, list_picker_users
from app.crud.user_verification import (
    VerificationError,
    public_base_url,
    require_connector,
    send_email_verification,
    verification_channels,
)
from app.models.identity import Role, Tenant, User, UserRole
from app.models.master import UserScopeAssignment
from app.schemas.identity import (
    RoleRead,
    SetPasswordInput,
    UserCreate,
    UserCreateRead,
    UserPickerRead,
    UserRead,
    UserRoleRead,
    UserUpdate,
)
from app.schemas.master import ScopeAssignmentInput, ScopeAssignmentRead

router = APIRouter(prefix="/users", tags=["users"])
crud = CRUDBase[User, UserCreate, UserUpdate](User)


def _admin_role_ids_subquery(tenant_id: uuid.UUID):
    return select(Role.id).where(Role.tenant_id == tenant_id, Role.is_system_role.is_(True))


async def _is_last_active_administrator(db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID) -> bool:
    """FR-USR-005: the platform's baseline Administrator role is flagged
    Role.is_system_role — see routers/roles.py. A user "holds" it via
    user_roles."""
    admin_roles = _admin_role_ids_subquery(tenant_id)
    holds_admin = (
        await db.execute(
            select(UserRole.role_id).where(UserRole.user_id == user_id, UserRole.role_id.in_(admin_roles))
        )
    ).first()
    if holds_admin is None:
        return False

    admin_roles = _admin_role_ids_subquery(tenant_id)
    remaining = (
        await db.execute(
            select(func.count(func.distinct(User.id)))
            .join(UserRole, UserRole.user_id == User.id)
            .where(
                User.tenant_id == tenant_id,
                User.status == "active",
                User.id != user_id,
                UserRole.role_id.in_(admin_roles),
            )
        )
    ).scalar_one()
    return remaining == 0


@router.get("", response_model=list[UserRead])
async def list_users(
    skip: int = 0,
    limit: int = 100,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "view")),
) -> list[User]:
    return await crud.list(db, ctx.tenant_id, skip=skip, limit=limit)


@router.get("/picker", response_model=list[UserPickerRead])
async def list_user_picker_options(
    user_type: UserPickerType,
    role_id: uuid.UUID | None = None,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
) -> list[User]:
    """Backs the "user" field type (Master datasource fields and Form
    fields that link to a system account) — deliberately open to any
    authenticated tenant member rather than gated by users_roles:view, since
    actual access to *use* one of these fields is already governed by
    whatever permission guards the Form/Master it lives on. Returns only
    id+name (UserPickerRead), never email/status/vendor_id."""
    return await list_picker_users(db, ctx.tenant_id, user_type, role_id=role_id, caller_vendor_id=ctx.vendor_id)


@router.get("/{user_id}", response_model=UserRead)
async def get_user(
    user_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "view")),
) -> User:
    return await crud.get(db, ctx.tenant_id, user_id)


@router.post("", response_model=UserCreateRead, status_code=status.HTTP_201_CREATED)
async def create_user(
    payload: UserCreate,
    request: Request,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "create")),
    __: AuthContext = Depends(deny_vendor()),
) -> UserCreateRead:
    # Checked before anything is written, so a missing connector doesn't
    # leave behind an account nobody can ever verify.
    try:
        for channel in verification_channels(payload.verification_method):
            await require_connector(db, ctx.tenant_id, channel)
    except VerificationError as exc:
        raise HTTPException(status_code=exc.status_code, detail=str(exc)) from None
    if payload.status == "active":
        await licensing.ensure_seat(db, ctx.tenant_id, portal=payload.vendor_id is not None)

    # password isn't a User column (password_hash is) — set aside so the
    # generic CRUDBase.create() (which does Model(**data)) never sees it.
    data = payload.model_dump(exclude={"password"}, exclude_unset=True, mode="json")
    data["tenant_id"] = ctx.tenant_id
    user = User(**data)
    password_policy.set_password(await db.get(Tenant, ctx.tenant_id), user, payload.password, by_admin=True)
    db.add(user)
    await db.flush()

    delivery = None
    if "email" in verification_channels(payload.verification_method):
        tenant = await db.get(Tenant, ctx.tenant_id)
        base_url = public_base_url(request.headers.get("origin"), request.headers.get("referer"))
        delivery = await send_email_verification(db, user, tenant, base_url)
    # SMS sends nothing now — the code goes out at first sign-in.

    await db.refresh(user)
    return UserCreateRead(**UserRead.model_validate(user).model_dump(), verification_delivery=delivery)


@router.post("/{user_id}/resend-verification", response_model=UserCreateRead)
async def resend_user_verification(
    user_id: uuid.UUID,
    request: Request,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> UserCreateRead:
    """Re-sends a pending email verification (new link + code; the old ones
    stop working). SMS verification has nothing to resend from here — its
    code is sent, and can be re-sent, from the sign-in screen itself."""
    user = await crud.get(db, ctx.tenant_id, user_id)
    if not user.verification_pending:
        raise HTTPException(status_code=409, detail="This user has no pending verification.")
    if not user.needs_email_verification:
        raise HTTPException(status_code=409, detail="SMS verification happens at the user's first sign-in — there's nothing to send now.")
    tenant = await db.get(Tenant, ctx.tenant_id)
    base_url = public_base_url(request.headers.get("origin"), request.headers.get("referer"))
    try:
        delivery = await send_email_verification(db, user, tenant, base_url, enforce_cooldown=True)
    except VerificationError as exc:
        raise HTTPException(status_code=exc.status_code, detail=str(exc)) from None
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "user.verification_resent", "user", user_id)
    await db.refresh(user)
    return UserCreateRead(**UserRead.model_validate(user).model_dump(), verification_delivery=delivery)


@router.post("/{user_id}/reset-mfa", response_model=UserRead)
async def reset_user_mfa(
    user_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> User:
    """Turns off someone's two-factor sign-in — for a lost phone with no
    recovery codes left. If the workspace requires it, they set it up again
    at their next sign-in."""
    user = await crud.get(db, ctx.tenant_id, user_id)
    if not mfa.enabled(user):
        raise HTTPException(status_code=409, detail="This user doesn't have two-factor sign-in turned on.")
    mfa.disable(user)
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "user.mfa.reset", "user", user_id)
    await db.flush()
    await db.refresh(user)
    return user


@router.post("/{user_id}/unlock", response_model=UserRead)
async def unlock_user(
    user_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> User:
    """Lifts a lockout after too many failed sign-in attempts."""
    user = await crud.get(db, ctx.tenant_id, user_id)
    sign_in_policy.record_success(user)
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "user.unlock", "user", user_id)
    await db.flush()
    await db.refresh(user)
    return user


@router.post("/{user_id}/set-password", status_code=status.HTTP_204_NO_CONTENT)
async def set_user_password(
    user_id: uuid.UUID,
    payload: SetPasswordInput,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> None:
    user = await crud.get(db, ctx.tenant_id, user_id)
    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)
    await account_emails.queue(db, await db.get(Tenant, ctx.tenant_id), "password_changed", user)


@router.patch("/{user_id}", response_model=UserRead)
async def update_user(
    user_id: uuid.UUID,
    payload: UserUpdate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> User:
    if payload.status == "inactive" and await _is_last_active_administrator(db, ctx.tenant_id, user_id):
        raise HTTPException(
            status_code=409,
            detail="Cannot deactivate the last remaining Administrator-role account (FR-USR-005).",
        )
    if payload.status == "active":
        current = await crud.get(db, ctx.tenant_id, user_id)
        if current.status != "active":
            await licensing.ensure_seat(db, ctx.tenant_id, portal=current.vendor_id is not None)
    updated = await crud.update(db, ctx.tenant_id, user_id, payload)
    if payload.status == "inactive":
        await sessions.sign_out_everywhere(db, updated)
    if payload.status is not None:
        await write_audit_log(
            db, ctx.tenant_id, ctx.user_id, "user.status_changed", "user", user_id, {"status": payload.status}
        )
    return updated


@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
    user_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "delete")),
    __: AuthContext = Depends(deny_vendor()),
) -> None:
    if await _is_last_active_administrator(db, ctx.tenant_id, user_id):
        raise HTTPException(
            status_code=409,
            detail="Cannot remove the last remaining Administrator-role account (FR-USR-005).",
        )
    await crud.delete(db, ctx.tenant_id, user_id)
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "user.deleted", "user", user_id)


@router.get("/{user_id}/roles", response_model=list[RoleRead])
async def list_user_roles(
    user_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
) -> list[Role]:
    # Viewing your OWN roles is never a privilege escalation, so it's exempt
    # from the users_roles grant — checking someone else's still needs it.
    # Without this, a Public Portal (vendor) account — which has no
    # "users_roles" entry in any portal role's action_matrix by design (that
    # matrix holds record ids for a portal role, not action verbs) — could
    # never resolve its own portal grants (usePortalAccess), leaving every
    # vendor-audience menu item permanently hidden regardless of what its
    # role actually grants.
    if ctx.user_id != user_id:
        await require_permission("users_roles", "view")(ctx=ctx, db=db)
    await crud.get(db, ctx.tenant_id, user_id)  # 404s if user isn't in this tenant
    stmt = select(Role).join(UserRole, UserRole.role_id == Role.id).where(UserRole.user_id == user_id)
    return list((await db.execute(stmt)).scalars().all())


@router.put("/{user_id}/roles/{role_id}", response_model=UserRoleRead, status_code=status.HTTP_201_CREATED)
async def assign_user_role(
    user_id: uuid.UUID,
    role_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> UserRole:
    user = await crud.get(db, ctx.tenant_id, user_id)
    role = (
        await db.execute(select(Role).where(Role.id == role_id, Role.tenant_id == ctx.tenant_id))
    ).scalar_one_or_none()
    if role is None:
        raise HTTPException(status_code=404, detail="Role not found")
    is_portal_role = role.scope == "portal"
    if bool(user.vendor_id) != is_portal_role:
        raise HTTPException(
            status_code=422,
            detail="Portal accounts can only hold portal-scope roles, and internal accounts can only hold internal-scope roles.",
        )

    existing = (
        await db.execute(select(UserRole).where(UserRole.user_id == user_id, UserRole.role_id == role_id))
    ).scalar_one_or_none()
    if existing is not None:
        return existing

    link = UserRole(user_id=user_id, role_id=role_id)
    db.add(link)
    await db.flush()
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "user.role_assigned", "user", user_id, {"role_id": str(role_id)})
    return link


@router.delete("/{user_id}/roles/{role_id}", status_code=status.HTTP_204_NO_CONTENT)
async def unassign_user_role(
    user_id: uuid.UUID,
    role_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> None:
    role = (
        await db.execute(select(Role).where(Role.id == role_id, Role.tenant_id == ctx.tenant_id))
    ).scalar_one_or_none()
    if role is not None and role.is_system_role:
        if await _is_last_active_administrator(db, ctx.tenant_id, user_id):
            raise HTTPException(
                status_code=409,
                detail="Cannot remove the Administrator role from the last remaining Administrator account (FR-USR-005).",
            )

    link = (
        await db.execute(select(UserRole).where(UserRole.user_id == user_id, UserRole.role_id == role_id))
    ).scalar_one_or_none()
    if link is None:
        raise HTTPException(status_code=404, detail="User does not have this role")
    await db.delete(link)
    await db.flush()
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "user.role_unassigned", "user", user_id, {"role_id": str(role_id)})


@router.get("/{user_id}/scope-assignments", response_model=list[ScopeAssignmentRead])
async def list_user_scope_assignments(
    user_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "view")),
) -> list[UserScopeAssignment]:
    await crud.get(db, ctx.tenant_id, user_id)  # 404s if user isn't in this tenant
    stmt = select(UserScopeAssignment).where(UserScopeAssignment.tenant_id == ctx.tenant_id, UserScopeAssignment.user_id == user_id)
    return list((await db.execute(stmt)).scalars().all())


@router.put("/{user_id}/scope-assignments", response_model=list[ScopeAssignmentRead])
async def set_user_scope_assignments(
    user_id: uuid.UUID,
    assignments: list[ScopeAssignmentInput],
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> list[UserScopeAssignment]:
    """Replaces this user's full scope-assignment set (see app/crud/scope.py
    for what these mean) — same replace-all shape as PUT /dashboards/{id}/roles."""
    await crud.get(db, ctx.tenant_id, user_id)

    existing = (
        await db.execute(
            select(UserScopeAssignment).where(UserScopeAssignment.tenant_id == ctx.tenant_id, UserScopeAssignment.user_id == user_id)
        )
    ).scalars().all()
    for link in existing:
        await db.delete(link)
    await db.flush()

    new_links = [
        UserScopeAssignment(tenant_id=ctx.tenant_id, user_id=user_id, master_id=a.master_id, record_id=a.record_id)
        for a in assignments
    ]
    db.add_all(new_links)
    await db.flush()
    await write_audit_log(
        db, ctx.tenant_id, ctx.user_id, "user.scope_assignments_changed", "user", user_id,
        {"count": len(new_links)},
    )
    return new_links
