from __future__ import annotations

import re
from dataclasses import replace
from datetime import UTC, date, datetime, timedelta
from io import BytesIO
from typing import Annotated

from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
from fastapi.responses import StreamingResponse
from openpyxl import Workbook, load_workbook
from pydantic import BaseModel
from pydantic import ValidationError
from sqlalchemy import extract, func, or_, select
from sqlalchemy.orm import Session

from app.core.database import get_db
from app.core.dependencies import get_current_admin
from app.core.responses import api_success
from app.models.admin import Admin
from app.models.ai_config import SystemConfig
from app.models.entitlement import EntitlementPlan, UserEntitlement
from app.models.chat import ChatMessage, ChatSession, TopicSession
from app.models.growth import ShareDraft, TeacherHelpCard, TopicSummary
from app.models.logs import AiRequestLog
from app.models.user import User
from app.schemas.admin import AdminUserCreateRequest, AdminUserImportItem, AdminUserImportRequest, AdminUserUpdateRequest
from app.services.admin_service import OperationLogService
from app.services.entitlement_service import EntitlementService, entitlement_dict, view_from_plan
from app.services.growth_profile_service import GrowthProfileService, growth_profile_dict, topic_dict, topic_summary_dict
from app.services.help_card_service import help_card_dict
from app.services.periodic_report_service import PeriodicReportService, periodic_report_dict
from app.services.share_draft_service import share_draft_dict
from app.api.pagination import page_result

router = APIRouter()

STUDENT_TEMPLATE_HEADERS = ["手机号", "姓名", "昵称", "每日聊天额度", "状态", "有效期"]


class AdminGenerateReportRequest(BaseModel):
    reportType: str
    periodStart: datetime | None = None
    periodEnd: datetime | None = None


@router.get("/user/list")
def list_users(
    keyword: str = Query(default=""),
    planId: Annotated[int | None, Query(gt=0)] = None,
    entitlementStatus: Annotated[str, Query(pattern="^(all|active|expiring7|expiring30|expired|default)$")] = "all",
    page: int = Query(default=1, ge=1),
    pageSize: int = Query(default=20, ge=10, le=100),
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    query = select(User).where(User.is_deleted == 0).order_by(User.id.desc())
    if keyword:
        like = f"%{keyword}%"
        query = query.where((User.phone.like(like)) | (User.name.like(like)))
    now = datetime.now(UTC).replace(tzinfo=None)
    active_user_ids = select(UserEntitlement.user_id).join(
        EntitlementPlan, EntitlementPlan.id == UserEntitlement.plan_id
    ).where(
        UserEntitlement.status == "active",
        EntitlementPlan.status == 1,
        EntitlementPlan.plan_type != "teacher",
        (UserEntitlement.effective_at.is_(None)) | (UserEntitlement.effective_at <= now),
        (UserEntitlement.expired_at.is_(None)) | (UserEntitlement.expired_at >= now),
    )
    if planId is not None:
        query = query.where(User.id.in_(active_user_ids.where(UserEntitlement.plan_id == planId)))
    if entitlementStatus == "active":
        query = query.where(User.id.in_(active_user_ids))
    elif entitlementStatus == "expiring7":
        query = query.where(
            User.id.in_(
                active_user_ids.where(
                    UserEntitlement.expired_at.is_not(None),
                    UserEntitlement.expired_at <= now + timedelta(days=7),
                )
            )
        )
    elif entitlementStatus == "expiring30":
        query = query.where(
            User.id.in_(
                active_user_ids.where(
                    UserEntitlement.expired_at.is_not(None),
                    UserEntitlement.expired_at > now + timedelta(days=7),
                    UserEntitlement.expired_at <= now + timedelta(days=30),
                )
            )
        )
    elif entitlementStatus == "default":
        query = query.where(User.id.not_in(active_user_ids))
    elif entitlementStatus == "expired":
        expired_user_ids = select(UserEntitlement.user_id).join(
            EntitlementPlan, EntitlementPlan.id == UserEntitlement.plan_id
        ).where(
            UserEntitlement.status.in_(("active", "expired")),
            UserEntitlement.expired_at.is_not(None),
            UserEntitlement.expired_at < now,
            EntitlementPlan.plan_type != "teacher",
        )
        query = query.where(User.id.not_in(active_user_ids), User.id.in_(expired_user_ids))
    total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
    users = db.scalars(query.offset((page - 1) * pageSize).limit(pageSize)).all()
    entitlements = _entitlement_views(db, users)
    return api_success(page_result([_user_dict(user, entitlements.get(user.id)) for user in users], total=total, page=page, page_size=pageSize))


@router.post("/user")
def create_user(
    payload: AdminUserCreateRequest,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    phone = _normalize_phone(payload.phone)
    _validate_phone(phone)
    existing = db.scalar(select(User).where(User.phone == phone))
    if existing is not None and not existing.is_deleted:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="手机号已存在")

    if existing is not None:
        user = existing
        user.is_deleted = 0
        user.daily_chat_used = 0
    else:
        user = User(phone=phone, daily_chat_used=0)

    _apply_user_payload(user, payload, _default_daily_chat_limit(db))
    db.add(user)
    db.flush()
    OperationLogService.write(db, admin_id=current_admin.id, module="user", action="create", target_id=user.id)
    db.commit()
    db.refresh(user)
    entitlement = EntitlementService.active_entitlement(
        db,
        user,
        monthly_topic_used=_monthly_topic_counts(db, [user.id]).get(user.id, 0),
    )
    return api_success(_user_dict(user, entitlement_dict(entitlement)))


@router.post("/user/import")
def import_users(
    payload: AdminUserImportRequest,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    if not payload.students:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请提供学员数据")

    return api_success(_import_user_items(list(enumerate(payload.students, start=1)), db, current_admin))


@router.get("/user/import/template")
def download_user_import_template(
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> StreamingResponse:
    workbook = Workbook()
    sheet = workbook.active
    sheet.title = "学员导入模板"
    sheet.append(STUDENT_TEMPLATE_HEADERS)
    sheet.append(["13800000001", "张三", "三三", _default_daily_chat_limit(db), "启用", "2026-12-31 23:59:59"])
    sheet.append(["13800000002", "李四", "", "", "启用", ""])

    widths = [18, 14, 14, 16, 12, 22]
    for index, width in enumerate(widths, start=1):
        sheet.column_dimensions[chr(64 + index)].width = width

    stream = BytesIO()
    workbook.save(stream)
    stream.seek(0)
    return StreamingResponse(
        stream,
        media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        headers={"Content-Disposition": 'attachment; filename="student_import_template.xlsx"'},
    )


@router.post("/user/import/excel")
def import_users_excel(
    file: UploadFile = File(...),
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    filename = (file.filename or "").lower()
    if not filename.endswith(".xlsx"):
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请上传 .xlsx 格式的 Excel 文件")

    content = file.file.read()
    if not content:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Excel 文件为空")

    try:
        workbook = load_workbook(BytesIO(content), data_only=True)
    except Exception as exc:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Excel 文件解析失败") from exc

    sheet = workbook.active
    header_map = _excel_header_map(next(sheet.iter_rows(min_row=1, max_row=1, values_only=True), ()))
    for required in ("手机号", "姓名"):
        if required not in header_map:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"模板缺少必填列：{required}")

    students: list[tuple[int, AdminUserImportItem]] = []
    parse_failures: list[dict] = []
    for row_number, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2):
        if not any(_cell_text(cell) for cell in row):
            continue
        try:
            item = AdminUserImportItem(
                phone=_cell_text(_excel_value(row, header_map, "手机号")),
                name=_cell_text(_excel_value(row, header_map, "姓名")),
                nickname=_cell_text(_excel_value(row, header_map, "昵称")) or None,
                dailyChatLimit=_parse_optional_int(_excel_value(row, header_map, "每日聊天额度")),
                status=_parse_status(_excel_value(row, header_map, "状态")),
                expiredAt=_parse_optional_datetime(_excel_value(row, header_map, "有效期")),
            )
            students.append((row_number, item))
        except (ValueError, ValidationError) as exc:
            parse_failures.append(
                {
                    "row": row_number,
                    "phone": _cell_text(_excel_value(row, header_map, "手机号")),
                    "reason": _row_error_message(exc),
                }
            )

    if not students and not parse_failures:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Excel 中没有可导入的学员数据")

    result = _import_user_items(students, db, current_admin, parse_failures)
    return api_success(result)


def _import_user_items(
    students: list[tuple[int, AdminUserImportItem]],
    db: Session,
    current_admin: Admin,
    initial_failures: list[dict] | None = None,
) -> dict:
    created = 0
    updated = 0
    failed: list[dict] = list(initial_failures or [])
    seen: set[str] = set()
    default_daily_limit = _default_daily_chat_limit(db)

    for row_number, item in students:
        phone = _normalize_phone(item.phone)
        name = item.name.strip()
        try:
            _validate_phone(phone)
            if not name:
                raise ValueError("姓名不能为空")
            if phone in seen:
                raise ValueError("手机号在本次导入中重复")
            seen.add(phone)

            user = db.scalar(select(User).where(User.phone == phone))
            if user is None:
                user = User(phone=phone, daily_chat_used=0)
                created += 1
            else:
                user.is_deleted = 0
                updated += 1
            _apply_user_payload(user, item, default_daily_limit)
            db.add(user)
        except ValueError as exc:
            failed.append({"row": row_number, "phone": item.phone, "reason": str(exc)})

    if created or updated:
        db.flush()
        OperationLogService.write(
            db,
            admin_id=current_admin.id,
            module="user",
            action="import",
            target_id=None,
            result="SUCCESS",
        )
        db.commit()

    return {"created": created, "updated": updated, "failed": len(failed), "failures": failed}


@router.get("/user/{user_id}")
def user_detail(
    user_id: int,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    user = _get_user(db, user_id)
    return api_success(_user_dict(user))


@router.get("/user/{user_id}/detail")
def user_operation_detail(
    user_id: int,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    user = _get_user(db, user_id)
    entitlement = EntitlementService.active_entitlement(
        db,
        user,
        monthly_topic_used=_monthly_topic_counts(db, [user.id]).get(user.id, 0),
    )
    return api_success(
        {
            "user": _user_dict(user, entitlement_dict(entitlement)),
            "metrics": _user_metrics(db, user=user, monthly_topic_limit=entitlement.monthly_topic_limit),
            "growthProfile": growth_profile_dict(GrowthProfileService.get_growth_profile(db, user.id)),
            "recentTopics": _recent_topics(db, user.id),
            "recentHelpCards": [help_card_dict(item) for item in _recent_help_cards(db, user.id)],
            "recentShareDrafts": [share_draft_dict(item) for item in _recent_share_drafts(db, user.id)],
            "recentReports": [
                periodic_report_dict(item)
                for item in PeriodicReportService.list_user_reports(db, user_id=user.id, limit=10)
            ],
        }
    )


@router.get("/user/{user_id}/topics")
def user_topic_options(
    user_id: int,
    keyword: str = Query(default="", max_length=100),
    page: int = Query(default=1, ge=1),
    pageSize: int = Query(default=50, ge=10, le=100),
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    """Lightweight topic selector data for Agent preview; no message bodies are returned."""
    user = _get_user(db, user_id)
    conditions = [TopicSession.user_id == user.id]
    normalized_keyword = keyword.strip()
    if normalized_keyword:
        like = f"%{normalized_keyword}%"
        conditions.append(or_(TopicSession.title.like(like), TopicSession.core_question.like(like)))
    total = db.scalar(select(func.count(TopicSession.id)).where(*conditions)) or 0
    rows = db.execute(
        select(TopicSession, TopicSummary.id)
        .join(TopicSummary, TopicSummary.topic_session_id == TopicSession.id, isouter=True)
        .where(*conditions)
        .order_by(TopicSession.updated_at.desc(), TopicSession.id.desc())
        .offset((page - 1) * pageSize)
        .limit(pageSize)
    ).all()
    items = [
        {
            **topic_dict(topic),
            "summaryAvailable": summary_id is not None,
        }
        for topic, summary_id in rows
    ]
    return api_success(page_result(items, total=total, page=page, page_size=pageSize))


@router.get("/user/{user_id}/reports")
def user_reports(
    user_id: int,
    limit: int = Query(default=20, ge=1, le=50),
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    user = _get_user(db, user_id)
    reports = PeriodicReportService.list_user_reports(db, user_id=user.id, limit=limit)
    return api_success([periodic_report_dict(item) for item in reports])


@router.post("/user/{user_id}/reports/generate")
def generate_user_report(
    user_id: int,
    payload: AdminGenerateReportRequest,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    user = _get_user(db, user_id)
    if payload.reportType not in ("weekly", "monthly", "stage"):
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不支持的报告类型")
    try:
        report = PeriodicReportService.enqueue_for_user(
            db,
            user=user,
            report_type=payload.reportType,  # type: ignore[arg-type]
            period_start=payload.periodStart,
            period_end=payload.periodEnd,
            generated_by=f"admin:{current_admin.id}",
        )
    except ValueError as exc:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
    OperationLogService.write(db, admin_id=current_admin.id, module="user_report", action="enqueue", target_id=report.id)
    db.commit()
    db.refresh(report)
    return api_success(periodic_report_dict(report), message="报告任务已进入后台队列")


@router.post("/user/{user_id}/topic/{topic_id}/settlement/retry")
def retry_topic_settlement(
    user_id: int,
    topic_id: int,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    user = _get_user(db, user_id)
    topic = db.scalar(
        select(TopicSession).where(TopicSession.id == topic_id, TopicSession.user_id == user.id)
    )
    if topic is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="主题不存在")
    if topic.status != "completed":
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="只能重试已结束主题的沉淀任务")
    summary = GrowthProfileService.retry_topic_settlement(db, user=user, topic=topic)
    OperationLogService.write(
        db,
        admin_id=current_admin.id,
        module="topic_settlement",
        action="retry",
        target_id=summary.id,
    )
    db.commit()
    db.refresh(summary)
    return api_success(topic_summary_dict(summary), message="主题沉淀任务已重新进入队列")


@router.put("/user/{user_id}")
def update_user(
    user_id: int,
    payload: AdminUserUpdateRequest,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    user = _get_user(db, user_id)
    if payload.status is not None:
        user.status = payload.status
    if payload.dailyChatLimit is not None:
        user.daily_chat_limit = payload.dailyChatLimit
    if payload.expiredAt is not None:
        user.expired_at = payload.expiredAt.replace(tzinfo=None)
    db.add(user)
    if payload.entitlementPlanId is not None:
        EntitlementService.assign_user_plan(
            db,
            user=user,
            plan_id=payload.entitlementPlanId,
            operated_by=current_admin.id,
            expired_at=payload.entitlementExpiredAt,
            remark=payload.entitlementRemark,
        )
    OperationLogService.write(db, admin_id=current_admin.id, module="user", action="update", target_id=user.id)
    db.commit()
    db.refresh(user)
    entitlement = EntitlementService.active_entitlement(
        db,
        user,
        monthly_topic_used=_monthly_topic_counts(db, [user.id]).get(user.id, 0),
    )
    return api_success(_user_dict(user, entitlement_dict(entitlement)))


@router.delete("/user/{user_id}")
def delete_user(
    user_id: int,
    db: Session = Depends(get_db),
    current_admin: Admin = Depends(get_current_admin),
) -> dict:
    user = _get_user(db, user_id)
    user.is_deleted = 1
    db.add(user)
    OperationLogService.write(db, admin_id=current_admin.id, module="user", action="delete", target_id=user.id)
    db.commit()
    return api_success()


def _get_user(db: Session, user_id: int) -> User:
    user = db.get(User, user_id)
    if user is None or user.is_deleted:
        from fastapi import HTTPException, status

        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
    return user


def _user_dict(user: User, entitlement: dict | None = None) -> dict:
    return {
        "id": user.id,
        "phone": user.phone,
        "name": user.name,
        "nickname": user.nickname,
        "registrationSource": user.registration_source,
        "registrationClientId": user.registration_client_id,
        "status": user.status,
        "dailyChatLimit": user.daily_chat_limit,
        "dailyChatUsed": user.daily_chat_used,
        "expiredAt": user.expired_at,
        "lastLoginAt": user.last_login_at,
        "createdAt": user.created_at,
        "entitlement": entitlement,
    }


def _entitlement_views(db: Session, users: list[User]) -> dict[int, dict]:
    user_ids = [user.id for user in users]
    if not user_ids:
        return {}
    counts = _monthly_topic_counts(db, user_ids)
    explicit = _active_entitlement_rows(db, user_ids)
    previous_expired = _latest_expired_entitlement_rows(db, user_ids)
    default_plan = EntitlementService.default_plan(db)
    result: dict[int, dict] = {}
    for user in users:
        if user.id in explicit:
            entitlement, plan = explicit[user.id]
            view = view_from_plan(plan, monthly_topic_used=counts.get(user.id, 0), entitlement=entitlement, source="assigned")
        else:
            if default_plan is None:
                view = EntitlementService.active_entitlement(db, user, monthly_topic_used=counts.get(user.id, 0))
            else:
                view = view_from_plan(
                    default_plan,
                    monthly_topic_used=counts.get(user.id, 0),
                    entitlement=None,
                    source="default",
                )
                if user.id in previous_expired:
                    expired_entitlement, expired_plan = previous_expired[user.id]
                    view = replace(
                        view,
                        source="default_after_expiry",
                        lifecycle_status="expired_fallback",
                        previous_plan_name=expired_plan.name,
                        previous_expired_at=expired_entitlement.expired_at,
                    )
        result[user.id] = entitlement_dict(view)
    return result


def _active_entitlement_rows(db: Session, user_ids: list[int]) -> dict[int, tuple[UserEntitlement, EntitlementPlan]]:
    now = datetime.now(UTC).replace(tzinfo=None)
    rows = db.execute(
        select(UserEntitlement, EntitlementPlan)
        .join(EntitlementPlan, EntitlementPlan.id == UserEntitlement.plan_id)
        .where(
            UserEntitlement.user_id.in_(user_ids),
            UserEntitlement.status == "active",
            EntitlementPlan.status == 1,
        )
        .where((UserEntitlement.effective_at.is_(None)) | (UserEntitlement.effective_at <= now))
        .where((UserEntitlement.expired_at.is_(None)) | (UserEntitlement.expired_at >= now))
        .order_by(UserEntitlement.created_at.desc(), UserEntitlement.id.desc())
    ).all()
    result: dict[int, tuple[UserEntitlement, EntitlementPlan]] = {}
    for entitlement, plan in rows:
        result.setdefault(entitlement.user_id, (entitlement, plan))
    return result


def _latest_expired_entitlement_rows(db: Session, user_ids: list[int]) -> dict[int, tuple[UserEntitlement, EntitlementPlan]]:
    now = datetime.now(UTC).replace(tzinfo=None)
    rows = db.execute(
        select(UserEntitlement, EntitlementPlan)
        .join(EntitlementPlan, EntitlementPlan.id == UserEntitlement.plan_id)
        .where(
            UserEntitlement.user_id.in_(user_ids),
            UserEntitlement.status.in_(("active", "expired")),
            UserEntitlement.expired_at.is_not(None),
            UserEntitlement.expired_at < now,
            EntitlementPlan.plan_type != "teacher",
        )
        .order_by(UserEntitlement.expired_at.desc(), UserEntitlement.id.desc())
    ).all()
    result: dict[int, tuple[UserEntitlement, EntitlementPlan]] = {}
    for entitlement, plan in rows:
        result.setdefault(entitlement.user_id, (entitlement, plan))
    return result


def _user_metrics(db: Session, *, user: User, monthly_topic_limit: int | None) -> dict:
    now = datetime.now(UTC).replace(tzinfo=None)
    month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
    recent_start = now - timedelta(days=30)
    total_sessions = db.scalar(select(func.count(ChatSession.id)).where(ChatSession.user_id == user.id, ChatSession.is_deleted == 0)) or 0
    total_messages = db.scalar(select(func.count(ChatMessage.id)).where(ChatMessage.user_id == user.id)) or 0
    total_topics = db.scalar(select(func.count(TopicSession.id)).where(TopicSession.user_id == user.id)) or 0
    month_topics = db.scalar(
        select(func.count(TopicSession.id)).where(TopicSession.user_id == user.id, TopicSession.started_at >= month_start)
    ) or 0
    recent_active_days = db.scalar(
        select(func.count(func.distinct(func.date(ChatMessage.created_at)))).where(
            ChatMessage.user_id == user.id,
            ChatMessage.created_at >= recent_start,
        )
    ) or 0
    last_message_at = db.scalar(select(func.max(ChatMessage.created_at)).where(ChatMessage.user_id == user.id))
    token_row = db.execute(
        select(
            func.coalesce(func.sum(AiRequestLog.input_token), 0),
            func.coalesce(func.sum(AiRequestLog.output_token), 0),
            func.coalesce(func.sum(AiRequestLog.total_token), 0),
            func.coalesce(func.sum(AiRequestLog.estimated_cost), 0),
            func.max(AiRequestLog.currency),
            func.count(AiRequestLog.id),
        ).where(AiRequestLog.user_id == user.id)
    ).one()
    help_card_count = db.scalar(select(func.count(TeacherHelpCard.id)).where(TeacherHelpCard.user_id == user.id)) or 0
    share_draft_count = db.scalar(select(func.count(ShareDraft.id)).where(ShareDraft.user_id == user.id)) or 0
    usage_ratio = (month_topics / monthly_topic_limit) if monthly_topic_limit else None
    return {
        "totalSessions": int(total_sessions),
        "totalMessages": int(total_messages),
        "totalTopics": int(total_topics),
        "monthTopics": int(month_topics),
        "recentActiveDays": int(recent_active_days),
        "lastMessageAt": last_message_at,
        "aiRequestCount": int(token_row[5] or 0),
        "inputToken": int(token_row[0] or 0),
        "outputToken": int(token_row[1] or 0),
        "totalToken": int(token_row[2] or 0),
        "estimatedCost": float(token_row[3] or 0),
        "costCurrency": token_row[4] or "CNY",
        "helpCardCount": int(help_card_count),
        "shareDraftCount": int(share_draft_count),
        "isHighFrequency": bool(usage_ratio is not None and usage_ratio >= 0.8) or recent_active_days >= 15,
        "isInactive": total_sessions == 0 or last_message_at is None or last_message_at < recent_start,
    }


def _recent_topics(db: Session, user_id: int, *, limit: int = 10) -> list[dict]:
    rows = db.execute(
        select(TopicSession, TopicSummary)
        .join(TopicSummary, TopicSummary.topic_session_id == TopicSession.id, isouter=True)
        .where(TopicSession.user_id == user_id)
        .order_by(TopicSession.updated_at.desc(), TopicSession.id.desc())
        .limit(limit)
    ).all()
    return [
        {
            **topic_dict(topic),
            "summary": topic_summary_dict(summary) if summary else None,
        }
        for topic, summary in rows
    ]


def _recent_help_cards(db: Session, user_id: int, *, limit: int = 10) -> list[TeacherHelpCard]:
    return list(
        db.scalars(
            select(TeacherHelpCard)
            .where(TeacherHelpCard.user_id == user_id)
            .order_by(TeacherHelpCard.created_at.desc(), TeacherHelpCard.id.desc())
            .limit(limit)
        )
    )


def _recent_share_drafts(db: Session, user_id: int, *, limit: int = 10) -> list[ShareDraft]:
    return list(
        db.scalars(
            select(ShareDraft)
            .where(ShareDraft.user_id == user_id)
            .order_by(ShareDraft.created_at.desc(), ShareDraft.id.desc())
            .limit(limit)
        )
    )


def _monthly_topic_counts(db: Session, user_ids: list[int]) -> dict[int, int]:
    if not user_ids:
        return {}
    now = datetime.now(UTC).replace(tzinfo=None)
    rows = db.execute(
        select(TopicSession.user_id, func.count(TopicSession.id))
        .where(
            TopicSession.user_id.in_(user_ids),
            TopicSession.quota_deducted == 1,
            extract("year", TopicSession.started_at) == now.year,
            extract("month", TopicSession.started_at) == now.month,
        )
        .group_by(TopicSession.user_id)
    ).all()
    return {int(user_id): int(count) for user_id, count in rows}


def _apply_user_payload(
    user: User,
    payload: AdminUserCreateRequest | AdminUserImportItem,
    default_daily_limit: int,
) -> None:
    user.name = payload.name.strip()
    user.nickname = payload.nickname.strip() if payload.nickname else None
    user.status = payload.status
    user.daily_chat_limit = payload.dailyChatLimit if payload.dailyChatLimit is not None else default_daily_limit
    user.expired_at = payload.expiredAt.replace(tzinfo=None) if payload.expiredAt is not None else None


def _default_daily_chat_limit(db: Session) -> int:
    config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == "daily_chat_limit"))
    if config is not None and config.config_value.strip():
        try:
            return max(0, min(100000, int(float(config.config_value.strip()))))
        except ValueError:
            pass
    try:
        from app.core.config import get_settings

        return get_settings().default_daily_chat_limit
    except Exception:
        return 100


def _excel_header_map(headers: tuple) -> dict[str, int]:
    return {_cell_text(header): index for index, header in enumerate(headers) if _cell_text(header)}


def _excel_value(row: tuple, header_map: dict[str, int], header: str) -> object | None:
    index = header_map.get(header)
    if index is None or index >= len(row):
        return None
    return row[index]


def _cell_text(value: object | None) -> str:
    if value is None:
        return ""
    if isinstance(value, float) and value.is_integer():
        return str(int(value))
    return str(value).strip()


def _parse_optional_int(value: object | None) -> int | None:
    text = _cell_text(value)
    if not text:
        return None
    try:
        parsed = int(float(text))
    except ValueError as exc:
        raise ValueError("每日聊天额度必须是数字") from exc
    if parsed < 0 or parsed > 100000:
        raise ValueError("每日聊天额度必须在 0 到 100000 之间")
    return parsed


def _parse_status(value: object | None) -> int:
    text = _cell_text(value).lower()
    if not text:
        return 1
    if text in {"1", "启用", "正常", "是", "true", "enabled"}:
        return 1
    if text in {"0", "禁用", "停用", "否", "false", "disabled"}:
        return 0
    raise ValueError("状态只能填写启用/禁用或 1/0")


def _parse_optional_datetime(value: object | None) -> datetime | None:
    if value is None or _cell_text(value) == "":
        return None
    if isinstance(value, datetime):
        return value.replace(tzinfo=None)
    if isinstance(value, date):
        return datetime.combine(value, datetime.min.time())

    text = _cell_text(value)
    for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%Y/%m/%d %H:%M:%S", "%Y/%m/%d"):
        try:
            return datetime.strptime(text, fmt)
        except ValueError:
            continue
    raise ValueError("有效期格式应为 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss")


def _row_error_message(exc: ValueError | ValidationError) -> str:
    if isinstance(exc, ValidationError):
        return "字段格式不正确"
    return str(exc)


def _normalize_phone(phone: str) -> str:
    return re.sub(r"\D", "", phone or "")


def _validate_phone(phone: str) -> None:
    if not re.fullmatch(r"1[3-9]\d{9}", phone):
        raise ValueError("手机号格式不正确")
