"""API endpoints for admin user management.

Provides CRUD operations for user profiles and aggregate statistics so that
administrators can inspect, configure, and manage users in multi-user mode.
Also provides endpoints for admins to create and manage local (email/password)
user accounts directly, without requiring email verification.
"""

import logging
from datetime import datetime, timezone
from typing import Annotated, Any

from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from pydantic import BaseModel, Field
from sqlalchemy import func
from sqlalchemy.orm import Session

from app.config import settings
from app.database import get_db
from app.models import FileRecord, LocalUser, UserProfile
from app.utils.local_auth import generate_token, hash_password, send_password_reset_email

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/users", tags=["admin-users"])

DbSession = Annotated[Session, Depends(get_db)]


# ---------------------------------------------------------------------------
# Auth helper
# ---------------------------------------------------------------------------


def _require_admin(request: Request) -> dict:
    """Ensure the caller is an admin. Raises 403 otherwise."""
    user = request.session.get("user")
    if not user or not user.get("is_admin"):
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
    return user


AdminUser = Annotated[dict, Depends(_require_admin)]


# ---------------------------------------------------------------------------
# Pydantic schemas
# ---------------------------------------------------------------------------


class UserProfileUpsert(BaseModel):
    """Body for creating or updating a user profile."""

    display_name: str | None = Field(default=None, max_length=255, description="Human-readable display name")
    daily_upload_limit: int | None = Field(
        default=None, ge=0, description="Per-user daily upload cap; null = use global default"
    )
    notes: str | None = Field(default=None, max_length=4096, description="Admin notes about this user")
    is_blocked: bool = Field(default=False, description="Block this user from uploading")
    subscription_tier: str | None = Field(
        default="free",
        description="Subscription tier: free | starter | professional | business",
    )
    subscription_billing_cycle: str = Field(default="monthly", pattern="^(monthly|yearly)$")
    subscription_period_start: datetime | None = None
    allow_overage: bool = False
    is_complimentary: bool = Field(
        default=False,
        description="When True the user is on a complimentary (uncharged) plan — they keep all tier "
        "quota benefits but are never billed via Stripe.",
    )


class PaymentIssueBody(BaseModel):
    """Body for reporting a payment issue for a user."""

    issue: str = Field(..., min_length=1, max_length=2048, description="Description of the payment issue")


class UserProfileResponse(BaseModel):
    """Response schema for a user profile record."""

    id: int
    user_id: str
    display_name: str | None
    daily_upload_limit: int | None
    notes: str | None
    is_blocked: bool
    subscription_tier: str | None
    subscription_billing_cycle: str
    subscription_period_start: str | None
    allow_overage: bool
    is_complimentary: bool
    created_at: str | None
    updated_at: str | None

    model_config = {"from_attributes": True}


class UserSummary(BaseModel):
    """Per-user summary combining profile data with document statistics."""

    user_id: str
    display_name: str | None
    daily_upload_limit: int | None
    notes: str | None
    is_blocked: bool
    subscription_tier: str | None
    subscription_billing_cycle: str | None
    subscription_period_start: str | None
    allow_overage: bool
    is_complimentary: bool
    profile_id: int | None
    document_count: int
    last_upload: str | None


class LocalUserCreate(BaseModel):
    """Body for admin-creating a local (email/password) user account."""

    email: str = Field(..., max_length=255, description="Email address for the new user")
    username: str = Field(..., min_length=3, max_length=64, pattern=r"^[a-zA-Z0-9_-]+$")
    display_name: str | None = Field(default=None, max_length=255)
    password: str = Field(..., min_length=8, max_length=128)
    is_admin: bool = Field(default=False, description="Grant admin privileges")


class LocalUserUpdate(BaseModel):
    """Body for admin-updating a local (email/password) user account."""

    email: str | None = Field(default=None, max_length=255, description="New email address")
    display_name: str | None = Field(default=None, max_length=255, description="New display name")
    is_admin: bool | None = Field(default=None, description="Grant or revoke admin privileges")
    is_active: bool | None = Field(default=None, description="Activate or deactivate the account")


class LocalUserSetPassword(BaseModel):
    """Body for admin setting a temporary password for a local user."""

    password: str = Field(..., min_length=8, max_length=128, description="New temporary password")


class LocalUserResponse(BaseModel):
    """Summary of a local user account."""

    id: int
    email: str
    username: str
    display_name: str | None
    is_active: bool
    is_admin: bool
    created_at: str | None

    model_config = {"from_attributes": True}


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _get_or_none(db: Session, user_id: str) -> UserProfile | None:
    """Return the UserProfile row for *user_id*, or None if it doesn't exist."""
    return db.query(UserProfile).filter(UserProfile.user_id == user_id).first()


def _profile_to_dict(profile: UserProfile) -> dict[str, Any]:
    return {
        "id": profile.id,
        "user_id": profile.user_id,
        "display_name": profile.display_name,
        "daily_upload_limit": profile.daily_upload_limit,
        "notes": profile.notes,
        "is_blocked": profile.is_blocked,
        "subscription_tier": profile.subscription_tier or "free",
        "subscription_billing_cycle": profile.subscription_billing_cycle or "monthly",
        "subscription_period_start": profile.subscription_period_start.isoformat()
        if profile.subscription_period_start
        else None,
        "allow_overage": bool(profile.allow_overage),
        "is_complimentary": bool(profile.is_complimentary),
        "created_at": profile.created_at.isoformat() if profile.created_at else None,
        "updated_at": profile.updated_at.isoformat() if profile.updated_at else None,
    }


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------


@router.get("/", summary="List all known users with statistics")
def list_users(
    db: DbSession,
    _admin: AdminUser,
    q: str = Query("", description="Filter by user_id substring (case-insensitive)"),
    page: int = Query(1, ge=1, description="Page number"),
    per_page: int = Query(25, ge=1, le=100, description="Items per page"),
) -> dict[str, Any]:
    """Return every distinct user_id that has at least one document or an explicit profile,
    enriched with aggregate document statistics and the admin-managed profile.

    Supports substring filtering (``q``) and pagination.
    """
    # 1. Collect every distinct owner_id from documents
    doc_stats_query = (
        db.query(
            FileRecord.owner_id.label("user_id"),
            func.count(FileRecord.id).label("doc_count"),
            func.max(FileRecord.created_at).label("last_upload"),
        )
        .filter(FileRecord.owner_id.isnot(None))
        .group_by(FileRecord.owner_id)
    )

    # 2. Collect all user_ids that have explicit profiles (may not have docs yet)
    profile_query = db.query(UserProfile)

    # Build a unified set of user_ids
    doc_rows = {row.user_id: row for row in doc_stats_query.all()}
    profile_rows = {p.user_id: p for p in profile_query.all()}

    all_user_ids = set(doc_rows.keys()) | set(profile_rows.keys())

    # Apply optional substring filter
    if q.strip():
        q_lower = q.strip().lower()
        all_user_ids = {uid for uid in all_user_ids if q_lower in uid.lower()}

    # Sort and paginate
    sorted_ids = sorted(all_user_ids)
    total = len(sorted_ids)
    start = (page - 1) * per_page
    page_ids = sorted_ids[start : start + per_page]

    users: list[dict[str, Any]] = []
    for uid in page_ids:
        doc_row = doc_rows.get(uid)
        profile = profile_rows.get(uid)
        users.append(
            {
                "user_id": uid,
                "display_name": profile.display_name if profile else None,
                "daily_upload_limit": profile.daily_upload_limit if profile else None,
                "notes": profile.notes if profile else None,
                "is_blocked": profile.is_blocked if profile else False,
                "subscription_tier": (profile.subscription_tier or "free") if profile else "free",
                "subscription_billing_cycle": (profile.subscription_billing_cycle or "monthly")
                if profile
                else "monthly",
                "subscription_period_start": profile.subscription_period_start.isoformat()
                if (profile and profile.subscription_period_start)
                else None,
                "allow_overage": bool(profile.allow_overage) if profile else False,
                "is_complimentary": bool(profile.is_complimentary) if profile else False,
                "profile_id": profile.id if profile else None,
                "document_count": doc_row.doc_count if doc_row else 0,
                "last_upload": doc_row.last_upload.isoformat() if (doc_row and doc_row.last_upload) else None,
            }
        )

    return {
        "users": users,
        "total": total,
        "page": page,
        "per_page": per_page,
        "pages": max(1, (total + per_page - 1) // per_page),
    }


# ---------------------------------------------------------------------------
# Local user management (admin-only)
# ---------------------------------------------------------------------------
# NOTE: These routes MUST be defined before /{user_id:path} to avoid being
# swallowed by the catch-all path parameter.
# ---------------------------------------------------------------------------


@router.get("/local", summary="List all local (email/password) user accounts")
def list_local_users(db: DbSession, _admin: AdminUser) -> list[dict[str, Any]]:
    """Return every local user account with basic metadata."""
    users = db.query(LocalUser).order_by(LocalUser.created_at.desc()).all()
    return [
        {
            "id": u.id,
            "email": u.email,
            "username": u.username,
            "display_name": u.display_name,
            "is_active": u.is_active,
            "is_admin": u.is_admin,
            "created_at": u.created_at.isoformat() if u.created_at else None,
        }
        for u in users
    ]


@router.post("/local", status_code=status.HTTP_201_CREATED, summary="Create a local user account")
def create_local_user(body: LocalUserCreate, db: DbSession, _admin: AdminUser) -> dict[str, Any]:
    """Create a new local (email/password) user account.

    The account is immediately active — no email verification is required when
    created by an administrator.  A matching UserProfile row is also created.

    Raises:
        409: Email or username already registered.
    """
    if db.query(LocalUser).filter(LocalUser.email == body.email).first():
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Email already registered.")
    if db.query(LocalUser).filter(LocalUser.username == body.username).first():
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Username already taken.")

    user = LocalUser(
        email=body.email,
        username=body.username,
        display_name=body.display_name,
        hashed_password=hash_password(body.password),
        is_active=True,
        is_admin=body.is_admin,
    )
    db.add(user)

    # Ensure a UserProfile exists for the new user
    if not db.query(UserProfile).filter(UserProfile.user_id == body.email).first():
        db.add(UserProfile(user_id=body.email, display_name=body.display_name or body.username))

    try:
        db.commit()
        db.refresh(user)
    except Exception:
        db.rollback()
        raise

    logger.info("Admin created local user account: %s", body.email)
    return {
        "id": user.id,
        "email": user.email,
        "username": user.username,
        "display_name": user.display_name,
        "is_active": user.is_active,
        "is_admin": user.is_admin,
        "created_at": user.created_at.isoformat() if user.created_at else None,
    }


@router.delete(
    "/local/{local_user_id}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Delete a local user account",
)
def delete_local_user(local_user_id: int, db: DbSession, _admin: AdminUser) -> None:
    """Delete a local user account by its numeric ID.

    The associated UserProfile is also removed.  Documents owned by this user
    are **not** deleted.
    """
    user = db.query(LocalUser).filter(LocalUser.id == local_user_id).first()
    if not user:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Local user not found.")

    # Remove associated profile if present
    profile = db.query(UserProfile).filter(UserProfile.user_id == user.email).first()
    if profile:
        db.delete(profile)

    try:
        db.delete(user)
        db.commit()
    except Exception:
        db.rollback()
        raise

    logger.info("Admin deleted local user account: %s", user.email)


@router.patch("/local/{local_user_id}", summary="Update a local user account")
def update_local_user(local_user_id: int, body: LocalUserUpdate, db: DbSession, _admin: AdminUser) -> dict[str, Any]:
    """Update the email address, display name, admin flag, or active status of a local user account.

    Only fields explicitly provided (non-None) are modified.  If the email is changed
    the associated UserProfile row is also updated to keep ``user_id`` in sync.

    Raises:
        404: Local user not found.
        409: The new email is already taken by another account.
    """
    user = db.query(LocalUser).filter(LocalUser.id == local_user_id).first()
    if not user:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Local user not found.")

    old_email = user.email

    if body.email is not None and body.email != user.email:
        if db.query(LocalUser).filter(LocalUser.email == body.email, LocalUser.id != local_user_id).first():
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Email already registered.")
        user.email = body.email

    if body.display_name is not None:
        # Normalise empty string to None so that clearing the field removes the display name
        user.display_name = body.display_name or None

    if body.is_admin is not None:
        user.is_admin = body.is_admin

    if body.is_active is not None:
        user.is_active = body.is_active

    try:
        db.flush()
        # Keep UserProfile.user_id in sync when email changes
        if body.email is not None and body.email != old_email:
            profile = db.query(UserProfile).filter(UserProfile.user_id == old_email).first()
            if profile:
                profile.user_id = body.email
        db.commit()
        db.refresh(user)
    except Exception:
        db.rollback()
        raise

    logger.info("Admin updated local user %s (id=%d)", user.email, user.id)
    return {
        "id": user.id,
        "email": user.email,
        "username": user.username,
        "display_name": user.display_name,
        "is_active": user.is_active,
        "is_admin": user.is_admin,
        "created_at": user.created_at.isoformat() if user.created_at else None,
    }


@router.post(
    "/local/{local_user_id}/send-password-reset",
    status_code=status.HTTP_200_OK,
    summary="Send a password reset email to a local user",
)
def admin_send_password_reset(local_user_id: int, request: Request, db: DbSession, _admin: AdminUser) -> dict[str, Any]:
    """Generate a password reset token and email the reset link to the local user.

    This is a last-resort tool for admins to help users who are locked out.
    Returns ``{"sent": true}`` on success and ``{"sent": false, "reason": "..."}`` when
    SMTP is not configured or sending fails.

    Raises:
        404: Local user not found.
    """
    user = db.query(LocalUser).filter(LocalUser.id == local_user_id).first()
    if not user:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Local user not found.")

    if not settings.email_host:
        logger.warning("Admin requested password reset for %s but SMTP is not configured", user.email)
        return {"sent": False, "reason": "SMTP is not configured on this server."}

    token = generate_token()
    user.password_reset_token = token
    user.password_reset_sent_at = datetime.now(tz=timezone.utc)
    db.commit()

    base_url = str(request.base_url).rstrip("/")
    try:
        send_password_reset_email(user.email, user.username, token, base_url)
    except Exception as exc:
        logger.warning("Admin-triggered password reset email failed for %s: %s", user.email, exc)
        return {"sent": False, "reason": str(exc)}

    logger.info("[SECURITY] ADMIN_PASSWORD_RESET_EMAIL user=%s admin=%s", user.email, _admin.get("email", "unknown"))
    return {"sent": True, "email": user.email}


@router.post(
    "/local/{local_user_id}/set-password",
    status_code=status.HTTP_200_OK,
    summary="Set a temporary password for a local user account",
)
def admin_set_password(
    local_user_id: int, body: LocalUserSetPassword, db: DbSession, _admin: AdminUser
) -> dict[str, Any]:
    """Directly set a new password for a local user without requiring an email token.

    Use this as a last resort when email delivery is unavailable.  The user
    should be advised to change their password after logging in.

    Raises:
        404: Local user not found.
    """
    user = db.query(LocalUser).filter(LocalUser.id == local_user_id).first()
    if not user:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Local user not found.")

    user.hashed_password = hash_password(body.password)
    # Clear any outstanding reset tokens and activate the account so the user
    # can log in immediately after an admin sets their password.
    user.password_reset_token = None
    user.password_reset_sent_at = None
    user.is_active = True

    try:
        db.commit()
    except Exception:
        db.rollback()
        raise

    logger.info("[SECURITY] ADMIN_SET_PASSWORD user=%s admin=%s", user.email, _admin.get("email", "unknown"))
    return {"updated": True, "email": user.email}


@router.get("/{user_id:path}", summary="Get details for a single user")
def get_user(user_id: str, db: DbSession, _admin: AdminUser) -> dict[str, Any]:
    """Return profile and document statistics for a specific user."""
    doc_count = db.query(func.count(FileRecord.id)).filter(FileRecord.owner_id == user_id).scalar() or 0
    last_row = (
        db.query(FileRecord.created_at)
        .filter(FileRecord.owner_id == user_id)
        .order_by(FileRecord.created_at.desc())
        .first()
    )
    last_upload = last_row[0].isoformat() if last_row and last_row[0] else None

    profile = _get_or_none(db, user_id)

    return {
        "user_id": user_id,
        "display_name": profile.display_name if profile else None,
        "daily_upload_limit": profile.daily_upload_limit if profile else None,
        "notes": profile.notes if profile else None,
        "is_blocked": profile.is_blocked if profile else False,
        "subscription_tier": (profile.subscription_tier or "free") if profile else "free",
        "subscription_billing_cycle": (profile.subscription_billing_cycle or "monthly") if profile else "monthly",
        "subscription_period_start": profile.subscription_period_start.isoformat()
        if (profile and profile.subscription_period_start)
        else None,
        "allow_overage": bool(profile.allow_overage) if profile else False,
        "is_complimentary": bool(profile.is_complimentary) if profile else False,
        "profile_id": profile.id if profile else None,
        "document_count": doc_count,
        "last_upload": last_upload,
        "profile": _profile_to_dict(profile) if profile else None,
    }


@router.put("/{user_id:path}", summary="Create or update a user profile")
def upsert_user_profile(
    user_id: str,
    body: UserProfileUpsert,
    db: DbSession,
    _admin: AdminUser,
) -> dict[str, Any]:
    """Create a new profile or update an existing one for *user_id*.

    Returns the persisted profile.
    """
    profile = _get_or_none(db, user_id)
    if profile is None:
        profile = UserProfile(user_id=user_id)
        db.add(profile)

    old_tier = (profile.subscription_tier or "free") if profile.id else None  # None means brand-new profile
    profile.display_name = body.display_name
    profile.daily_upload_limit = body.daily_upload_limit
    profile.notes = body.notes
    profile.is_blocked = body.is_blocked
    profile.subscription_billing_cycle = body.subscription_billing_cycle
    profile.subscription_period_start = body.subscription_period_start
    profile.allow_overage = body.allow_overage
    profile.is_complimentary = body.is_complimentary
    tier_changed = False
    new_tier: str | None = None
    if body.subscription_tier is not None:
        from app.utils.subscription import TIERS

        if body.subscription_tier not in TIERS:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail=f"Invalid subscription_tier '{body.subscription_tier}'. Valid values: {list(TIERS.keys())}",
            )
        # Detect a real change only for existing profiles (old_tier is not None)
        if old_tier is not None and old_tier != body.subscription_tier:
            tier_changed = True
            new_tier = body.subscription_tier
        profile.subscription_tier = body.subscription_tier

    try:
        db.commit()
        db.refresh(profile)
    except Exception:
        db.rollback()
        raise

    logger.info("Admin upserted profile for user %s", user_id)

    # Notify admins and fire webhook when plan is changed by an admin
    if tier_changed and new_tier is not None:
        try:
            from app.utils.notification import notify_plan_changed
            from app.utils.webhook import dispatch_webhook_event

            notify_plan_changed(user_id, old_tier=old_tier, new_tier=new_tier, changed_by="admin")  # type: ignore[arg-type]
            dispatch_webhook_event(
                "user.plan_changed",
                {
                    "user_id": user_id,
                    "old_tier": old_tier,
                    "new_tier": new_tier,
                    "billing_cycle": body.subscription_billing_cycle,
                    "changed_by": "admin",
                },
            )
        except Exception:
            logger.exception("Failed to send plan-change notification/webhook for user %s", user_id)

    return _profile_to_dict(profile)


@router.post(
    "/{user_id:path}/payment-issue", status_code=status.HTTP_200_OK, summary="Report a payment issue for a user"
)
def report_payment_issue(user_id: str, body: PaymentIssueBody, db: DbSession, _admin: AdminUser) -> dict[str, Any]:
    """Notify admins and fire a webhook for a payment issue reported against *user_id*.

    The user profile must exist.  Use this endpoint when a payment processor
    webhook or manual review identifies a billing problem (e.g. failed charge,
    expired card, disputed transaction).

    Returns the user profile dict alongside an acknowledgement flag.
    """
    profile = _get_or_none(db, user_id)
    if not profile:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User profile not found")

    logger.warning("Payment issue reported for user %s: %s", user_id, body.issue)

    try:
        from app.utils.notification import notify_payment_issue
        from app.utils.webhook import dispatch_webhook_event

        notify_payment_issue(user_id, issue=body.issue)
        dispatch_webhook_event(
            "user.payment_issue",
            {
                "user_id": user_id,
                "issue": body.issue,
            },
        )
    except Exception:
        logger.exception("Failed to send payment-issue notification/webhook for user %s", user_id)

    return {"acknowledged": True, "user_id": user_id, "profile": _profile_to_dict(profile)}


@router.delete("/{user_id:path}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete a user profile")
def delete_user_profile(user_id: str, db: DbSession, _admin: AdminUser) -> None:
    """Delete the admin-managed profile for *user_id*.

    Documents owned by this user are **not** removed; only the profile record
    is deleted.  To reassign or purge documents use the files API.
    """
    profile = _get_or_none(db, user_id)
    if not profile:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User profile not found")

    try:
        db.delete(profile)
        db.commit()
    except Exception:
        db.rollback()
        raise

    logger.info("Admin deleted profile for user %s", user_id)
