"""User management API routes (admin only)."""

import asyncio
import logging

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

import bcrypt

from ..auth.dependencies import require_admin
from ..database.engine import get_session
from ..database.models import User


async def _hash_password(password: str) -> str:
    """Run bcrypt off the event loop. Matches the helper in auth/routes.py."""

    def _work() -> str:
        return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()

    return await asyncio.to_thread(_work)

_LOGGER = logging.getLogger(__name__)

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


class UserCreate(BaseModel):
    username: str
    password: str
    role: str = "user"


class UserUpdate(BaseModel):
    username: str | None = None
    password: str | None = None
    role: str | None = None


@router.get("")
async def list_users(
    admin: User = Depends(require_admin),
    session: AsyncSession = Depends(get_session),
):
    """List all users (admin only).

    Excludes the internal ``__system__`` placeholder (id=0) used as the
    owner of default templates/configs — it is never a real account.
    """
    result = await session.exec(select(User).where(User.id != 0))
    return [
        {"id": u.id, "username": u.username, "role": u.role, "created_at": u.created_at.isoformat()}
        for u in result.all()
    ]


@router.post("", status_code=status.HTTP_201_CREATED)
async def create_user(
    body: UserCreate,
    admin: User = Depends(require_admin),
    session: AsyncSession = Depends(get_session),
):
    """Create a new user (admin only).

    Username is normalised to ``strip().lower()`` so "Admin" and "admin"
    cannot coexist. We do not add a CHECK constraint at the DB level — that
    would require rebuilding the table on SQLite — so the application is
    the single source of truth for normalisation.
    """
    # Normalise so case-only variants collide with existing accounts.
    username = (body.username or "").strip().lower()
    if not username:
        raise HTTPException(status_code=400, detail="Username cannot be empty")
    # Check for duplicate username
    result = await session.exec(select(User).where(User.username == username))
    if result.first():
        raise HTTPException(status_code=409, detail="Username already exists")

    if len(body.password) < 8:
        raise HTTPException(status_code=400, detail="Password must be at least 8 characters")

    user = User(
        username=username,
        hashed_password=await _hash_password(body.password),
        role=body.role if body.role in ("admin", "user") else "user",
    )
    session.add(user)
    await session.commit()
    await session.refresh(user)

    # Auto-create the bridge_self provider so the new user immediately gets
    # internal-failure notifications without manual setup. Best-effort —
    # a seeding hiccup must not fail the user creation itself.
    try:
        from ..database.seeds import ensure_bridge_self_provider_for_user
        await ensure_bridge_self_provider_for_user(session, user.id)
        await session.commit()
    except Exception:  # noqa: BLE001
        _LOGGER.exception("Failed to auto-seed bridge_self provider for user %s", user.id)
        await session.rollback()

    return {"id": user.id, "username": user.username, "role": user.role}


@router.patch("/{user_id}")
async def update_user(
    user_id: int,
    body: UserUpdate,
    admin: User = Depends(require_admin),
    session: AsyncSession = Depends(get_session),
):
    """Update username and/or role for a user (admin only)."""
    user = await session.get(User, user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")

    # Track whether the identity that JWTs encode has changed. Any such change
    # must bump ``token_version`` so already-issued tokens are rejected — a
    # user demoted admin→user must not keep admin in their cached JWT until
    # expiry, and a rename should invalidate prior sessions too.
    identity_changed = False

    if body.username is not None and body.username != user.username:
        # Normalise to match the case-insensitive uniqueness rule applied
        # at user creation. Comparing the normalised form against the
        # stored username also avoids false-positive "no change" when a
        # legacy mixed-case account is being renamed to its lower form.
        new_username = (body.username or "").strip().lower()
        if not new_username:
            raise HTTPException(status_code=400, detail="Username cannot be empty")
        if new_username != user.username:
            dup = await session.exec(select(User).where(User.username == new_username))
            if dup.first():
                raise HTTPException(status_code=409, detail="Username already exists")
            user.username = new_username
            identity_changed = True

    if body.role is not None and body.role != user.role:
        if body.role not in ("admin", "user"):
            raise HTTPException(status_code=400, detail="Invalid role")
        # Prevent demoting the last admin. Done via a COUNT to avoid loading
        # every admin row; more importantly, re-checked *after* the role
        # change is staged (TOCTOU guard — two concurrent demotes can each
        # see admin_count=2 and both proceed, dropping to 0).
        if user.role == "admin" and body.role != "admin":
            admin_count = (await session.exec(
                select(func.count(User.id)).where(User.role == "admin")
            )).one()
            if isinstance(admin_count, tuple):
                admin_count = admin_count[0]
            if (admin_count or 0) <= 1:
                raise HTTPException(status_code=400, detail="Cannot demote the last admin")
        user.role = body.role
        identity_changed = True

    if identity_changed:
        user.token_version = (user.token_version or 1) + 1

    session.add(user)
    try:
        await session.commit()
    except Exception:
        await session.rollback()
        raise

    # Final defense against admin-count race: if we just demoted the last admin
    # due to a concurrent demote landing between our check and commit, undo.
    if body.role is not None and body.role != "admin":
        admin_count_after = (await session.exec(
            select(func.count(User.id)).where(User.role == "admin")
        )).one()
        if isinstance(admin_count_after, tuple):
            admin_count_after = admin_count_after[0]
        if (admin_count_after or 0) < 1:
            # Roll the user back to admin and re-commit.
            user.role = "admin"
            session.add(user)
            await session.commit()
            raise HTTPException(status_code=409, detail="Refused: would remove the last admin")

    await session.refresh(user)
    return {"id": user.id, "username": user.username, "role": user.role}


class ResetPasswordRequest(BaseModel):
    new_password: str


@router.put("/{user_id}/password")
async def reset_user_password(
    user_id: int,
    body: ResetPasswordRequest,
    admin: User = Depends(require_admin),
    session: AsyncSession = Depends(get_session),
):
    """Reset a user's password (admin only)."""
    user = await session.get(User, user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    if len(body.new_password) < 8:
        raise HTTPException(status_code=400, detail="Password must be at least 8 characters")
    user.hashed_password = await _hash_password(body.new_password)
    # Invalidate all prior JWTs issued for this user — matches the self-serve
    # password-change path in auth/routes.py.
    user.token_version = (user.token_version or 1) + 1
    session.add(user)
    await session.commit()
    return {"success": True}


@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
    user_id: int,
    admin: User = Depends(require_admin),
    session: AsyncSession = Depends(get_session),
):
    """Delete a user (admin only, cannot delete self).

    Cascades through every user-owned table by hand. The model declares
    ``ondelete=CASCADE`` on each FK, but SQLite only enforces FK actions
    on tables created *after* the ondelete clause was added — existing
    installs upgraded from older schemas need this Python-side cascade
    instead of a multi-step table rebuild.

    TODO: drop this manual cascade once we ship a real
    rebuild-with-FK-actions migration for legacy SQLite installs (or
    once Postgres becomes the default deployment target).
    """
    from sqlalchemy import delete as sa_delete, update as sa_update

    if user_id == admin.id:
        raise HTTPException(status_code=400, detail="Cannot delete yourself")
    user = await session.get(User, user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")

    # Lazy import to avoid circulars.
    from ..database.models import (
        Action,
        ActionExecution,
        ActionRule,
        CommandConfig,
        CommandTracker,
        CommandTrackerListener,
        DeferredDispatch,
        EventLog,
        NotificationTarget,
        NotificationTracker,
        NotificationTrackerState,
        NotificationTrackerTarget,
        ServiceProvider,
        TelegramBot,
        TelegramChat,
        TrackingConfig,
        EmailBot,
        MatrixBot,
    )

    # Wrap the entire cascade in one transaction so a failure mid-way
    # cannot leave dangling child rows pointing at a missing user.
    try:
        # Order: leaves first, then their parents, finally the user. This
        # matters even with FKs disabled — it's the natural dependency
        # graph and avoids accidental constraint trips on engines that do
        # enforce FKs (Postgres).

        # Resolve tracker ids first (needed for state + link cleanup
        # before the parent rows themselves are deleted further down).
        from sqlmodel import select as _select
        tracker_ids = list((await session.exec(
            _select(NotificationTracker.id).where(NotificationTracker.user_id == user_id)
        )).all())
        if tracker_ids:
            await session.execute(
                sa_delete(NotificationTrackerState).where(
                    NotificationTrackerState.tracker_id.in_(tracker_ids)
                )
            )
            await session.execute(
                sa_delete(NotificationTrackerTarget).where(
                    NotificationTrackerTarget.tracker_id.in_(tracker_ids)
                )
            )
            await session.execute(
                sa_delete(DeferredDispatch).where(
                    DeferredDispatch.tracker_id.in_(tracker_ids)
                )
            )

        # Action children: rules and execution log.
        action_ids = list((await session.exec(
            _select(Action.id).where(Action.user_id == user_id)
        )).all())
        if action_ids:
            await session.execute(
                sa_delete(ActionRule).where(ActionRule.action_id.in_(action_ids))
            )
            await session.execute(
                sa_delete(ActionExecution).where(
                    ActionExecution.action_id.in_(action_ids)
                )
            )

        # Command tracker children: listeners.
        cmd_tracker_ids = list((await session.exec(
            _select(CommandTracker.id).where(CommandTracker.user_id == user_id)
        )).all())
        if cmd_tracker_ids:
            await session.execute(
                sa_delete(CommandTrackerListener).where(
                    CommandTrackerListener.command_tracker_id.in_(cmd_tracker_ids)
                )
            )

        # Telegram bot children: chats.
        bot_ids = list((await session.exec(
            _select(TelegramBot.id).where(TelegramBot.user_id == user_id)
        )).all())
        if bot_ids:
            await session.execute(
                sa_delete(TelegramChat).where(TelegramChat.bot_id.in_(bot_ids))
            )

        # Owned top-level entities (user is a direct owner).
        for model in (
            NotificationTracker,
            NotificationTarget,
            CommandTracker,
            CommandConfig,
            TrackingConfig,
            Action,
            TelegramBot,
            EmailBot,
            MatrixBot,
            ServiceProvider,
        ):
            await session.execute(
                sa_delete(model).where(model.user_id == user_id)
            )

        # EventLog: keep the audit trail but null the owner reference so
        # the rows survive the user delete (matches the SET NULL semantic
        # declared on the model).
        await session.execute(
            sa_update(EventLog).where(EventLog.user_id == user_id).values(user_id=None)
        )

        await session.delete(user)
        await session.commit()
    except Exception:
        await session.rollback()
        raise
