from datetime import datetime, timezone, timedelta
import json

from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession

from app.dependencies import get_db, get_admin_user
from app.models.user import User
from app.models.project import Project
from app.models.generation_record import GenerationRecord
from app.models.chat_generation_task import ChatGenerationTask
from app.models.credit_record import CreditRecord
from app.models.model_config import ModelConfig
from app.models.system_config import SystemConfig
from app.models.payment_order import PaymentOrder
from app.models.token_usage import TokenUsage
from app.models.notification import Notification
from app.models.notification_read import NotificationRead
from app.models.industry_config import IndustryConfig
from app.models.video_engine import VideoEngine
from app.models.image_engine import ImageEngine
from app.models.credit_ratio import CreditRatio
from app.models.operation_log import OperationLog
from app.enums.user import FrontendUserKind, UserType
from app.schemas.admin import (
    CreditAdjustRequest,
    ModelConfigCreate,
    ModelConfigOut,
    SystemConfigUpdate,
    SystemConfigOut,
    AdminUserOut,
    AdminStatsOut,
    CreateUserRequest,
    UpdateMenusRequest,
    ResetPasswordRequest,
    UpdateFrontendUserKindRequest,
    OperationLogOut,
)
from app.schemas.industry import IndustryConfigCreate, IndustryConfigOut
from app.schemas.video_engine import VideoEngineCreate, VideoEngineOut
from app.schemas.image_engine import ImageEngineCreate, ImageEngineOut
from app.schemas.credit_ratio import CreditRatioCreate, CreditRatioOut
from app.services.credits import add_credits, deduct_credits
from app.services.credit_record_meta_service import build_admin_adjust_meta
from app.services.admin_credit_record_service import list_admin_credit_records
from app.services.notification import create_notification
from app.services.auth import hash_password, verify_password
from app.services.operation_log import log_operation
from app.services.resource_signed_url_service import build_resource_signed_url
from app.services.payment import sync_pending_orders, process_refund

from app.services.generation_billing_service import (
    OWNER_GENERATION_RECORD,
    charge_generation_media_by_params,
    get_next_credit_attempt_no,
)
from app.services.generation_refund_service import mark_generation_record_failed_and_refund_once
from app.utils.id_gen import generate_id
from app.schemas.generation import GenerationType, ASPECT_RATIOS, RESOLUTIONS


CST = timezone(timedelta(hours=8))


def _iso(dt):
    """Serialize datetime as naive ISO string (UTC→CST, strip tzinfo)."""
    if dt is None:
        return None
    try:
        d = dt if isinstance(dt, datetime) else datetime.fromisoformat(str(dt))
        if d.tzinfo and d.tzinfo.utcoffset(None) == timedelta(0):
            d = d.astimezone(CST)
        return d.replace(tzinfo=None).isoformat()
    except (ValueError, TypeError):
        return str(dt)


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


# ── User Management ──────────────────────────────────────

@router.get("/users")
async def list_users(
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=1000),
    search: str = Query(""),
    user_type: str | None = Query(None, pattern="^(frontend|admin)$"),
    frontend_user_kind: str | None = Query(None, pattern="^(internal|external)$"),
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    query = select(User).order_by(User.created_at.desc())
    count_query = select(func.count(User.id))
    if search:
        like = f"%{search}%"
        search_filter = (User.username.ilike(like)) | (User.email.ilike(like)) | (User.phone.ilike(like))
        query = query.where(search_filter)
        count_query = count_query.where(search_filter)
    if user_type:
        query = query.where(User.user_type == user_type)
        count_query = count_query.where(User.user_type == user_type)
    if frontend_user_kind:
        query = query.where(User.user_type == UserType.FRONTEND.value, User.frontend_user_kind == frontend_user_kind)
        count_query = count_query.where(User.user_type == UserType.FRONTEND.value, User.frontend_user_kind == frontend_user_kind)
    total = (await db.execute(count_query)).scalar() or 0
    result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
    items = result.scalars().all()
    return {"items": [AdminUserOut.model_validate(u) for u in items], "total": total}


@router.post("/users", response_model=AdminUserOut)
async def create_user(
    req: CreateUserRequest,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    import random
    # Frontend users: use phone, auto-generate username
    if req.user_type == "frontend":
        if not req.phone:
            raise HTTPException(status_code=400, detail="前台用户必须填写手机号")
        existing_phone = await db.execute(select(User).where(User.phone == req.phone).limit(1))
        if existing_phone.scalar_one_or_none():
            raise HTTPException(status_code=400, detail="该手机号已注册")
        username = f"用户{req.phone[-4:]}"
        existing_name = await db.execute(select(User).where(User.username == username).limit(1))
        if existing_name.scalar_one_or_none():
            username = f"用户{req.phone[-4:]}{random.randint(10, 99)}"
    else:
        if not req.username:
            raise HTTPException(status_code=400, detail="后台用户必须填写用户名")
        username = req.username
        existing = await db.execute(select(User).where(User.username == username).limit(1))
        if existing.scalar_one_or_none():
            raise HTTPException(status_code=400, detail="用户名已存在")

    user = User(
        id=generate_id(),
        username=username,
        hashed_password=hash_password(req.password),
        email=req.email,
        phone=req.phone,
        credits=req.credits,
        is_admin=(req.user_type == "admin"),
        user_type=req.user_type,
        frontend_user_kind=req.frontend_user_kind if req.user_type == "frontend" else FrontendUserKind.EXTERNAL.value,
        allowed_menus=req.allowed_menus,
    )
    user.credits = round(user.credits, 2)
    db.add(user)
    await db.flush()
    await log_operation(db, admin.id, admin.username, f"创建用户 {username}", "POST", "/admin/users", ip=None)
    return user


@router.put("/users/{user_id}/menus")
async def update_user_menus(
    user_id: str,
    req: UpdateMenusRequest,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(select(User).where(User.id == user_id).limit(1))
    user = result.scalar_one_or_none()
    if not user:
        raise HTTPException(status_code=404, detail="用户不存在")
    user.allowed_menus = req.allowed_menus
    await db.flush()
    await log_operation(db, admin.id, admin.username, f"更新菜单权限", "PUT", f"/admin/users/{user_id}/menus")
    return {"message": "ok"}


@router.get("/users/{user_id}", response_model=AdminUserOut)
async def get_user(
    user_id: str,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(select(User).where(User.id == user_id).limit(1))
    user = result.scalar_one_or_none()
    if not user:
        raise HTTPException(status_code=404, detail="用户不存在")
    user.credits = round(user.credits, 2)
    return user


@router.post("/users/{user_id}/credits")
async def adjust_credits(
    user_id: str,
    req: CreditAdjustRequest,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    if req.amount > 0:
        await add_credits(db, user_id, req.amount, f"管理员调整: {req.description}", record_meta=build_admin_adjust_meta())
    else:
        await deduct_credits(db, user_id, abs(req.amount), f"管理员调整: {req.description}", record_meta=build_admin_adjust_meta())
    await create_notification(
        db, user_id, "积分变动通知",
        f"您的积分已{'增加' if req.amount > 0 else '扣除'}{abs(req.amount)}积分。原因：{req.description}",
        "credit",
    )
    await log_operation(db, admin.id, admin.username, f"调整积分 {'+' if req.amount > 0 else ''}{req.amount}", "POST", f"/admin/users/{user_id}/credits")
    return {"message": "ok"}


@router.put("/users/{user_id}/status")
async def update_user_status(
    user_id: str,
    body: dict,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    is_active = body.get("is_active", True)
    await db.execute(
        update(User).where(User.id == user_id).values(is_active=is_active)
    )
    await db.flush()
    await log_operation(db, admin.id, admin.username, f"{'启用' if is_active else '禁用'}用户", "PUT", f"/admin/users/{user_id}/status")
    return {"message": "ok"}


@router.put("/users/{user_id}/frontend-kind", response_model=AdminUserOut)
async def update_user_frontend_kind(
    user_id: str,
    req: UpdateFrontendUserKindRequest,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(select(User).where(User.id == user_id).limit(1))
    user = result.scalar_one_or_none()
    if not user:
        raise HTTPException(status_code=404, detail="用户不存在")
    if user.user_type != UserType.FRONTEND.value:
        raise HTTPException(status_code=400, detail="仅前台用户支持设置内部/外部归类")
    user.frontend_user_kind = req.frontend_user_kind or FrontendUserKind.EXTERNAL.value
    await db.flush()
    await log_operation(db, admin.id, admin.username, f"设置前台用户归类为 {user.frontend_user_kind}", "PUT", f"/admin/users/{user_id}/frontend-kind")
    return user


@router.put("/users/{user_id}/reset-password")
async def reset_user_password(
    user_id: str,
    req: ResetPasswordRequest,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(select(User).where(User.id == user_id).limit(1))
    user = result.scalar_one_or_none()
    if not user:
        raise HTTPException(status_code=404, detail="用户不存在")
    user.hashed_password = hash_password(req.new_password)
    await db.flush()
    await log_operation(db, admin.id, admin.username, f"重置密码", "PUT", f"/admin/users/{user_id}/reset-password")
    return {"message": "ok"}


@router.post("/change-password")
async def admin_change_password(
    body: dict,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    old_password = body.get("old_password", "")
    new_password = body.get("new_password", "")
    if not verify_password(old_password, admin.hashed_password):
        raise HTTPException(status_code=400, detail="原密码错误")
    if len(new_password) < 6:
        raise HTTPException(status_code=400, detail="密码至少6位")
    admin.hashed_password = hash_password(new_password)
    await db.flush()
    await log_operation(db, admin.id, admin.username, "修改密码", "POST", "/admin/change-password")
    return {"message": "密码修改成功"}


# ── Credit Records ───────────────────────────────────────

@router.get("/credit-records")
async def list_credit_records(
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=1000),
    user_id: str | None = Query(None),
    user_name: str | None = Query(None),
    user_type: str | None = Query(None),
    frontend_user_kind: str | None = Query(None),
    record_type: str | None = Query(None),
    type: str | None = Query(None),
    credit_subject: str | None = Query(None),
    media_type: str | None = Query(None),
    charge_kind: str | None = Query(None),
    source_module: str | None = Query(None),
    source_step_code: str | None = Query(None),
    billing_scene: str | None = Query(None),
    start_date: str = Query(None),
    end_date: str = Query(None),
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """List all credit transaction records with filters and full summary."""
    return await list_admin_credit_records(
        db,
        page=page,
        page_size=page_size,
        user_id=user_id,
        user_name=user_name,
        user_type=user_type,
        frontend_user_kind=frontend_user_kind,
        record_type=record_type or type,
        credit_subject=credit_subject,
        media_type=media_type,
        charge_kind=charge_kind,
        source_module=source_module,
        source_step_code=source_step_code,
        billing_scene=billing_scene,
        start_date=start_date,
        end_date=end_date,
    )


# ── Notification Admin ───────────────────────────────────

@router.get("/notifications")
async def list_admin_notifications(
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=500),
    user_id: str | None = Query(None),
    is_read: bool = Query(None),
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """List all notifications with optional filters."""
    query = select(Notification).order_by(Notification.created_at.desc())
    count_query = select(func.count(Notification.id))

    if user_id:
        query = query.where(Notification.user_id == user_id)
        count_query = count_query.where(Notification.user_id == user_id)
    
    if is_read is not None:
        query = query.where(Notification.is_read == is_read)
        count_query = count_query.where(Notification.is_read == is_read)

    total = (await db.execute(count_query)).scalar() or 0
    result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
    items = result.scalars().all()

    return {
        "total": total,
        "items": [
            {
                "id": item.id,
                "user_id": item.user_id,
                "title": item.title,
                "content": item.content,
                "type": item.type,
                "is_read": item.is_read,
                "related_id": item.related_id,
                "created_at": _iso(item.created_at),
            }
            for item in items
        ],
    }


@router.post("/notifications")
async def create_admin_notification(
    body: dict,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Create notification. target_user_id=None broadcasts to all users."""
    title = body.get("title", "")
    content = body.get("content", "")
    notif_type = body.get("type", "system")
    target_user_id = body.get("target_user_id")

    if not title or not content:
        raise HTTPException(status_code=400, detail="标题和内容不能为空")

    if target_user_id:
        # Send to specific user
        await create_notification(
            db, target_user_id, title, content, notif_type, push_ws=True
        )
    else:
        # Broadcast: create one notification with user_id=NULL
        await create_notification(
            db, None, title, content, notif_type, push_ws=True
        )

    return {"message": "ok"}


@router.delete("/notifications/{notification_id}")
async def delete_admin_notification(
    notification_id: str,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(Notification).where(Notification.id == notification_id).limit(1)
    )
    notif = result.scalar_one_or_none()
    if not notif:
        raise HTTPException(status_code=404, detail="通知不存在")
    await db.execute(
        delete(NotificationRead).where(NotificationRead.notification_id == notification_id)
    )
    await db.delete(notif)
    await db.flush()
    return {"message": "ok"}


@router.get("/notifications/{notification_id}/read-users")
async def get_notification_read_users(
    notification_id: str,
    page: int = Query(1, ge=1),
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Get list of users who have read a specific notification."""
    from app.services.notification import get_notification_read_users
    items, total = await get_notification_read_users(db, notification_id, page)
    return {"total": total, "items": items}


# ── Payment Config ───────────────────────────────────────

@router.get("/payment-configs")
async def list_payment_configs(
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Return system configs that start with payment_ prefix."""
    result = await db.execute(
        select(SystemConfig).where(SystemConfig.key.like("payment_%"))
    )
    configs = result.scalars().all()
    return [
        {
            "id": c.id,
            "key": c.key,
            "value": c.value,
            "description": c.description,
        }
        for c in configs
    ]


@router.put("/payment-configs/batch")
async def batch_update_payment_configs(
    req: dict[str, str],
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Batch upsert payment configs. Creates missing keys, updates existing ones."""
    from app.utils.id_gen import generate_id

    for key, value in req.items():
        if not key.startswith("payment_"):
            continue
        result = await db.execute(
            select(SystemConfig).where(SystemConfig.key == key).limit(1)
        )
        config = result.scalar_one_or_none()
        if config:
            config.value = value
        else:
            db.add(SystemConfig(
                id=generate_id(),
                key=key,
                value=value,
            ))
    await db.flush()
    return {"ok": True}


@router.get("/payment-stats")
async def get_payment_stats(
    payment_method: str | None = Query(None),
    status: str | None = Query(None),
    start_date: str | None = Query(None),
    end_date: str | None = Query(None),
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Return payment statistics for admin dashboard with filters."""
    from sqlalchemy import func

    # Ensure by_status has all expected statuses with defaults
    by_status = {
        "pending": {"count": 0, "amount": 0.0},
        "paid": {"count": 0, "amount": 0.0},
        "cancelled": {"count": 0, "amount": 0.0},
        "refunded": {"count": 0, "amount": 0.0},
    }

    # Parse dates and build base query filters
    now_cst = datetime.now(CST)
    today_start = now_cst.replace(hour=0, minute=0, second=0, microsecond=0)
    today_end = today_start + timedelta(days=1)
    
    # Default to today if no date range provided
    query_start = today_start
    query_end = today_end
    
    if start_date:
        query_start = datetime.fromisoformat(start_date).replace(tzinfo=CST)
    if end_date:
        query_end = (datetime.fromisoformat(end_date) + timedelta(days=1)).replace(tzinfo=CST)

    # Build filter list for status breakdown
    breakdown_filters = []
    if payment_method:
        breakdown_filters.append(PaymentOrder.payment_method == payment_method)
    if status:
        breakdown_filters.append(PaymentOrder.status == status)
    # Always apply date range to breakdown
    breakdown_filters.append(PaymentOrder.created_at >= query_start)
    breakdown_filters.append(PaymentOrder.created_at < query_end)

    # Status breakdown
    status_result = await db.execute(
        select(
            PaymentOrder.status,
            func.count().label("count"),
            func.coalesce(func.sum(PaymentOrder.amount), 0).label("amount"),
        )
        .where(*breakdown_filters)
        .group_by(PaymentOrder.status)
    )
    for row in status_result.all():
        if row.status in by_status:
            by_status[row.status] = {
                "count": row.count,
                "amount": round(float(row.amount), 2)
            }
        else:
            # Map any unexpected status to cancelled
            by_status["cancelled"]["count"] += row.count
            by_status["cancelled"]["amount"] += round(float(row.amount), 2)

    # Today's stats (CST time zone) - independent of filter
    today_result = await db.execute(
        select(
            func.count().label("paid_count"),
            func.coalesce(func.sum(PaymentOrder.amount), 0).label("paid_amount"),
        ).where(
            PaymentOrder.status == "paid",
            PaymentOrder.paid_at >= today_start,
            PaymentOrder.paid_at < today_end,
        )
    )
    today_row = today_result.one()

    # Monthly cumulative stats
    month_start = now_cst.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
    month_end = (month_start + timedelta(days=32)).replace(day=1, hour=0, minute=0, second=0, microsecond=0)
    
    month_result = await db.execute(
        select(
            func.count().label("paid_count"),
            func.coalesce(func.sum(PaymentOrder.amount), 0).label("paid_amount"),
        ).where(
            PaymentOrder.status == "paid",
            PaymentOrder.paid_at >= month_start,
            PaymentOrder.paid_at < month_end,
        )
    )
    month_row = month_result.one()

    return {
        "by_status": by_status,
        "today": {
            "paid_count": today_row.paid_count,
            "paid_amount": round(float(today_row.paid_amount), 2),
        },
        "month": {
            "paid_count": month_row.paid_count,
            "paid_amount": round(float(month_row.paid_amount), 2),
        },
    }


@router.get("/payment-orders")
async def list_payment_orders(
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=500),
    payment_method: str | None = Query(None),
    status: str | None = Query(None),
    start_date: str | None = Query(None),
    end_date: str | None = Query(None),
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Return paginated payment orders for admin dashboard."""
    query = select(PaymentOrder, User.username).join(User, PaymentOrder.user_id == User.id)
    count_query = select(func.count(PaymentOrder.id))

    filters = []
    if payment_method:
        filters.append(PaymentOrder.payment_method == payment_method)
    if status:
        filters.append(PaymentOrder.status == status)
    if start_date:
        filters.append(PaymentOrder.created_at >= datetime.fromisoformat(start_date).replace(tzinfo=CST))
    if end_date:
        filters.append(PaymentOrder.created_at < (datetime.fromisoformat(end_date) + timedelta(days=1)).replace(tzinfo=CST))

    for f in filters:
        query = query.where(f)
        count_query = count_query.where(f)

    total = (await db.execute(count_query)).scalar() or 0
    result = await db.execute(
        query.order_by(PaymentOrder.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
    )
    rows = result.all()

    items = [
        {
            "id": o.id,
            "orderNo": o.order_no,
            "order_no": o.order_no,
            "userId": o.user_id,
            "user_id": o.user_id,
            "username": username,
            "amount": round(float(o.amount), 2),
            "credits": round(float(o.credits), 2),
            "paymentMethod": o.payment_method,
            "payment_method": o.payment_method,
            "status": o.status,
            "tradeNo": o.trade_no,
            "trade_no": o.trade_no,
            "paidAt": o.paid_at.isoformat() if o.paid_at else None,
            "paid_at": o.paid_at.isoformat() if o.paid_at else None,
            "createdAt": o.created_at.isoformat() if o.created_at else None,
            "created_at": o.created_at.isoformat() if o.created_at else None,
        }
        for o, username in rows
    ]

    return {"items": items, "total": total, "page": page, "page_size": page_size}

@router.put("/payment-configs/{config_id}")
async def update_payment_config(
    config_id: str,
    req: SystemConfigUpdate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Update a payment system config value."""
    result = await db.execute(
        select(SystemConfig).where(
            SystemConfig.id == config_id,
            SystemConfig.key.like("payment_%"),
        )
        .limit(1)
    )
    config = result.scalar_one_or_none()
    if not config:
        raise HTTPException(status_code=404, detail="支付配置不存在")
    config.value = req.value
    await db.flush()
    return {
        "id": config.id,
        "key": config.key,
        "value": config.value,
        "description": config.description,
    }


@router.post("/payment-orders/{order_no}/refund")
async def refund_payment_order(
    order_no: str,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Refund a paid payment order."""
    result = await process_refund(db, order_no)
    if not result.get("success"):
        raise HTTPException(status_code=400, detail=result.get("message", "退款失败"))
    return result


# ── Industry Config ──────────────────────────────────────

def _serialize_industry(ind: IndustryConfig) -> dict:
    """Convert industry config to dict with parsed skills."""
    skills = []
    if ind.skills:
        try:
            raw = json.loads(ind.skills)
            if isinstance(raw, list):
                skills = raw if raw and isinstance(raw[0], dict) else [{"key": s, "label": s} for s in raw]
        except (json.JSONDecodeError, TypeError):
            skills = []
    return {
        "id": ind.id,
        "key": ind.key,
        "label": ind.label,
        "icon": ind.icon or "",
        "description": ind.description,
        "skills": skills,
        "is_active": ind.is_active,
        "sort_order": ind.sort_order,
        "created_at": ind.created_at,
    }


@router.get("/industry-configs")
async def list_industry_configs(
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(IndustryConfig).order_by(IndustryConfig.sort_order.asc())
    )
    return [_serialize_industry(ind) for ind in result.scalars().all()]


@router.post("/industry-configs")
async def create_industry_config(
    req: IndustryConfigCreate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    config = IndustryConfig(
        id=generate_id(),
        key=req.key,
        label=req.label,
        icon=req.icon or "",
        description=req.description,
        skills=json.dumps(req.skills, ensure_ascii=False),
        is_active=req.is_active,
        sort_order=req.sort_order,
    )
    db.add(config)
    await db.flush()
    return _serialize_industry(config)


@router.put("/industry-configs/{config_id}")
async def update_industry_config(
    config_id: str,
    req: IndustryConfigCreate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(IndustryConfig).where(IndustryConfig.id == config_id).limit(1)
    )
    config = result.scalar_one_or_none()
    if not config:
        raise HTTPException(status_code=404, detail="行业配置不存在")
    config.key = req.key
    config.label = req.label
    config.icon = req.icon or ""
    config.description = req.description
    config.skills = json.dumps(req.skills, ensure_ascii=False)
    config.is_active = req.is_active
    config.sort_order = req.sort_order
    await db.flush()
    return _serialize_industry(config)


@router.delete("/industry-configs/{config_id}")
async def delete_industry_config(
    config_id: str,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(IndustryConfig).where(IndustryConfig.id == config_id).limit(1)
    )
    config = result.scalar_one_or_none()
    if not config:
        raise HTTPException(status_code=404, detail="行业配置不存在")
    await db.delete(config)
    await db.flush()
    return {"message": "ok"}


# ── Video Engine ─────────────────────────────────────────

@router.get("/video-engines", response_model=list[VideoEngineOut])
async def list_video_engines(
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(VideoEngine).order_by(VideoEngine.priority.desc())
    )
    return result.scalars().all()


@router.post("/video-engines", response_model=VideoEngineOut)
async def create_video_engine(
    req: VideoEngineCreate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    engine = VideoEngine(id=generate_id(), **req.model_dump())
    db.add(engine)
    await db.flush()
    return engine


@router.put("/video-engines/{engine_id}", response_model=VideoEngineOut)
async def update_video_engine(
    engine_id: str,
    req: VideoEngineCreate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(VideoEngine).where(VideoEngine.id == engine_id).limit(1)
    )
    engine = result.scalar_one_or_none()
    if not engine:
        raise HTTPException(status_code=404, detail="视频引擎不存在")
    for k, v in req.model_dump().items():
        setattr(engine, k, v)
    await db.flush()
    return engine


@router.delete("/video-engines/{engine_id}")
async def delete_video_engine(
    engine_id: str,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(VideoEngine).where(VideoEngine.id == engine_id).limit(1)
    )
    engine = result.scalar_one_or_none()
    if not engine:
        raise HTTPException(status_code=404, detail="视频引擎不存在")
    await db.delete(engine)
    await db.flush()
    return {"message": "ok"}


# ── Image Engine ──────────────────────────────────────────

@router.get("/image-engines", response_model=list[ImageEngineOut])
async def list_image_engines(
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(ImageEngine).order_by(ImageEngine.priority.desc())
    )
    return result.scalars().all()


@router.post("/image-engines", response_model=ImageEngineOut)
async def create_image_engine(
    req: ImageEngineCreate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    engine = ImageEngine(id=generate_id(), **req.model_dump())
    db.add(engine)
    await db.flush()
    return engine


@router.put("/image-engines/{engine_id}", response_model=ImageEngineOut)
async def update_image_engine(
    engine_id: str,
    req: ImageEngineCreate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(ImageEngine).where(ImageEngine.id == engine_id).limit(1)
    )
    engine = result.scalar_one_or_none()
    if not engine:
        raise HTTPException(status_code=404, detail="图片引擎不存在")
    for k, v in req.model_dump().items():
        setattr(engine, k, v)
    await db.flush()
    return engine


@router.delete("/image-engines/{engine_id}")
async def delete_image_engine(
    engine_id: str,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(ImageEngine).where(ImageEngine.id == engine_id).limit(1)
    )
    engine = result.scalar_one_or_none()
    if not engine:
        raise HTTPException(status_code=404, detail="图片引擎不存在")
    await db.delete(engine)
    await db.flush()
    return {"message": "ok"}




async def _validate_credit_ratio_engine(db: AsyncSession, req: CreditRatioCreate) -> None:
    """校验积分规则绑定的引擎是否存在。

    CreditRatio.model_config_id 为兼容旧字段名，当前实际保存引擎ID：
    - gen_type=image 时对应 image_engines.id
    - gen_type=video 时对应 video_engines.id
    """
    gen_type = (req.gen_type or "").lower().strip()
    engine_id = (req.model_config_id or "").strip()
    if gen_type not in ("image", "video"):
        raise HTTPException(status_code=400, detail="gen_type 仅支持 image 或 video")
    if not engine_id:
        raise HTTPException(status_code=400, detail="model_config_id 不能为空，当前字段用于保存图片/视频引擎ID")

    model = ImageEngine if gen_type == "image" else VideoEngine
    result = await db.execute(select(model).where(model.id == engine_id).limit(1))
    engine = result.scalar_one_or_none()
    if not engine:
        detail = "图片积分规则绑定的图片引擎不存在" if gen_type == "image" else "视频积分规则绑定的视频引擎不存在"
        raise HTTPException(status_code=400, detail=detail)


# ── Credit Ratio ─────────────────────────────────────────

@router.get("/credit-ratios", response_model=list[CreditRatioOut])
async def list_credit_ratios(
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(select(CreditRatio))
    return result.scalars().all()


@router.post("/credit-ratios", response_model=CreditRatioOut)
async def create_credit_ratio(
    req: CreditRatioCreate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    await _validate_credit_ratio_engine(db, req)
    data = req.model_dump()
    data["gen_type"] = data["gen_type"].lower().strip()
    data["model_config_id"] = data["model_config_id"].strip()
    ratio = CreditRatio(id=generate_id(), **data)
    db.add(ratio)
    await db.flush()
    return ratio


@router.put("/credit-ratios/{ratio_id}", response_model=CreditRatioOut)
async def update_credit_ratio(
    ratio_id: str,
    req: CreditRatioCreate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(CreditRatio).where(CreditRatio.id == ratio_id).limit(1)
    )
    ratio = result.scalar_one_or_none()
    if not ratio:
        raise HTTPException(status_code=404, detail="积分比例不存在")
    await _validate_credit_ratio_engine(db, req)
    data = req.model_dump()
    data["gen_type"] = data["gen_type"].lower().strip()
    data["model_config_id"] = data["model_config_id"].strip()
    for k, v in data.items():
        setattr(ratio, k, v)
    await db.flush()
    return ratio


@router.delete("/credit-ratios/{ratio_id}")
async def delete_credit_ratio(
    ratio_id: str,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(CreditRatio).where(CreditRatio.id == ratio_id).limit(1)
    )
    ratio = result.scalar_one_or_none()
    if not ratio:
        raise HTTPException(status_code=404, detail="积分比例不存在")
    await db.delete(ratio)
    await db.flush()
    return {"message": "ok"}


@router.get("/credit-ratios/grouped", response_model=dict)
async def list_credit_ratios_grouped(
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(select(CreditRatio))
    ratios = result.scalars().all()

    grouped = {}
    for ratio in ratios:
        if ratio.gen_type not in grouped:
            grouped[ratio.gen_type] = []
        grouped[ratio.gen_type].append(CreditRatioOut.model_validate(ratio))

    return grouped


# ── Model Config ─────────────────────────────────────────

@router.get("/model-configs", response_model=list[ModelConfigOut])
async def list_model_configs(
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(select(ModelConfig).order_by(ModelConfig.priority.desc()))
    return result.scalars().all()


@router.post("/model-configs", response_model=ModelConfigOut)
async def create_model_config(
    req: ModelConfigCreate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    config = ModelConfig(id=generate_id(), **req.model_dump())
    db.add(config)
    await db.flush()
    return config


@router.put("/model-configs/{config_id}", response_model=ModelConfigOut)
async def update_model_config(
    config_id: str,
    req: ModelConfigCreate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(select(ModelConfig).where(ModelConfig.id == config_id).limit(1))
    config = result.scalar_one_or_none()
    if not config:
        raise HTTPException(status_code=404, detail="配置不存在")
    for k, v in req.model_dump().items():
        setattr(config, k, v)
    await db.flush()
    return config


@router.delete("/model-configs/{config_id}")
async def delete_model_config(
    config_id: str,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(select(ModelConfig).where(ModelConfig.id == config_id).limit(1))
    config = result.scalar_one_or_none()
    if not config:
        raise HTTPException(status_code=404, detail="配置不存在")
    await db.delete(config)
    await db.flush()
    return {"message": "ok"}


# ── System Config ────────────────────────────────────────

@router.get("/system-configs", response_model=list[SystemConfigOut])
async def list_system_configs(
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(select(SystemConfig))
    return result.scalars().all()


@router.put("/system-configs/{config_id}", response_model=SystemConfigOut)
async def update_system_config(
    config_id: str,
    req: SystemConfigUpdate,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(select(SystemConfig).where(SystemConfig.id == config_id).limit(1))
    config = result.scalar_one_or_none()
    if not config:
        raise HTTPException(status_code=404, detail="配置不存在")
    config.value = str(req.value)
    await db.commit()
    return config


# ── Operation Logs ──────────────────────────────────────

@router.get("/operation-logs")
async def list_operation_logs(
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=500),
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    query = select(OperationLog).order_by(OperationLog.created_at.desc())
    count_query = select(func.count(OperationLog.id))
    total = (await db.execute(count_query)).scalar() or 0
    result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
    items = result.scalars().all()
    return {
        "total": total,
        "items": [
            {
                "id": item.id,
                "user_id": item.user_id,
                "username": item.username,
                "action": item.action,
                "method": item.method,
                "path": item.path,
                "detail": item.detail,
                "ip": item.ip,
                "created_at": _iso(item.created_at),
            }
            for item in items
        ],
    }


# ── Stats ────────────────────────────────────────────────

@router.get("/stats", response_model=AdminStatsOut)
async def get_stats(
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
    start_date: str = Query(None),
    end_date: str = Query(None),
):
    today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
    
    try:
        if start_date:
            date_start = datetime.strptime(start_date, "%Y-%m-%d")
        else:
            date_start = today_start
        if end_date:
            date_end = datetime.strptime(end_date, "%Y-%m-%d")
            date_end = date_end.replace(hour=23, minute=59, second=59, microsecond=999999)
        else:
            date_end = datetime.now()
    except:
        date_start = today_start
        date_end = datetime.now()

    total_users = (await db.execute(
        select(func.count(User.id)).where(
            User.user_type == "frontend",
            User.created_at >= date_start,
            User.created_at <= date_end,
        )
    )).scalar() or 0

    total_projects = (await db.execute(
        select(func.count(Project.id)).where(
            Project.deleted_at.is_(None),
            Project.created_at >= date_start,
            Project.created_at <= date_end,
        )
    )).scalar() or 0
    
    total_generations = (await db.execute(
        select(func.count(ChatGenerationTask.id)).where(
            ChatGenerationTask.created_at >= date_start,
            ChatGenerationTask.created_at <= date_end,
        )
    )).scalar() or 0
    
    total_records = (await db.execute(
        select(func.count(GenerationRecord.id)).where(
            GenerationRecord.deleted_at.is_(None),
            GenerationRecord.created_at >= date_start,
            GenerationRecord.created_at <= date_end,
        )
    )).scalar() or 0
    
    total_revenue = (await db.execute(
        select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
            PaymentOrder.status == "paid",
            PaymentOrder.created_at >= date_start,
            PaymentOrder.created_at <= date_end,
        )
    )).scalar() or 0

    credits_consumed = (await db.execute(
        select(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0)).where(
            CreditRecord.type == "consume",
            CreditRecord.created_at >= date_start,
            CreditRecord.created_at <= date_end,
        )
    )).scalar() or 0

    alipay_revenue = (await db.execute(
        select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
            PaymentOrder.status == "paid",
            PaymentOrder.payment_method == "alipay",
            PaymentOrder.created_at >= date_start,
            PaymentOrder.created_at <= date_end,
        )
    )).scalar() or 0

    wechat_revenue = (await db.execute(
        select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
            PaymentOrder.status == "paid",
            PaymentOrder.payment_method == "wechat",
            PaymentOrder.created_at >= date_start,
            PaymentOrder.created_at <= date_end,
        )
    )).scalar() or 0

    period_duration = date_end - date_start
    
    last_period_start = date_start - period_duration
    last_period_end = date_start

    last_period_users = (await db.execute(
        select(func.count(User.id)).where(
            User.user_type == "frontend",
            User.created_at >= last_period_start,
            User.created_at <= last_period_end,
        )
    )).scalar() or 0

    last_period_projects = (await db.execute(
        select(func.count(Project.id)).where(
            Project.deleted_at.is_(None),
            Project.created_at >= last_period_start,
            Project.created_at <= last_period_end,
        )
    )).scalar() or 0

    last_period_generations = (await db.execute(
        select(func.count(ChatGenerationTask.id)).where(
            ChatGenerationTask.created_at >= last_period_start,
            ChatGenerationTask.created_at <= last_period_end,
        )
    )).scalar() or 0

    last_period_records = (await db.execute(
        select(func.count(GenerationRecord.id)).where(
            GenerationRecord.deleted_at.is_(None),
            GenerationRecord.created_at >= last_period_start,
            GenerationRecord.created_at <= last_period_end,
        )
    )).scalar() or 0

    last_period_revenue = (await db.execute(
        select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
            PaymentOrder.status == "paid",
            PaymentOrder.created_at >= last_period_start,
            PaymentOrder.created_at <= last_period_end,
        )
    )).scalar() or 0

    last_period_credits_consumed = (await db.execute(
        select(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0)).where(
            CreditRecord.type == "consume",
            CreditRecord.created_at >= last_period_start,
            CreditRecord.created_at <= last_period_end,
        )
    )).scalar() or 0

    return AdminStatsOut(
        total_users=total_users,
        total_projects=total_projects,
        total_generations=total_generations,
        total_records=total_records,
        total_revenue=float(total_revenue),
        credits_consumed_today=float(credits_consumed),
        today_alipay_revenue=float(alipay_revenue),
        today_wechat_revenue=float(wechat_revenue),
        last_period_users=last_period_users,
        last_period_projects=last_period_projects,
        last_period_generations=last_period_generations,
        last_period_records=last_period_records,
        last_period_revenue=float(last_period_revenue),
        last_period_credits_consumed=float(last_period_credits_consumed),
    )


# ── Token Usage ─────────────────────────────────────────

@router.get("/token-usage")
async def list_token_usage(
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=500),
    user_id: str | None = Query(None),
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """List token usage records with optional user filter."""
    query = select(TokenUsage).order_by(TokenUsage.created_at.desc())
    if user_id:
        query = query.where(TokenUsage.user_id == user_id)

    count_query = select(func.count(TokenUsage.id))
    if user_id:
        count_query = count_query.where(TokenUsage.user_id == user_id)

    total = (await db.execute(count_query)).scalar() or 0
    result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
    items = result.scalars().all()

    return {
        "total": total,
        "items": [
            {
                "id": item.id,
                "model_config_id": item.model_config_id,
                "user_id": item.user_id,
                "input_tokens": item.input_tokens,
                "output_tokens": item.output_tokens,
                "total_tokens": item.total_tokens,
                "created_at": _iso(item.created_at),
            }
            for item in items
        ],
    }


# ── Generation Records (Admin) ─────────────────────────────

@router.get("/generation-records")
async def admin_list_generation_records(
    user_id: str | None = Query(None),
    status: str | None = Query(None),
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=500),
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """List all generation records across all users, with optional filters."""
    query = (
        select(GenerationRecord, User.username, Project.name)
        .join(User, GenerationRecord.user_id == User.id)
        .join(Project, GenerationRecord.project_id == Project.id)
        .where(GenerationRecord.deleted_at.is_(None), Project.deleted_at.is_(None))
        .order_by(GenerationRecord.created_at.desc())
    )
    if user_id:
        query = query.where(GenerationRecord.user_id == user_id)
    if status:
        query = query.where(GenerationRecord.status == status)

    # Count total
    count_query = select(func.count(GenerationRecord.id)).where(GenerationRecord.deleted_at.is_(None))
    if user_id:
        count_query = count_query.where(GenerationRecord.user_id == user_id)
    if status:
        count_query = count_query.where(GenerationRecord.status == status)
    total_result = await db.execute(count_query)
    total = total_result.scalar() or 0

    # Paginate
    offset = (page - 1) * page_size
    query = query.offset(offset).limit(page_size)
    result = await db.execute(query)
    rows = result.all()

    items = []
    for record, username, project_name in rows:
        refs = None
        if record.media_references:
            try:
                refs = json.loads(record.media_references)
            except (json.JSONDecodeError, TypeError):
                refs = None
        items.append({
            "id": record.id,
            "user_id": record.user_id,
            "username": username,
            "project_id": record.project_id,
            "project_name": project_name,
            "original_prompt": record.original_prompt,
            "optimized_prompt": record.optimized_prompt,
            "duration": record.duration,
            "aspect_ratio": record.aspect_ratio,
            "resolution": record.resolution,
            "status": record.status,
            "video_url": build_resource_signed_url(record.video_url) if record.video_url else '',
            "video_cover_url": build_resource_signed_url(record.video_cover_url) if record.video_cover_url else '',
            "references": refs,
            "credits_cost": record.credits_cost or 0,
            "text_credits_cost": record.text_credits_cost or 0,
            "text_tokens_used": record.text_tokens_used or 0,
            "video_tokens_used": record.video_tokens_used or 0,
            "error_message": record.error_message,
            "created_at": _iso(record.created_at),
            "generated_at": _iso(record.generated_at),

            # append img param
            "gen_type": record.gen_type,
            "image_size": record.image_size or '',
            "image_url": build_resource_signed_url(record.image_url) if record.image_url else '',
            "image_tokens_used": record.image_tokens_used or 0,
            "image_proportion": record.image_proportion or '',
            "image_px": record.image_px or '',
        })

    return {"total": total, "items": items}


@router.put("/generation-records/{record_id}/status")
async def admin_update_generation_status(
    record_id: str,
    body: dict,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Admin update generation record status (e.g., confirm/reject)."""
    result = await db.execute(
        select(GenerationRecord).where(
            GenerationRecord.id == record_id,
            GenerationRecord.deleted_at.is_(None),
        )
        .with_for_update()
        .limit(1)
    )
    record = result.scalar_one_or_none()
    if not record:
        raise HTTPException(status_code=404, detail="记录不存在")

    new_status = body.get("status")
    if new_status not in ("prompt_optimized", "generating", "completed", "failed"):
        raise HTTPException(status_code=400, detail="无效状态")

    if new_status == "failed":
        await mark_generation_record_failed_and_refund_once(
            db,
            record=record,
            error_message=body.get("error_message") or record.error_message or "管理员设置为失败",
        )
    else:
        record.status = new_status

    if body.get("video_url"):
        record.video_url = body["video_url"]
    if body.get("video_cover_url"):
        record.video_cover_url = body["video_cover_url"]
    if body.get("image_url"):
        record.image_url = body["image_url"]
    if new_status == "completed":
        record.generated_at = datetime.now()
    await db.flush()
    return {"message": "ok"}


@router.post("/generation-records/{record_id}/generate")
async def admin_generate_video(
    record_id: str,
    body: dict,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Admin trigger video/image generation for a record with specified params."""
    from app.models.project import Project
    from app.services.video_queue import task_queue

    result = await db.execute(
        select(GenerationRecord, Project.name)
        .join(Project, GenerationRecord.project_id == Project.id)
        .where(
            GenerationRecord.id == record_id,
            GenerationRecord.deleted_at.is_(None),
            Project.deleted_at.is_(None),
        )
        .with_for_update()
    )
    row = result.first()
    if not row:
        raise HTTPException(status_code=404, detail="记录不存在")

    record, project_name = row
    type_str = "视频" if record.gen_type == GenerationType.video else "图片"

    if record.status not in ("prompt_optimized", "failed"):
        raise HTTPException(status_code=400, detail=f"当前状态不允许生成{type_str}")

    attempt_no = await get_next_credit_attempt_no(
        db,
        owner_type=OWNER_GENERATION_RECORD,
        owner_id=record.id,
    )

    if record.gen_type == GenerationType.video:
        # Video Generation
        aspect_ratio = body.get("aspect_ratio", "16:9")
        resolution = body.get("resolution", "720p")
        if aspect_ratio not in ASPECT_RATIOS:
            raise HTTPException(status_code=400, detail="不支持的画面比例")
        if resolution not in RESOLUTIONS:
            raise HTTPException(status_code=400, detail="不支持的分辨率")

        duration = record.duration or 5
        media_billing = await charge_generation_media_by_params(
            db,
            user_id=record.user_id,
            record_id=record.id,
            gen_type="video",
            duration=duration,
            resolution=resolution,
            project_name=project_name,
            description_prefix="视频生成(管理后台)",
            owner_type=OWNER_GENERATION_RECORD,
            attempt_no=attempt_no,
        )

        record.aspect_ratio = aspect_ratio
        record.resolution = resolution
        record.credits_cost = round(float(record.credits_cost or 0) + media_billing.total_charged, 2)
        record.status = "generating"
        record.error_message = None
        record.video_url = None
        record.video_cover_url = None
        record.image_url = None
        record.seedance_task_id = None
        await db.flush()

        try:
            from app.services.video_gen import get_active_engine, submit_video_task
            engine = await get_active_engine(db)
            task_id = await submit_video_task(
                db,
                engine,
                record,
                include_media_references=False,
            )
            record.seedance_task_id = task_id
            await db.flush()
            await task_queue.enqueue(record_id)
        except Exception as e:
            await mark_generation_record_failed_and_refund_once(
                db,
                record=record,
                error_message=str(e),
            )
            await db.flush()

    elif record.gen_type == GenerationType.image:
        # Image generation

        post_image_size = body.get("image_size", "")
        image_size = post_image_size or record.image_size or "2K"
        media_billing = await charge_generation_media_by_params(
            db,
            user_id=record.user_id,
            record_id=record.id,
            gen_type="image",
            image_size=image_size,
            project_name=project_name,
            description_prefix="图片生成(管理后台)",
            owner_type=OWNER_GENERATION_RECORD,
            attempt_no=attempt_no,
        )

        record.image_size = image_size
        record.credits_cost = round(float(record.credits_cost or 0) + media_billing.total_charged, 2)
        record.status = "generating"
        record.error_message = None
        record.image_url = None
        record.video_url = None
        record.video_cover_url = None
        record.seedance_task_id = None
        await db.flush()

        try:
            await task_queue.enqueue(record_id)
        except Exception as e:
            await mark_generation_record_failed_and_refund_once(
                db,
                record=record,
                error_message=str(e),
            )
            await db.flush()

    return {"message": "ok", "record_id": record_id}


# ── File Uploads ─────────────────────────────────────────

import os
from fastapi import File, UploadFile


@router.post("/upload-pdf")
async def upload_pdf(
    file: UploadFile = File(...),
    config_key: str | None = None,
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Upload a PDF file and save URL to system config."""
    from app.config import settings
    from app.utils.id_gen import generate_id

    if not file.filename:
        raise HTTPException(status_code=400, detail="请选择文件")

    if not file.filename.lower().endswith(".pdf"):
        raise HTTPException(status_code=400, detail="仅支持PDF格式")

    content = await file.read()
    if len(content) > 10 * 1024 * 1024:
        raise HTTPException(status_code=400, detail="文件大小不能超过10MB")

    safe_name = f"pdf_{generate_id()}.pdf"
    file_path = os.path.join(settings.UPLOAD_LOCAL_PATH, safe_name)
    with open(file_path, "wb") as f:
        f.write(content)

    url = f"/uploads/{safe_name}"

    if config_key:
        result = await db.execute(
            select(SystemConfig).where(SystemConfig.key == config_key).limit(1)
        )
        config = result.scalar_one_or_none()
        if config:
            config.value = url
        else:
            db.add(SystemConfig(
                id=generate_id(),
                key=config_key,
                value=url,
            ))
        await db.commit()

    return {"url": url}


@router.post("/upload-logo")
async def upload_logo(
    file: UploadFile = File(...),
    admin: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """Upload a Logo image file and save URL to system config."""
    from app.config import settings
    from app.utils.id_gen import generate_id

    if not file.filename:
        raise HTTPException(status_code=400, detail="请选择文件")

    allowed_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.webp')
    if not file.filename.lower().endswith(allowed_extensions):
        raise HTTPException(status_code=400, detail="仅支持 PNG、JPG、GIF、WebP 格式图片")

    content = await file.read()
    if len(content) > 2 * 1024 * 1024:
        raise HTTPException(status_code=400, detail="文件大小不能超过2MB")

    safe_name = "site_logo.png"
    file_path = os.path.join(settings.UPLOAD_LOCAL_PATH, safe_name)
    with open(file_path, "wb") as f:
        f.write(content)

    url = f"/uploads/{safe_name}"

    result = await db.execute(
        select(SystemConfig).where(SystemConfig.key == "site_logo").limit(1)
    )
    config = result.scalar_one_or_none()
    if config:
        config.value = url
    else:
        db.add(SystemConfig(
            id="cfg_site_logo",
            key="site_logo",
            value=url,
            description="网站Logo图片",
        ))
    await db.commit()

    return {"url": url}


# ── Payment Stats ────────────────────────────────────────


