import re
from datetime import datetime, timezone
from typing import Annotated, Any, Optional

from fastapi import APIRouter, HTTPException, Query, status
from pymongo import ASCENDING, DESCENDING, ReturnDocument

from ..crud import NO_PASSWORD, SORTABLE, not_found, ref_filter, to_object_id
from ..db import get_users
from ..models import Page, PasswordChange, StatsSummary, UserCreate, UserOut, UserUpdate

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


def _search_clause(q: str) -> dict[str, Any]:
    rx = {"$regex": re.escape(q), "$options": "i"}
    return {
        "$or": [
            {"fname": rx},
            {"lname": rx},
            {"email": rx},
            {"user_id": rx},
            {"pen_name": rx},
            {"pen_name_email": rx},
            {"phone": rx},
            {"designation": rx},
        ]
    }


@router.get("", response_model=Page[UserOut], summary="List / search users")
async def list_users(
    q: Annotated[Optional[str], Query(description="Free-text search across name, email, user_id, pen name, phone")] = None,
    role: Annotated[Optional[list[int]], Query(description="Repeatable: role=1&role=2")] = None,
    status_: Annotated[Optional[int], Query(alias="status", ge=0, le=1)] = None,
    department: Annotated[Optional[list[int]], Query()] = None,
    emp_type: Annotated[Optional[int], Query()] = None,
    team_id: Annotated[Optional[str], Query()] = None,
    team: Annotated[Optional[str], Query()] = None,
    sort_by: Annotated[str, Query()] = "created_on",
    sort_dir: Annotated[str, Query(pattern="^(asc|desc)$")] = "desc",
    skip: Annotated[int, Query(ge=0)] = 0,
    limit: Annotated[int, Query(ge=1, le=200)] = 25,
):
    if sort_by not in SORTABLE:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"sort_by must be one of: {', '.join(sorted(SORTABLE))}",
        )

    query: dict[str, Any] = {}
    if q:
        query.update(_search_clause(q))
    if role:
        query["role"] = {"$in": role}
    if status_ is not None:
        query["status"] = status_
    if department:
        query["department"] = {"$in": department}
    if emp_type is not None:
        query["emp_type"] = emp_type
    if team:
        query["team"] = team
    if team_id:
        oid = to_object_id(team_id)
        if oid is None:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="team_id must be a 24-character ObjectId",
            )
        query["team_id"] = oid

    users = get_users()
    total = await users.count_documents(query)
    cursor = (
        users.find(query, NO_PASSWORD)
        .sort(sort_by, ASCENDING if sort_dir == "asc" else DESCENDING)
        .skip(skip)
        .limit(limit)
    )
    return Page[UserOut](
        total=total, skip=skip, limit=limit, items=await cursor.to_list(length=limit)
    )


@router.get("/stats/summary", response_model=StatsSummary, summary="Aggregate user stats")
async def stats_summary():
    users = get_users()
    pipeline = [
        {
            "$group": {
                "_id": None,
                "total": {"$sum": 1},
                "active": {"$sum": {"$cond": [{"$eq": ["$status", 1]}, 1, 0]}},
                "total_score": {"$sum": {"$ifNull": ["$total_score", 0]}},
                "total_article_count": {"$sum": {"$ifNull": ["$article_count", 0]}},
                "total_queries_count": {"$sum": {"$ifNull": ["$queries_count", 0]}},
            }
        }
    ]
    # PyMongo's async aggregate() is a coroutine returning the cursor.
    cursor = await users.aggregate(pipeline)
    agg = await cursor.to_list(length=1)
    base = agg[0] if agg else {}
    total = base.get("total", 0)

    async def bucket(field: str) -> dict[str, int]:
        rows = await (
            await users.aggregate(
                [{"$group": {"_id": f"${field}", "n": {"$sum": 1}}}, {"$sort": {"_id": 1}}]
            )
        ).to_list(length=None)
        return {str(r["_id"]): r["n"] for r in rows}

    return StatsSummary(
        total=total,
        active=base.get("active", 0),
        inactive=total - base.get("active", 0),
        by_role=await bucket("role"),
        by_department=await bucket("department"),
        by_emp_type=await bucket("emp_type"),
        total_score=base.get("total_score", 0),
        total_article_count=base.get("total_article_count", 0),
        total_queries_count=base.get("total_queries_count", 0),
    )


@router.get("/{user_ref}", response_model=UserOut, summary="Get one user by _id or user_id")
async def get_user(user_ref: str):
    doc = await get_users().find_one(ref_filter(user_ref), NO_PASSWORD)
    if not doc:
        raise not_found(user_ref)
    return doc


@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED, summary="Create a user")
async def create_user(payload: UserCreate):
    users = get_users()

    if await users.find_one({"user_id": payload.user_id}, {"_id": 1}):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"user_id '{payload.user_id}' already exists",
        )
    if payload.email and await users.find_one({"email": payload.email}, {"_id": 1}):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"email '{payload.email}' already exists",
        )

    doc = payload.model_dump(exclude_none=True)
    for oid_field in ("team_id", "country", "state"):
        if doc.get(oid_field):
            oid = to_object_id(doc[oid_field])
            if oid is not None:
                doc[oid_field] = oid

    # Match the counter fields every existing ighub user carries.
    doc.setdefault("created_on", datetime.now(timezone.utc))
    for counter in (
        "available_amount",
        "queries_count",
        "automatic_removed_count",
        "bounced_removed_count",
        "read_removed_count",
        "unsub_removed_count",
        "day_score",
        "total_score",
        "article_count",
    ):
        doc.setdefault(counter, 0)

    result = await users.insert_one(doc)
    return await users.find_one({"_id": result.inserted_id}, NO_PASSWORD)


@router.patch("/{user_ref}", response_model=UserOut, summary="Partially update a user")
async def update_user(user_ref: str, payload: UserUpdate):
    changes = payload.model_dump(exclude_unset=True)
    if not changes:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail="No fields supplied"
        )

    for oid_field in ("team_id", "country", "state"):
        if changes.get(oid_field):
            oid = to_object_id(changes[oid_field])
            if oid is not None:
                changes[oid_field] = oid

    users = get_users()
    if "user_id" in changes:
        clash = await users.find_one(
            {"user_id": changes["user_id"], **{"$nor": [ref_filter(user_ref)]}}, {"_id": 1}
        )
        if clash:
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail=f"user_id '{changes['user_id']}' already exists",
            )

    doc = await users.find_one_and_update(
        ref_filter(user_ref),
        {"$set": changes},
        projection=NO_PASSWORD,
        return_document=ReturnDocument.AFTER,
    )
    if not doc:
        raise not_found(user_ref)
    return doc


@router.put("/{user_ref}/password", status_code=status.HTTP_204_NO_CONTENT, summary="Change a password")
async def change_password(user_ref: str, payload: PasswordChange):
    users = get_users()
    doc = await users.find_one(ref_filter(user_ref), {"password": 1})
    if not doc:
        raise not_found(user_ref)

    if payload.current_password is not None and doc.get("password") != payload.current_password:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="current_password does not match"
        )

    await users.update_one({"_id": doc["_id"]}, {"$set": {"password": payload.new_password}})


@router.delete("/{user_ref}", response_model=UserOut, summary="Deactivate (default) or hard-delete a user")
async def delete_user(
    user_ref: str,
    hard: Annotated[bool, Query(description="true permanently removes the document")] = False,
):
    users = get_users()

    if hard:
        doc = await users.find_one_and_delete(ref_filter(user_ref), projection=NO_PASSWORD)
        if not doc:
            raise not_found(user_ref)
        return doc

    doc = await users.find_one_and_update(
        ref_filter(user_ref),
        {"$set": {"status": 0}},
        projection=NO_PASSWORD,
        return_document=ReturnDocument.AFTER,
    )
    if not doc:
        raise not_found(user_ref)
    return doc
