import logging

from rest_framework import status
from rest_framework.decorators import api_view, permission_classes, parser_classes
from rest_framework.parsers import MultiPartParser, JSONParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.db import transaction
from django.db.models import Sum, Q, F, Count, Exists, OuterRef, Case, When, Value
from django.db.models.functions import TruncDate
from django.db.utils import OperationalError as DbOperationalError
from datetime import timedelta

from .models import GenerationRecord, QuotaConfig, AssetGroup, Asset
from .serializers import (
    VideoGenerateSerializer, QuotaUpdateSerializer,
    UserStatusSerializer, SystemSettingsSerializer,
    AdminCreateUserSerializer,
    TeamCreateSerializer, TeamUpdateSerializer, TeamTopUpSerializer,
    TeamAdminCreateSerializer, TeamMemberCreateSerializer, MemberQuotaSerializer,
    TeamAnomalyConfigSerializer,
)
from apps.accounts.models import Team, AdminAuditLog, log_admin_action, TeamAnomalyConfig, LoginAnomaly, ActiveSession, LoginRecord
from apps.accounts.permissions import IsSuperAdmin, IsTeamAdmin, IsTeamMember, IsSuperAdminOrObserver
from utils.tos_client import upload_file as tos_upload
from utils.airdrama_client import create_task, query_task, extract_video_url, map_status
from utils.billing import get_resolution, estimate_tokens, calculate_cost, calculate_base_cost

User = get_user_model()
logger = logging.getLogger(__name__)

# File validation constants
ALLOWED_IMAGE_EXTS = {'jpeg', 'jpg', 'png', 'webp', 'bmp', 'tiff', 'gif', 'heic', 'heif'}
ALLOWED_VIDEO_EXTS = {'mp4', 'mov'}
ALLOWED_AUDIO_EXTS = {'mp3', 'wav'}
MAX_IMAGE_SIZE = 30 * 1024 * 1024   # 30MB
MAX_VIDEO_SIZE = 50 * 1024 * 1024   # 50MB
MAX_AUDIO_SIZE = 15 * 1024 * 1024   # 15MB


def _safe_int(value, default=0):
    """Safely convert query parameter to int, returning default on failure."""
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def _has_video_reference(references):
    """判断参考素材里是否包含视频类型，用于选择单价。"""
    if not references:
        return False
    return any(ref.get('type') == 'video' for ref in references)


def _sum_video_duration(references):
    """累加所有视频类型参考素材的 duration（秒），用于 token 估算的输入时长。"""
    if not references:
        return 0.0
    total = 0.0
    for ref in references:
        if ref.get('type') == 'video':
            try:
                total += float(ref.get('duration') or 0)
            except (ValueError, TypeError):
                continue
    return total


def _format_prompt_for_ark(prompt, label_placeholders):
    """把 prompt 中的 @label 替换为火山可识别的「图片N/视频N/音频N」。

    火山 Seedance 模型只能理解"素材类型+序号"格式的指代（官方文档 FAQ Q3）；
    文件名 / asset id / URL 对它都是不可理解的字符串，会按位置概率性对齐，
    表现为"人物颠倒"。此函数在发给火山之前做一次静默替换，用户 prompt 原文
    保留在 DB 便于 reEdit 回填带缩略图的标签。

    label_placeholders: [(label, placeholder), ...] 调用方需保证按 label 长度
    降序，防止"碧"先于"碧碧"被替换的子串吞噬问题。

    用 str.replace 而非 re.sub，避免 label 含正则元字符（如 "@[test].png"）时崩溃。
    """
    result = prompt
    for label, placeholder in label_placeholders:
        if not label:
            continue
        result = result.replace(f'@{label}', placeholder)
    return result


def _get_token_price(config, model, has_video_ref, resolution):
    """根据模型、是否含视频、分辨率选择单价。

    约束（与官方文档一致）：
    - Seedance 2.0 Fast 不支持 1080p — 此组合在 UI 层已阻止、VideoGenerateSerializer
      也会在 video_generate_view 中拒绝。若仍进到这里，表示前端约束失效或绕过前端
      直接调 API，应 fail loud，绝不按 720p 价静默降级（那是欺骗用户）。
    - 1080p 仅 Seedance 2.0 使用独立单价（51/31）
    - 480p 和 720p 共享同一单价
    """
    if model == 'seedance_2.0_fast' and resolution == '1080p':
        raise ValueError(
            'Seedance 2.0 Fast 不支持 1080p — 前端应阻止此组合，不应进到计价函数'
        )
    if model == 'seedance_2.0_fast':
        return config.base_token_price_fast_video if has_video_ref else config.base_token_price_fast
    if resolution == '1080p':
        return config.base_token_price_1080p_video if has_video_ref else config.base_token_price_1080p
    return config.base_token_price_video if has_video_ref else config.base_token_price


# Columns added in migration 0003; may not exist in production DB yet.
_M0003_COLS = ('ark_task_id', 'result_url', 'error_message', 'reference_urls')
_m0003_ok = None  # None = unknown, True = columns exist, False = missing


def _eval_qs(qs, limit=None, get_kwargs=None):
    """Evaluate a GenerationRecord queryset, deferring migration-0003 columns if missing."""
    global _m0003_ok

    def _run(q, defer):
        if defer:
            q = q.defer(*_M0003_COLS)
        if get_kwargs is not None:
            return q.get(**get_kwargs)
        if limit is not None:
            return list(q[:limit])
        return list(q)

    if _m0003_ok is False:
        return _run(qs, defer=True)
    try:
        result = _run(qs, defer=False)
        _m0003_ok = True
        return result
    except DbOperationalError as e:
        if 'ark_task_id' in str(e):
            _m0003_ok = False
            return _run(qs, defer=True)
        raise


# ──────────────────────────────────────────────
# Media Upload
# ──────────────────────────────────────────────

@api_view(['POST'])
@permission_classes([IsAuthenticated])
@parser_classes([MultiPartParser])
def upload_media_view(request):
    """POST /api/v1/media/upload — Upload file to TOS, return public URL."""
    file = request.FILES.get('file')
    if not file:
        return Response({'error': '未上传文件'}, status=status.HTTP_400_BAD_REQUEST)

    ext = file.name.rsplit('.', 1)[-1].lower() if '.' in file.name else ''

    if ext in ALLOWED_IMAGE_EXTS:
        media_type = 'image'
        max_size = MAX_IMAGE_SIZE
    elif ext in ALLOWED_VIDEO_EXTS:
        media_type = 'video'
        max_size = MAX_VIDEO_SIZE
    elif ext in ALLOWED_AUDIO_EXTS:
        media_type = 'audio'
        max_size = MAX_AUDIO_SIZE
    else:
        return Response(
            {'error': f'不支持的文件格式: {ext}'},
            status=status.HTTP_400_BAD_REQUEST,
        )

    if file.size > max_size:
        limit_mb = max_size // (1024 * 1024)
        return Response(
            {'error': f'{media_type} 文件不能超过 {limit_mb}MB'},
            status=status.HTTP_400_BAD_REQUEST,
        )

    try:
        url = tos_upload(file, folder=media_type)
    except Exception as e:
        logger.exception('TOS upload failed')
        return Response(
            {'error': '文件上传失败，请稍后重试'},
            status=status.HTTP_500_INTERNAL_SERVER_ERROR,
        )

    return Response({
        'url': url,
        'type': media_type,
        'filename': file.name,
        'size': file.size,
    })


# ──────────────────────────────────────────────
# Video Generation (with 4-layer quota check)
# ──────────────────────────────────────────────

@api_view(['POST'])
@permission_classes([IsTeamMember])
def video_generate_view(request):
    """POST /api/v1/video/generate — Four-layer quota check + AirDrama API."""
    serializer = VideoGenerateSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    user = request.user
    team = user.team

    # Pre-check: team disabled
    if not team.is_active:
        return Response(
            {'error': 'team_disabled', 'message': '您的团队已被停用，请联系管理员'},
            status=status.HTTP_403_FORBIDDEN,
        )

    today = timezone.now().date()
    first_of_month = today.replace(day=1)
    duration = serializer.validated_data['duration']
    prompt = serializer.validated_data['prompt']
    mode = serializer.validated_data['mode']
    model = serializer.validated_data['model']
    aspect_ratio = serializer.validated_data['aspect_ratio']
    # serializer 已设 default='720p' + choices 约束，validated_data 必有合法值
    resolution = serializer.validated_data['resolution']
    search_mode = request.data.get('search_mode', 'off')
    seed = _safe_int(request.data.get('seed', -1), -1)

    # 1080P 仅 Seedance 2.0 支持，Fast 不支持
    if resolution == '1080p' and model == 'seedance_2.0_fast':
        return Response({
            'error': 'invalid_resolution',
            'message': '1080P 仅支持 AirDrama 模型，AirDrama Fast 不支持 1080P，请切换模型或选择 720P',
        }, status=status.HTTP_400_BAD_REQUEST)

    # ── 预估 token 和费用 ──
    config = QuotaConfig.objects.get_or_create(pk=1)[0]
    references = request.data.get('references', [])
    w, h = get_resolution(aspect_ratio, resolution)
    has_video_ref = _has_video_reference(references)
    input_video_dur = _sum_video_duration(references) if has_video_ref else 0
    estimated_tokens = estimate_tokens(w, h, duration, input_video_duration=input_video_dur)
    token_price = _get_token_price(config, model, has_video_ref, resolution)
    estimated_cost = calculate_cost(estimated_tokens, token_price, team.markup_percentage)

    # ── 所有额度检查在 transaction 内完成，select_for_update 串行化同团队请求 ──
    with transaction.atomic():
        locked_team = Team.objects.select_for_update().get(pk=team.pk)

        # Layer 1: 用户每日生成次数限额 (skip if -1)
        if user.daily_generation_limit != -1:
            daily_count = user.generation_records.filter(created_at__date=today).count()
            if daily_count >= user.daily_generation_limit:
                return Response({
                    'error': 'quota_exceeded',
                    'message': f'您今日的生成次数已达上限（{user.daily_generation_limit}次）',
                    'daily_generation_limit': user.daily_generation_limit,
                    'daily_generation_used': daily_count,
                    'reset_at': (timezone.now() + timedelta(days=1)).replace(
                        hour=0, minute=0, second=0, microsecond=0
                    ).isoformat(),
                }, status=status.HTTP_429_TOO_MANY_REQUESTS)

        # Layer 2: 用户每月生成次数限额 (skip if -1)
        if user.monthly_generation_limit != -1:
            monthly_count = user.generation_records.filter(
                created_at__date__gte=first_of_month
            ).count()
            if monthly_count >= user.monthly_generation_limit:
                return Response({
                    'error': 'quota_exceeded',
                    'message': f'您本月的生成次数已达上限（{user.monthly_generation_limit}次）',
                    'monthly_generation_limit': user.monthly_generation_limit,
                    'monthly_generation_used': monthly_count,
                }, status=status.HTTP_429_TOO_MANY_REQUESTS)

        # Layer 2.5: 用户总消费额度 (skip if -1)
        from decimal import Decimal
        if user.spending_limit != Decimal('-1'):
            # 已完成的用 cost_amount，处理中/排队的用 frozen_amount（预估费用）
            completed_cost = GenerationRecord.objects.filter(
                user=user, status='completed',
            ).aggregate(total=Sum('cost_amount'))['total'] or Decimal('0')
            pending_cost = GenerationRecord.objects.filter(
                user=user, status__in=['processing', 'queued'],
            ).aggregate(total=Sum('frozen_amount'))['total'] or Decimal('0')
            total_spent = completed_cost + pending_cost
            if total_spent + estimated_cost > user.spending_limit:
                return Response({
                    'error': 'spending_limit_exceeded',
                    'message': f'余额不足，总额度 ¥{user.spending_limit}，已消费 ¥{total_spent:.2f}，剩余 ¥{(user.spending_limit - total_spent):.2f}，本次预估 ¥{estimated_cost:.2f}',
                    'spending_limit': float(user.spending_limit),
                    'total_spent': float(total_spent),
                }, status=status.HTTP_429_TOO_MANY_REQUESTS)

        # Layer 2.6: 团队并发限制
        if locked_team.max_concurrent_tasks > 0:
            processing_count = GenerationRecord.objects.filter(
                user__team=locked_team,
                status__in=['queued', 'processing'],
            ).count()
            if processing_count >= locked_team.max_concurrent_tasks:
                return Response({
                    'error': 'concurrent_limit',
                    'message': f'团队当前有 {processing_count} 个任务正在处理，已达并发上限 {processing_count}/{locked_team.max_concurrent_tasks}',
                }, status=status.HTTP_429_TOO_MANY_REQUESTS)

        # Layer 3: 团队月消费限额
        if locked_team.monthly_spending_limit != -1:
            team_monthly_spent = GenerationRecord.objects.filter(
                user__team=locked_team,
                created_at__date__gte=first_of_month,
            ).aggregate(total=Sum('cost_amount'))['total'] or 0
            if team_monthly_spent + estimated_cost > locked_team.monthly_spending_limit:
                return Response({
                    'error': 'quota_exceeded',
                    'message': '团队本月消费额度已用完',
                    'team_monthly_limit': float(locked_team.monthly_spending_limit),
                    'team_monthly_spent': float(team_monthly_spent),
                }, status=status.HTTP_429_TOO_MANY_REQUESTS)

        # Layer 4: 团队可用余额
        available = locked_team.balance - locked_team.frozen_amount
        if estimated_cost > available:
            return Response({
                'error': 'quota_exceeded',
                'message': '团队余额不足，请联系管理员充值',
                'team_balance': float(locked_team.balance),
                'team_frozen': float(locked_team.frozen_amount),
                'team_available': float(available),
                'estimated_cost': float(estimated_cost),
            }, status=status.HTTP_429_TOO_MANY_REQUESTS)

        # 构建参考素材
        references = request.data.get('references', [])
        # 火山限制最多 9 张参考图片
        image_count = sum(1 for r in references if r.get('type', 'image') == 'image')
        if image_count > 9:
            return Response({
                'error': 'too_many_references',
                'message': f'参考图片最多 9 张，当前 {image_count} 张，请减少后重试',
            }, status=status.HTTP_400_BAD_REQUEST)

        reference_snapshots = []
        content_items = []
        seen_urls = set()  # 去重：同一个素材只引用一次
        _asset_cache = {}  # group_id → [(asset_url, asset_type), ...]，避免同一素材组重复查询

        # 火山规范要求 prompt 里用「图片N/视频N/音频N」指代素材（不能用文件名/asset id）。
        # 循环同步维护各类型 counter 和 label→placeholder 映射，循环结束后一次性替换 prompt。
        # 不变量：任意时刻 image_n / video_n / audio_n == content_items 里该类型 *_url 已 push 的个数。
        label_to_placeholder: dict = {}
        image_n = video_n = audio_n = 0

        def _placeholder_for(asset_type):
            """读取当前 counter 值对应的 placeholder。调用前 counter 必须已递增。"""
            if asset_type == 'Video':
                return f'视频{video_n}'
            if asset_type == 'Audio':
                return f'音频{audio_n}'
            return f'图片{image_n}'

        from .models import Asset as AssetModel

        def _resolve_asset_group_all(gid, lbl):
            """查询本地 DB 获取组内所有 active 素材，返回 [(asset_url, asset_type), ...] 列表。
            processing 的素材会尝试实时刷新状态。"""
            assets = list(AssetModel.objects.filter(
                group_id=gid, group__team=team, status__in=['active', 'processing']
            ).exclude(remote_asset_id='').order_by('created_at'))
            if not assets:
                logger.warning('No assets found for group %s (label=%s)', gid, lbl)
                return []
            resolved_list = []
            for asset in assets:
                # 本地 processing → 实时查火山刷新
                if asset.status == 'processing':
                    result, _ = _assets_api_call(assets_client.get_asset, asset.remote_asset_id)
                    if result and result.get('Status') == 'Active':
                        asset.status = 'active'
                        asset.url = result.get('Url', asset.url)
                        asset.save(update_fields=['status', 'url'])
                        logger.info('Asset %s refreshed to active from Volcano', asset.remote_asset_id)
                    else:
                        logger.warning('Asset %s still processing, skipped', asset.remote_asset_id)
                        continue  # 跳过未就绪的素材
                aid = asset.remote_asset_id
                if aid.startswith('Asset-'):
                    aid = 'asset-' + aid[6:]
                resolved_url = f'asset://{aid}'
                resolved_list.append((resolved_url, asset.asset_type))
            logger.info('Asset group %s resolved: %d assets', gid, len(resolved_list))
            return resolved_list

        from utils import assets_client

        for ref in references:
            url = ref.get('url', '')
            original_url = url  # 保留原始 URL 用于 reference_snapshots
            ref_type = ref.get('type', 'image')
            role = ref.get('role', '')
            label = ref.get('label', '')

            # 跳过重复 URL — 在所有操作之前判断
            if original_url in seen_urls:
                continue
            seen_urls.add(original_url)

            # 拦截 blob: URL（前端上传失败的兜底）
            if url.startswith('blob:'):
                return Response({
                    'error': 'upload_failed',
                    'message': f'素材「{label}」上传失败，请删除后重新添加',
                }, status=status.HTTP_400_BAD_REQUEST)

            # 快照存原始 URL（前端重建 reEdit 需要 asset://group-{id} 格式）
            snap = {'url': original_url, 'type': ref_type, 'role': role, 'label': label}
            thumb_url = ref.get('thumb_url', '')
            if thumb_url:
                snap['thumb_url'] = thumb_url
            reference_snapshots.append(snap)

            # 单素材引用：asset://local-{id} → 查 Asset 表 → 单个 content_item
            if url.startswith('asset://local-'):
                try:
                    asset_local_id = int(url.replace('asset://local-', ''))
                    asset_obj = AssetModel.objects.get(pk=asset_local_id, group__team=team)
                    if asset_obj.status != 'active':
                        return Response({
                            'error': 'asset_not_ready',
                            'message': f'素材「{label}」尚在处理中，请稍后重试',
                        }, status=status.HTTP_400_BAD_REQUEST)
                    if not asset_obj.remote_asset_id:
                        return Response({
                            'error': 'asset_not_ready',
                            'message': f'素材「{label}」尚未就绪，请稍后重试',
                        }, status=status.HTTP_400_BAD_REQUEST)
                    aid = asset_obj.remote_asset_id
                    if aid.startswith('Asset-'):
                        aid = 'asset-' + aid[6:]
                    resolved_asset_url = f'asset://{aid}'
                    if asset_obj.asset_type == 'Video':
                        video_n += 1
                        content_items.append({'type': 'video_url', 'video_url': {'url': resolved_asset_url}, 'role': 'reference_video'})
                    elif asset_obj.asset_type == 'Audio':
                        audio_n += 1
                        content_items.append({'type': 'audio_url', 'audio_url': {'url': resolved_asset_url}, 'role': 'reference_audio'})
                    else:
                        image_n += 1
                        content_items.append({'type': 'image_url', 'image_url': {'url': resolved_asset_url}, 'role': 'reference_image'})
                    if label and label not in label_to_placeholder:
                        label_to_placeholder[label] = _placeholder_for(asset_obj.asset_type)
                except AssetModel.DoesNotExist:
                    return Response({
                        'error': 'asset_not_found',
                        'message': f'素材「{label}」不存在或已被删除',
                    }, status=status.HTTP_400_BAD_REQUEST)
                except Exception as e:
                    logger.warning('Failed to resolve asset URL %s: %s', url, e)
                    return Response({
                        'error': 'asset_not_ready',
                        'message': f'素材「{label}」解析失败，请重试',
                    }, status=status.HTTP_400_BAD_REQUEST)
                continue

            # 向后兼容：asset://group-{id} → 展开为组内所有 active 素材
            if url.startswith('asset://group-'):
                try:
                    group_id = int(url.replace('asset://group-', ''))
                    if group_id in _asset_cache:
                        asset_list = _asset_cache[group_id]
                    else:
                        asset_list = _resolve_asset_group_all(group_id, label)
                        _asset_cache[group_id] = asset_list
                    if not asset_list:
                        return Response({
                            'error': 'asset_not_ready',
                            'message': f'素材「{label}」尚未就绪，请在素材库中确认状态为"可用"后重试',
                        }, status=status.HTTP_400_BAD_REQUEST)
                    for asset_url, asset_type in asset_list:
                        if asset_type == 'Video':
                            video_n += 1
                            content_items.append({'type': 'video_url', 'video_url': {'url': asset_url}, 'role': 'reference_video'})
                        elif asset_type == 'Audio':
                            audio_n += 1
                            content_items.append({'type': 'audio_url', 'audio_url': {'url': asset_url}, 'role': 'reference_audio'})
                        else:
                            image_n += 1
                            content_items.append({'type': 'image_url', 'image_url': {'url': asset_url}, 'role': 'reference_image'})
                    # 老兼容路径：一个 label 对应 N 张图，展开成"图片N"会改变语义，不登记 label。
                    # 但 counter 必须继续递增，否则后续 local 分支的编号会错位。
                    logger.warning('legacy asset://group-%s used (label=%s), skip @-replacement (counter advanced by %d)', group_id, label, len(asset_list))
                except Exception as e:
                    logger.warning('Failed to resolve asset group URL %s: %s', url, e)
                    return Response({
                        'error': 'asset_not_ready',
                        'message': f'素材「{label}」解析失败，请重试',
                    }, status=status.HTTP_400_BAD_REQUEST)
                continue  # 素材组已展开为多个 content_items，跳过下面的单项处理

            if ref_type == 'image':
                image_n += 1
                item = {'type': 'image_url', 'image_url': {'url': url}}
                # API 文档要求：参考图模式下所有图片的 role 必须为 reference_image
                if mode == 'universal':
                    item['role'] = 'reference_image'
                elif role:
                    item['role'] = role
                content_items.append(item)
            elif ref_type == 'video':
                video_n += 1
                item = {'type': 'video_url', 'video_url': {'url': url}}
                if role:
                    item['role'] = role
                content_items.append(item)
            elif ref_type == 'audio':
                audio_n += 1
                item = {'type': 'audio_url', 'audio_url': {'url': url}}
                if role:
                    item['role'] = role
                content_items.append(item)
            else:
                # 防御性：未知 ref_type（脏数据或未来扩展）→ 不推 content_item, 不登记
                logger.warning('unknown ref_type=%s url=%s label=%s, skipped', ref_type, url, label)
                continue

            if label and label not in label_to_placeholder:
                _type_map = {'image': 'Image', 'video': 'Video', 'audio': 'Audio'}
                label_to_placeholder[label] = _placeholder_for(_type_map[ref_type])

        logger.info('Video generate: %d content_items built (prompt=%s...)', len(content_items), prompt[:60])

        # 冻结（不扣余额）
        record = GenerationRecord.objects.create(
            user=user,
            prompt=prompt,
            mode=mode,
            model=model,
            aspect_ratio=aspect_ratio,
            duration=duration,
            seconds_consumed=duration,
            frozen_amount=estimated_cost,
            resolution=resolution,
            tokens_consumed=0,
            cost_amount=0,
            base_cost_amount=0,
            reference_urls=reference_snapshots,
            seed=seed,
        )

        locked_team.frozen_amount = F('frozen_amount') + estimated_cost
        locked_team.total_seconds_used = F('total_seconds_used') + duration
        locked_team.save(update_fields=['frozen_amount', 'total_seconds_used'])

    # ── 调用 AirDrama API（事务外，避免持锁） ──
    from django.conf import settings as django_settings
    if django_settings.SEEDANCE_ENABLED and django_settings.ARK_API_KEY:
        # 按火山规范把 @label 替换为「图片N/视频N/音频N」；DB 的 record.prompt 仍保留原文
        sorted_pairs = sorted(label_to_placeholder.items(), key=lambda kv: -len(kv[0]))
        api_prompt = _format_prompt_for_ark(prompt, sorted_pairs)
        logger.info('[ark-prompt] original=%s | converted=%s | mapping=%s',
                    prompt, api_prompt, label_to_placeholder)
        # 即使 create_task 抛错也保留 api_prompt 方便事后查看实际传了什么
        record.api_prompt = api_prompt
        record.save(update_fields=['api_prompt'])
        try:
            ark_response = create_task(
                prompt=api_prompt,
                model=model,
                content_items=content_items,
                aspect_ratio=aspect_ratio,
                duration=duration,
                search_mode=search_mode,
                seed=seed,
                resolution=resolution,
            )
            ark_task_id = ark_response.get('id', '')
            record.ark_task_id = ark_task_id
            record.status = 'processing'
            record.save(update_fields=['ark_task_id', 'status'])
            # 触发后端异步轮询
            try:
                from apps.generation.tasks import poll_video_task
                poll_video_task.delay(record.id)
            except Exception:
                logger.warning('Celery dispatch failed for record %s, retrying once...', record.id)
                import time
                time.sleep(1)
                try:
                    from apps.generation.tasks import poll_video_task as _poll
                    _poll.delay(record.id)
                except Exception:
                    logger.error('Celery dispatch failed twice for record %s, relying on recovery task', record.id)
        except Exception as e:
            logger.exception('AirDrama API create task failed')
            record.status = 'failed'
            record.completed_at = timezone.now()
            from utils.airdrama_client import AirDramaAPIError
            if isinstance(e, AirDramaAPIError):
                record.error_message = e.user_message
                record.raw_error = f'{e.code}: {e.api_message}' if hasattr(e, 'code') else str(e)
            else:
                record.error_message = '生成失败，请重试'
                record.raw_error = str(e)
            record.save(update_fields=['status', 'completed_at', 'error_message', 'raw_error'])
            # API 调用失败，释放冻结
            _release_freeze(record)
    else:
        record.status = 'completed'
        record.completed_at = timezone.now()
        record.save(update_fields=['status', 'completed_at'])

    return Response({
        'task_id': str(record.task_id),
        'ark_task_id': getattr(record, 'ark_task_id', ''),
        'status': record.status,
        'estimated_time': 120,
        'seconds_consumed': duration,
        'estimated_tokens': estimated_tokens,
        'estimated_cost': float(estimated_cost),
        'error_message': getattr(record, 'error_message', '') or '',
    }, status=status.HTTP_202_ACCEPTED)


def _release_freeze(record):
    """释放冻结金额（不扣费）。"""
    if record.frozen_amount == 0:
        return  # already released
    team = record.user.team
    if not team:
        return
    frozen = record.frozen_amount
    with transaction.atomic():
        locked_team = Team.objects.select_for_update().get(pk=team.pk)
        # 防止 frozen_amount 变负
        actual_release = min(frozen, locked_team.frozen_amount)
        if actual_release > 0:
            locked_team.frozen_amount = F('frozen_amount') - actual_release
        locked_team.total_seconds_used = F('total_seconds_used') - record.seconds_consumed
        locked_team.save(update_fields=['frozen_amount', 'total_seconds_used'])
    record.frozen_amount = 0
    record.seconds_consumed = 0
    record.save(update_fields=['frozen_amount', 'seconds_consumed'])


def _settle_payment(record, total_tokens):
    """任务完成时结算：按实际 tokens 扣费并释放冻结。"""
    if record.frozen_amount == 0:
        return  # 已结算，防止重复扣费
    team = record.user.team
    if not team:
        return
    config = QuotaConfig.objects.get_or_create(pk=1)[0]
    has_video_ref = _has_video_reference(record.reference_urls)
    # 按任务实际 resolution 取单价（1080P 任务用 1080P 单价结算）
    # record.resolution 有 model 层 default='720p' + choices 约束 + data migration 回填，永远不为空
    token_price = _get_token_price(config, record.model, has_video_ref, record.resolution)
    actual_cost = calculate_cost(total_tokens, token_price, team.markup_percentage)
    base_cost = calculate_base_cost(total_tokens, token_price)
    frozen = record.frozen_amount

    with transaction.atomic():
        locked_team = Team.objects.select_for_update().get(pk=team.pk)
        locked_team.balance = F('balance') - actual_cost
        locked_team.total_spent = F('total_spent') + actual_cost
        locked_team.frozen_amount = F('frozen_amount') - frozen
        locked_team.save(update_fields=['balance', 'total_spent', 'frozen_amount'])

    record.tokens_consumed = total_tokens
    record.cost_amount = actual_cost
    record.base_cost_amount = base_cost
    record.frozen_amount = 0
    record.save(update_fields=['tokens_consumed', 'cost_amount', 'base_cost_amount', 'frozen_amount'])


# ──────────────────────────────────────────────
# Video Tasks: List + Detail (with failure refund)
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def video_tasks_list_view(request):
    """GET /api/v1/video/tasks — User's recent generation tasks (paginated).

    Query params:
        page_size: Number of tasks per page (default 20, max 100).
        offset: Number of tasks to skip (default 0).
    """
    user = request.user
    page_size = min(_safe_int(request.query_params.get('page_size', 20), 20), 100)
    offset = max(_safe_int(request.query_params.get('offset', 0), 0), 0)

    qs = user.generation_records.filter(is_deleted=False).order_by('-created_at')
    total = qs.count()
    records = _eval_qs(qs, limit=offset + page_size)
    # Apply offset after evaluation (defer compat)
    records = records[offset:]

    results = [_serialize_task(r) for r in records]
    return Response({
        'results': results,
        'total': total,
        'has_more': offset + page_size < total,
    })


@api_view(['GET', 'DELETE'])
@permission_classes([IsAuthenticated])
def video_task_detail_view(request, task_id):
    """GET /api/v1/video/tasks/<task_id> — Read task status from DB (settlement by Celery only).
    DELETE /api/v1/video/tasks/<task_id> — Soft-delete task record."""
    try:
        record = _eval_qs(
            GenerationRecord.objects.filter(user=request.user, is_deleted=False),
            get_kwargs={'task_id': task_id},
        )
    except GenerationRecord.DoesNotExist:
        return Response({'error': '任务不存在'}, status=status.HTTP_404_NOT_FOUND)

    if request.method == 'DELETE':
        record.is_deleted = True
        record.save(update_fields=['is_deleted'])
        return Response(status=status.HTTP_204_NO_CONTENT)

    # Frontend polling only reads DB state — settlement is handled exclusively by Celery

    return Response(_serialize_task(record))


def _serialize_task(record):
    """Serialize a GenerationRecord for the frontend."""
    d = record.__dict__
    return {
        'id': record.id,
        'task_id': str(record.task_id),
        'ark_task_id': d.get('ark_task_id', ''),
        'prompt': record.prompt,
        'mode': record.mode,
        'model': record.model,
        'aspect_ratio': record.aspect_ratio,
        'resolution': record.resolution,
        'duration': record.duration,
        'seconds_consumed': record.seconds_consumed,
        'tokens_consumed': record.tokens_consumed,
        'cost_amount': float(record.cost_amount),
        'base_cost_amount': float(record.base_cost_amount),
        'status': record.status,
        'result_url': d.get('result_url', ''),
        'thumbnail_url': d.get('thumbnail_url', ''),
        'error_message': d.get('error_message', ''),
        'reference_urls': d.get('reference_urls') or [],
        'is_favorited': record.is_favorited,
        'seed': record.seed,
        'created_at': record.created_at.isoformat(),
    }


@api_view(['POST'])
@permission_classes([IsAuthenticated])
def video_task_toggle_favorite_view(request, task_id):
    """POST /api/v1/video/tasks/<task_id>/favorite — Toggle favorite."""
    try:
        record = GenerationRecord.objects.get(task_id=task_id, user=request.user)
    except GenerationRecord.DoesNotExist:
        return Response({'error': '任务不存在'}, status=status.HTTP_404_NOT_FOUND)

    record.is_favorited = not record.is_favorited
    record.save(update_fields=['is_favorited'])
    return Response({'is_favorited': record.is_favorited})


# ──────────────────────────────────────────────
# Admin: Dashboard Stats
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsSuperAdmin])
def admin_stats_view(request):
    """GET /api/v1/admin/stats"""
    today = timezone.now().date()
    yesterday = today - timedelta(days=1)
    first_of_month = today.replace(day=1)
    thirty_days_ago = today - timedelta(days=29)

    total_users = User.objects.count()
    total_teams = Team.objects.count()
    new_users_today = User.objects.filter(date_joined__date=today).count()

    seconds_today = GenerationRecord.objects.filter(
        created_at__date=today
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    seconds_yesterday = GenerationRecord.objects.filter(
        created_at__date=yesterday
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    seconds_this_month = GenerationRecord.objects.filter(
        created_at__date__gte=first_of_month
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    # Cost-based stats
    cost_today = GenerationRecord.objects.filter(
        created_at__date=today
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    cost_yesterday = GenerationRecord.objects.filter(
        created_at__date=yesterday
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    cost_this_month = GenerationRecord.objects.filter(
        created_at__date__gte=first_of_month
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    base_cost_today = GenerationRecord.objects.filter(
        created_at__date=today
    ).aggregate(total=Sum('base_cost_amount'))['total'] or 0

    base_cost_this_month = GenerationRecord.objects.filter(
        created_at__date__gte=first_of_month
    ).aggregate(total=Sum('base_cost_amount'))['total'] or 0

    # Total revenue / cost / profit
    total_revenue = GenerationRecord.objects.aggregate(total=Sum('cost_amount'))['total'] or 0
    total_base_cost = GenerationRecord.objects.aggregate(total=Sum('base_cost_amount'))['total'] or 0
    total_profit = total_revenue - total_base_cost
    profit_margin = round(float(total_profit) / max(float(total_revenue), 0.01) * 100, 1) if total_revenue else 0

    # Last month same period for comparison
    if first_of_month.month == 1:
        last_month_start = first_of_month.replace(year=first_of_month.year - 1, month=12)
    else:
        last_month_start = first_of_month.replace(month=first_of_month.month - 1)
    days_into_month = (today - first_of_month).days + 1
    last_month_same_day = last_month_start + timedelta(days=days_into_month - 1)
    seconds_last_month_period = GenerationRecord.objects.filter(
        created_at__date__gte=last_month_start,
        created_at__date__lte=last_month_same_day
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    cost_last_month_period = GenerationRecord.objects.filter(
        created_at__date__gte=last_month_start,
        created_at__date__lte=last_month_same_day
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    today_change = round(((float(cost_today) - float(cost_yesterday)) / max(float(cost_yesterday), 0.01)) * 100, 1) if cost_yesterday else 0
    month_change = round(((float(cost_this_month) - float(cost_last_month_period)) / max(float(cost_last_month_period), 0.01)) * 100, 1) if cost_last_month_period else 0

    # Daily trend for past 30 days (cost + base_cost)
    daily_trend_qs = (
        GenerationRecord.objects
        .filter(created_at__date__gte=thirty_days_ago)
        .annotate(date=TruncDate('created_at'))
        .values('date')
        .annotate(
            seconds=Sum('seconds_consumed'),
            cost=Sum('cost_amount'),
            base_cost=Sum('base_cost_amount'),
        )
        .order_by('date')
    )
    trend_map = {str(item['date']): item for item in daily_trend_qs}
    daily_trend = []
    for i in range(30):
        d = thirty_days_ago + timedelta(days=i)
        item = trend_map.get(str(d), {})
        daily_trend.append({
            'date': str(d),
            'seconds': item.get('seconds') or 0,
            'cost': float(item.get('cost') or 0),
            'base_cost': float(item.get('base_cost') or 0),
        })

    # Top 10 users by cost consumed this month
    top_users = (
        User.objects.annotate(
            cost_consumed=Sum(
                'generation_records__cost_amount',
                filter=Q(generation_records__created_at__date__gte=first_of_month),
            ),
            seconds_consumed=Sum(
                'generation_records__seconds_consumed',
                filter=Q(generation_records__created_at__date__gte=first_of_month),
            ),
        )
        .filter(Q(cost_consumed__gt=0) | Q(seconds_consumed__gt=0))
        .order_by('-cost_consumed', '-seconds_consumed')[:10]
    )

    # Team consumption ranking this month
    top_teams = (
        Team.objects.annotate(
            cost_consumed=Sum(
                'members__generation_records__cost_amount',
                filter=Q(members__generation_records__created_at__date__gte=first_of_month),
            ),
            seconds_consumed=Sum(
                'members__generation_records__seconds_consumed',
                filter=Q(members__generation_records__created_at__date__gte=first_of_month),
            ),
        )
        .filter(Q(cost_consumed__gt=0) | Q(seconds_consumed__gt=0))
        .order_by('-cost_consumed', '-seconds_consumed')
    )

    # Team profit ranking
    team_profit_ranking = (
        Team.objects.annotate(
            team_revenue=Sum(
                'members__generation_records__cost_amount',
                filter=Q(members__generation_records__created_at__date__gte=first_of_month),
            ),
            team_base_cost=Sum(
                'members__generation_records__base_cost_amount',
                filter=Q(members__generation_records__created_at__date__gte=first_of_month),
            ),
        )
        .filter(team_revenue__gt=0)
        .order_by('-team_revenue')
    )

    return Response({
        'total_users': total_users,
        'total_teams': total_teams,
        'new_users_today': new_users_today,
        'seconds_consumed_today': seconds_today,
        'seconds_consumed_this_month': seconds_this_month,
        'cost_today': float(cost_today),
        'cost_this_month': float(cost_this_month),
        'base_cost_today': float(base_cost_today),
        'base_cost_this_month': float(base_cost_this_month),
        'total_revenue': float(total_revenue),
        'total_base_cost': float(total_base_cost),
        'total_profit': float(total_profit),
        'profit_margin': profit_margin,
        'today_change_percent': today_change,
        'month_change_percent': month_change,
        'daily_trend': daily_trend,
        'top_users': [
            {
                'user_id': u.id, 'username': u.username,
                'cost_consumed': float(u.cost_consumed or 0),
                'seconds_consumed': u.seconds_consumed or 0,
            }
            for u in top_users
        ],
        'top_teams': [
            {
                'team_id': t.id, 'name': t.name,
                'cost_consumed': float(t.cost_consumed or 0),
                'seconds_consumed': t.seconds_consumed or 0,
            }
            for t in top_teams
        ],
        'team_profit_ranking': [
            {
                'team_id': t.id, 'name': t.name,
                'revenue': float(t.team_revenue or 0),
                'base_cost': float(t.team_base_cost or 0),
                'profit': float((t.team_revenue or 0) - (t.team_base_cost or 0)),
            }
            for t in team_profit_ranking
        ],
    })


# ──────────────────────────────────────────────
# Admin: Team Management (Super Admin only)
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsSuperAdmin])
def admin_teams_list_view(request):
    """GET /api/v1/admin/teams — List all teams."""
    today = timezone.now().date()
    first_of_month = today.replace(day=1)

    teams = Team.objects.all().order_by('-created_at')
    results = []
    for t in teams:
        monthly_used = GenerationRecord.objects.filter(
            user__team=t,
            created_at__date__gte=first_of_month,
        ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

        monthly_spent = GenerationRecord.objects.filter(
            user__team=t,
            created_at__date__gte=first_of_month,
        ).aggregate(total=Sum('cost_amount'))['total'] or 0

        results.append({
            'id': t.id,
            'name': t.name,
            'total_seconds_pool': t.total_seconds_pool,
            'total_seconds_used': t.total_seconds_used,
            'remaining_seconds': t.remaining_seconds,
            'monthly_seconds_limit': t.monthly_seconds_limit,
            'monthly_seconds_used': monthly_used,
            'balance': float(t.balance),
            'total_spent': float(t.total_spent),
            'available_balance': float(t.available_balance),
            'monthly_spending_limit': float(t.monthly_spending_limit),
            'monthly_spent': float(monthly_spent),
            'frozen_amount': float(t.frozen_amount),
            'markup_percentage': float(t.markup_percentage),
            'daily_member_limit_default': t.daily_member_limit_default,
            'max_concurrent_tasks': t.max_concurrent_tasks,
            'current_processing': GenerationRecord.objects.filter(
                user__team=t, status__in=['queued', 'processing'],
            ).count(),
            'member_count': t.members.count(),
            'is_active': t.is_active,
            'expected_regions': t.expected_regions,
            'disabled_by': t.disabled_by,
            'created_at': t.created_at.isoformat(),
        })

    return Response({'results': results})


@api_view(['POST'])
@permission_classes([IsSuperAdmin])
def admin_team_create_view(request):
    """POST /api/v1/admin/teams/create — Create a new team."""
    serializer = TeamCreateSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    name = serializer.validated_data['name']
    if Team.objects.filter(name=name).exists():
        return Response({'error': '团队名称已存在'}, status=status.HTTP_400_BAD_REQUEST)

    team = Team.objects.create(**serializer.validated_data)
    log_admin_action(request, 'team_create', 'team', target_id=team.id, target_name=team.name,
                     after={'name': team.name, 'monthly_seconds_limit': team.monthly_seconds_limit,
                            'daily_member_limit_default': team.daily_member_limit_default,
                            'markup_percentage': float(team.markup_percentage),
                            'monthly_spending_limit': float(team.monthly_spending_limit),
                            'daily_member_spending_default': float(team.daily_member_spending_default),
                            'expected_regions': team.expected_regions})
    return Response({
        'id': team.id,
        'name': team.name,
        'monthly_seconds_limit': team.monthly_seconds_limit,
        'daily_member_limit_default': team.daily_member_limit_default,
        'markup_percentage': float(team.markup_percentage),
        'monthly_spending_limit': float(team.monthly_spending_limit),
        'daily_member_spending_default': float(team.daily_member_spending_default),
        'expected_regions': team.expected_regions,
        'created_at': team.created_at.isoformat(),
    }, status=status.HTTP_201_CREATED)


@api_view(['GET', 'PUT'])
@permission_classes([IsSuperAdmin])
def admin_team_detail_view(request, team_id):
    """GET/PUT /api/v1/admin/teams/<id> — Team detail + members / update team."""
    try:
        team = Team.objects.get(id=team_id)
    except Team.DoesNotExist:
        return Response({'error': '团队不存在'}, status=status.HTTP_404_NOT_FOUND)

    if request.method == 'PUT':
        serializer = TeamUpdateSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        # Handle disabled_by based on is_active change
        def _json_safe(v):
            from decimal import Decimal as D
            return float(v) if isinstance(v, D) else v
        before = {f: _json_safe(getattr(team, f)) for f in serializer.validated_data}
        before['disabled_by'] = team.disabled_by
        for field, value in serializer.validated_data.items():
            setattr(team, field, value)

        # If admin manually toggles is_active, update disabled_by
        if 'is_active' in serializer.validated_data:
            if serializer.validated_data['is_active']:
                team.disabled_by = ''
            else:
                team.disabled_by = 'admin'

        team.save()

        # Update TeamAnomalyConfig if provided
        anomaly_config_data = request.data.get('anomaly_config')
        if anomaly_config_data and isinstance(anomaly_config_data, dict):
            ac_serializer = TeamAnomalyConfigSerializer(data=anomaly_config_data)
            ac_serializer.is_valid(raise_exception=True)
            ac, _ = TeamAnomalyConfig.objects.get_or_create(team=team)
            for field, value in ac_serializer.validated_data.items():
                setattr(ac, field, value)
            ac.save()

        after = {f: _json_safe(getattr(team, f)) for f in serializer.validated_data}
        after['disabled_by'] = team.disabled_by
        log_admin_action(request, 'team_update', 'team', target_id=team.id, target_name=team.name,
                         before=before, after=after)
        return Response({
            'id': team.id,
            'name': team.name,
            'monthly_seconds_limit': team.monthly_seconds_limit,
            'daily_member_limit_default': team.daily_member_limit_default,
            'markup_percentage': float(team.markup_percentage),
            'monthly_spending_limit': float(team.monthly_spending_limit),
            'daily_member_spending_default': float(team.daily_member_spending_default),
            'max_concurrent_tasks': team.max_concurrent_tasks,
            'is_active': team.is_active,
            'expected_regions': team.expected_regions,
            'disabled_by': team.disabled_by,
            'updated_at': team.updated_at.isoformat(),
        })

    # GET: team detail + members
    today = timezone.now().date()
    first_of_month = today.replace(day=1)

    monthly_used = GenerationRecord.objects.filter(
        user__team=team,
        created_at__date__gte=first_of_month,
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    monthly_spent = GenerationRecord.objects.filter(
        user__team=team,
        created_at__date__gte=first_of_month,
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    members = team.members.annotate(
        seconds_today=Sum(
            'generation_records__seconds_consumed',
            filter=Q(generation_records__created_at__date=today),
        ),
        seconds_this_month=Sum(
            'generation_records__seconds_consumed',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
        generations_today=Count(
            'generation_records',
            filter=Q(generation_records__created_at__date=today),
        ),
        generations_this_month=Count(
            'generation_records',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
        spent_today=Sum(
            'generation_records__cost_amount',
            filter=Q(generation_records__created_at__date=today),
        ),
        spent_this_month=Sum(
            'generation_records__cost_amount',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
        is_online=Exists(ActiveSession.objects.filter(user_id=OuterRef('pk'))),
    ).order_by('-date_joined')

    # TeamAnomalyConfig
    try:
        ac = team.anomaly_config
        anomaly_config = {
            'r1_enabled': ac.r1_enabled,
            'r2_enabled': ac.r2_enabled,
            'r2_window_seconds': ac.r2_window_seconds,
            'r3_enabled': ac.r3_enabled,
            'r3_window_seconds': ac.r3_window_seconds,
            'r3_max_count': ac.r3_max_count,
            'r4_enabled': ac.r4_enabled,
            'r4_window_seconds': ac.r4_window_seconds,
            'r4_city_count': ac.r4_city_count,
            'r5_enabled': ac.r5_enabled,
            'r5_days': ac.r5_days,
            'r5_country_count': ac.r5_country_count,
        }
    except TeamAnomalyConfig.DoesNotExist:
        anomaly_config = None

    return Response({
        'id': team.id,
        'name': team.name,
        'total_seconds_pool': team.total_seconds_pool,
        'total_seconds_used': team.total_seconds_used,
        'remaining_seconds': team.remaining_seconds,
        'monthly_seconds_limit': team.monthly_seconds_limit,
        'monthly_seconds_used': monthly_used,
        'balance': float(team.balance),
        'total_spent': float(team.total_spent),
        'available_balance': float(team.available_balance),
        'monthly_spending_limit': float(team.monthly_spending_limit),
        'monthly_spent': float(monthly_spent),
        'frozen_amount': float(team.frozen_amount),
        'markup_percentage': float(team.markup_percentage),
        'daily_member_limit_default': team.daily_member_limit_default,
        'max_concurrent_tasks': team.max_concurrent_tasks,
        'current_processing': GenerationRecord.objects.filter(
            user__team=team, status__in=['queued', 'processing'],
        ).count(),
        'member_count': team.members.count(),
        'is_active': team.is_active,
        'expected_regions': team.expected_regions,
        'disabled_by': team.disabled_by,
        'anomaly_config': anomaly_config,
        'created_at': team.created_at.isoformat(),
        'members': [{
            'id': m.id,
            'username': m.username,
            'email': m.email,
            'is_team_admin': m.is_team_admin,
            'is_team_owner': m.is_team_owner,
            'is_active': m.is_active,
            'disabled_by': m.disabled_by,
            'daily_seconds_limit': m.daily_seconds_limit,
            'monthly_seconds_limit': m.monthly_seconds_limit,
            'daily_generation_limit': m.daily_generation_limit,
            'monthly_generation_limit': m.monthly_generation_limit,
            'seconds_today': m.seconds_today or 0,
            'seconds_this_month': m.seconds_this_month or 0,
            'generations_today': m.generations_today or 0,
            'generations_this_month': m.generations_this_month or 0,
            'spent_today': float(m.spent_today or 0),
            'spent_this_month': float(m.spent_this_month or 0),
            'is_online': m.is_online,
            'date_joined': m.date_joined.isoformat(),
        } for m in members],
    })


@api_view(['PATCH'])
@permission_classes([IsSuperAdmin])
def admin_team_member_role_view(request, team_id, member_id):
    """PATCH /api/v1/admin/teams/<id>/members/<id>/role — Toggle team admin role."""
    try:
        team = Team.objects.get(id=team_id)
    except Team.DoesNotExist:
        return Response({'error': '团队不存在'}, status=status.HTTP_404_NOT_FOUND)
    try:
        member = team.members.get(id=member_id)
    except User.DoesNotExist:
        return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)

    is_admin = request.data.get('is_team_admin')
    is_owner = request.data.get('is_team_owner')
    if is_admin is None and is_owner is None:
        return Response({'error': '请提供 is_team_admin 或 is_team_owner 参数'}, status=status.HTTP_400_BAD_REQUEST)

    before = {'is_team_admin': member.is_team_admin, 'is_team_owner': member.is_team_owner}
    update_fields = []
    if is_admin is not None:
        member.is_team_admin = bool(is_admin)
        update_fields.append('is_team_admin')
        # 取消管理员时同时取消主管
        if not bool(is_admin):
            member.is_team_owner = False
            update_fields.append('is_team_owner')
    if is_owner is not None:
        member.is_team_owner = bool(is_owner)
        if bool(is_owner):
            member.is_team_admin = True  # 主管一定是管理员
            update_fields.append('is_team_admin')
        if 'is_team_owner' not in update_fields:
            update_fields.append('is_team_owner')
    member.save(update_fields=update_fields)
    log_admin_action(request, 'team_update', 'user', target_id=member.id, target_name=member.username,
                     before=before, after={'is_team_admin': member.is_team_admin, 'is_team_owner': member.is_team_owner})

    return Response({
        'user_id': member.id,
        'username': member.username,
        'is_team_admin': member.is_team_admin,
        'is_team_owner': member.is_team_owner,
    })


@api_view(['POST'])
@permission_classes([IsSuperAdmin])
def admin_team_topup_view(request, team_id):
    """POST /api/v1/admin/teams/<id>/topup — Add balance to team."""
    try:
        team = Team.objects.get(id=team_id)
    except Team.DoesNotExist:
        return Response({'error': '团队不存在'}, status=status.HTTP_404_NOT_FOUND)

    serializer = TeamTopUpSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    amount = serializer.validated_data['amount']
    old_balance = float(team.balance)
    with transaction.atomic():
        locked = Team.objects.select_for_update().get(pk=team.pk)
        locked.balance = F('balance') + amount
        locked.save(update_fields=['balance'])

    team.refresh_from_db()
    log_admin_action(request, 'team_topup', 'team', target_id=team.id, target_name=team.name,
                     before={'balance': old_balance},
                     after={'balance': float(team.balance), 'topped_up': float(amount)})
    return Response({
        'id': team.id,
        'name': team.name,
        'balance': float(team.balance),
        'total_spent': float(team.total_spent),
        'available_balance': float(team.available_balance),
        'frozen_amount': float(team.frozen_amount),
        'topped_up': float(amount),
        'total_seconds_pool': team.total_seconds_pool,
        'total_seconds_used': team.total_seconds_used,
        'remaining_seconds': team.remaining_seconds,
    })


@api_view(['PUT'])
@permission_classes([IsSuperAdmin])
def admin_team_set_pool_view(request, team_id):
    """PUT /api/v1/admin/teams/<id>/set-pool — Directly set team balance."""
    try:
        team = Team.objects.get(id=team_id)
    except Team.DoesNotExist:
        return Response({'error': '团队不存在'}, status=status.HTTP_404_NOT_FOUND)

    # Accept both 'balance' (new) and 'total_seconds_pool' (backward compat)
    new_balance = request.data.get('balance')
    new_pool = request.data.get('total_seconds_pool')

    if new_balance is not None:
        from decimal import Decimal, InvalidOperation
        try:
            new_balance = Decimal(str(new_balance))
        except (InvalidOperation, ValueError, TypeError):
            return Response({'error': '请输入有效的金额'}, status=status.HTTP_400_BAD_REQUEST)

        if new_balance < 0:
            return Response({'error': '余额不能为负数'}, status=status.HTTP_400_BAD_REQUEST)

        old_balance = float(team.balance)
        with transaction.atomic():
            locked = Team.objects.select_for_update().get(pk=team.pk)
            locked.balance = new_balance
            locked.save(update_fields=['balance'])

        team.refresh_from_db()
        log_admin_action(request, 'team_set_pool', 'team', target_id=team.id, target_name=team.name,
                         before={'balance': old_balance},
                         after={'balance': float(team.balance)})
        return Response({
            'id': team.id,
            'name': team.name,
            'balance': float(team.balance),
            'total_spent': float(team.total_spent),
            'available_balance': float(team.available_balance),
            'frozen_amount': float(team.frozen_amount),
            'total_seconds_pool': team.total_seconds_pool,
            'total_seconds_used': team.total_seconds_used,
            'remaining_seconds': team.remaining_seconds,
        })

    # Backward compat: total_seconds_pool
    if new_pool is None:
        return Response({'error': 'balance or total_seconds_pool is required'}, status=status.HTTP_400_BAD_REQUEST)

    try:
        new_pool = int(new_pool)
    except (ValueError, TypeError):
        return Response({'error': '请输入有效的数字'}, status=status.HTTP_400_BAD_REQUEST)

    if new_pool < 0:
        return Response({'error': '总秒数池不能为负数'}, status=status.HTTP_400_BAD_REQUEST)

    if new_pool < team.total_seconds_used:
        return Response({'error': f'不能低于已消耗秒数 ({int(team.total_seconds_used)}s)'}, status=status.HTTP_400_BAD_REQUEST)

    old_pool = team.total_seconds_pool
    with transaction.atomic():
        locked = Team.objects.select_for_update().get(pk=team.pk)
        locked.total_seconds_pool = new_pool
        locked.save(update_fields=['total_seconds_pool'])

    team.refresh_from_db()
    log_admin_action(request, 'team_set_pool', 'team', target_id=team.id, target_name=team.name,
                     before={'total_seconds_pool': old_pool},
                     after={'total_seconds_pool': team.total_seconds_pool})
    return Response({
        'id': team.id,
        'name': team.name,
        'balance': float(team.balance),
        'total_spent': float(team.total_spent),
        'available_balance': float(team.available_balance),
        'total_seconds_pool': team.total_seconds_pool,
        'total_seconds_used': team.total_seconds_used,
        'remaining_seconds': team.remaining_seconds,
    })


@api_view(['POST'])
@permission_classes([IsSuperAdmin])
def admin_team_create_admin_view(request, team_id):
    """POST /api/v1/admin/teams/<id>/admin — Create team admin account."""
    try:
        team = Team.objects.get(id=team_id)
    except Team.DoesNotExist:
        return Response({'error': '团队不存在'}, status=status.HTTP_404_NOT_FOUND)

    serializer = TeamAdminCreateSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    username = serializer.validated_data['username']
    email = serializer.validated_data['email']

    if User.objects.filter(username=username).exists():
        return Response({'error': '用户名已存在'}, status=status.HTTP_400_BAD_REQUEST)
    if User.objects.filter(email=email).exists():
        return Response({'error': '邮箱已存在'}, status=status.HTTP_400_BAD_REQUEST)

    config = QuotaConfig.objects.get_or_create(pk=1)[0]
    user = User.objects.create_user(
        username=username,
        email=email,
        password=serializer.validated_data['password'],
        team=team,
        is_team_admin=True,
        is_team_owner=True,
        daily_seconds_limit=team.daily_member_limit_default,
        monthly_seconds_limit=-1,  # Team admin unlimited by default
        daily_generation_limit=-1,  # Team admin unlimited by default
        monthly_generation_limit=-1,
    )
    log_admin_action(request, 'team_create_admin', 'user', target_id=user.id, target_name=user.username,
                     after={'username': user.username, 'email': user.email, 'team': team.name})

    return Response({
        'id': user.id,
        'username': user.username,
        'email': user.email,
        'team': team.name,
        'is_team_admin': True,
    }, status=status.HTTP_201_CREATED)


# ──────────────────────────────────────────────
# Admin: User Management
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsSuperAdmin])
def admin_users_list_view(request):
    """GET /api/v1/admin/users"""
    today = timezone.now().date()
    first_of_month = today.replace(day=1)

    page = _safe_int(request.query_params.get('page', 1), 1)
    page_size = min(_safe_int(request.query_params.get('page_size', 20), 20), 100)
    search = request.query_params.get('search', '').strip()
    status_filter = request.query_params.get('status', '').strip()
    team_id = request.query_params.get('team_id', '').strip()

    qs = User.objects.select_related('team').annotate(
        seconds_today=Sum(
            'generation_records__seconds_consumed',
            filter=Q(generation_records__created_at__date=today),
        ),
        seconds_this_month=Sum(
            'generation_records__seconds_consumed',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
        generations_today=Count(
            'generation_records',
            filter=Q(generation_records__created_at__date=today),
        ),
        generations_this_month=Count(
            'generation_records',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
        spent_today=Sum(
            'generation_records__cost_amount',
            filter=Q(generation_records__created_at__date=today),
        ),
        spent_this_month=Sum(
            'generation_records__cost_amount',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
        is_online=Exists(ActiveSession.objects.filter(user_id=OuterRef('pk'))),
        total_spent_all=Sum(
            'generation_records__cost_amount',
            filter=Q(generation_records__status='completed'),
        ),
    )

    if search:
        qs = qs.filter(Q(username__icontains=search) | Q(email__icontains=search))
    if status_filter == 'active':
        qs = qs.filter(is_active=True)
    elif status_filter == 'disabled':
        qs = qs.filter(is_active=False)
    if team_id:
        qs = qs.filter(team_id=_safe_int(team_id))

    total = qs.count()
    offset = (page - 1) * page_size
    users = qs.order_by('-date_joined')[offset:offset + page_size]

    results = []
    for u in users:
        results.append({
            'id': u.id,
            'username': u.username,
            'email': u.email,
            'is_active': u.is_active,
            'disabled_by': u.disabled_by,
            'is_staff': u.is_staff,
            'is_team_admin': u.is_team_admin,
            'is_team_owner': u.is_team_owner,
            'is_observer': u.is_observer,
            'team_id': u.team_id,
            'team_name': u.team.name if u.team else None,
            'date_joined': u.date_joined.isoformat(),
            'daily_seconds_limit': u.daily_seconds_limit,
            'monthly_seconds_limit': u.monthly_seconds_limit,
            'daily_generation_limit': u.daily_generation_limit,
            'monthly_generation_limit': u.monthly_generation_limit,
            'seconds_today': u.seconds_today or 0,
            'seconds_this_month': u.seconds_this_month or 0,
            'generations_today': u.generations_today or 0,
            'generations_this_month': u.generations_this_month or 0,
            'spent_today': float(u.spent_today or 0),
            'spent_this_month': float(u.spent_this_month or 0),
            'spending_limit': float(u.spending_limit),
            'total_spent': float(u.total_spent_all or 0),
            'is_online': u.is_online,
        })

    return Response({
        'total': total,
        'page': page,
        'page_size': page_size,
        'results': results,
    })


@api_view(['GET'])
@permission_classes([IsSuperAdmin])
def admin_user_detail_view(request, user_id):
    """GET /api/v1/admin/users/:id"""
    try:
        user = User.objects.select_related('team').get(id=user_id)
    except User.DoesNotExist:
        return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)

    today = timezone.now().date()
    first_of_month = today.replace(day=1)

    seconds_today = user.generation_records.filter(
        created_at__date=today
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    seconds_this_month = user.generation_records.filter(
        created_at__date__gte=first_of_month
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    seconds_total = user.generation_records.aggregate(
        total=Sum('seconds_consumed')
    )['total'] or 0

    generations_today = user.generation_records.filter(
        created_at__date=today
    ).count()

    generations_this_month = user.generation_records.filter(
        created_at__date__gte=first_of_month
    ).count()

    spent_today = user.generation_records.filter(
        created_at__date=today
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    spent_this_month = user.generation_records.filter(
        created_at__date__gte=first_of_month
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    total_spent = user.generation_records.aggregate(
        total=Sum('cost_amount')
    )['total'] or 0

    recent_records = _eval_qs(user.generation_records.order_by('-created_at'), limit=20)

    return Response({
        'id': user.id,
        'username': user.username,
        'email': user.email,
        'is_active': user.is_active,
        'is_staff': user.is_staff,
        'is_team_admin': user.is_team_admin,
        'team_id': user.team_id,
        'team_name': user.team.name if user.team else None,
        'date_joined': user.date_joined.isoformat(),
        'daily_seconds_limit': user.daily_seconds_limit,
        'monthly_seconds_limit': user.monthly_seconds_limit,
        'daily_generation_limit': user.daily_generation_limit,
        'monthly_generation_limit': user.monthly_generation_limit,
        'seconds_today': seconds_today,
        'seconds_this_month': seconds_this_month,
        'seconds_total': seconds_total,
        'generations_today': generations_today,
        'generations_this_month': generations_this_month,
        'spent_today': float(spent_today),
        'spent_this_month': float(spent_this_month),
        'total_spent': float(total_spent),
        'recent_records': [
            {
                'id': r.id,
                'created_at': r.created_at.isoformat(),
                'seconds_consumed': r.seconds_consumed,
                'tokens_consumed': r.tokens_consumed,
                'cost_amount': float(r.cost_amount),
                'prompt': r.prompt,
                'mode': r.mode,
                'model': r.model,
                'status': r.status,
                'error_message': r.error_message or '',
            }
            for r in recent_records
        ],
    })


@api_view(['PUT'])
@permission_classes([IsSuperAdmin])
def admin_user_quota_view(request, user_id):
    """PUT /api/v1/admin/users/:id/quota"""
    try:
        user = User.objects.get(id=user_id)
    except User.DoesNotExist:
        return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)

    serializer = QuotaUpdateSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    before = {
        'daily_generation_limit': user.daily_generation_limit,
        'monthly_generation_limit': user.monthly_generation_limit,
        'spending_limit': float(user.spending_limit),
    }
    update_fields = ['daily_generation_limit', 'monthly_generation_limit']
    user.daily_generation_limit = serializer.validated_data['daily_generation_limit']
    user.monthly_generation_limit = serializer.validated_data['monthly_generation_limit']
    if 'spending_limit' in serializer.validated_data:
        user.spending_limit = serializer.validated_data['spending_limit']
        update_fields.append('spending_limit')
    user.save(update_fields=update_fields)
    log_admin_action(request, 'user_quota_update', 'user', target_id=user.id, target_name=user.username,
                     before=before,
                     after={
                         'daily_generation_limit': user.daily_generation_limit,
                         'monthly_generation_limit': user.monthly_generation_limit,
                         'spending_limit': float(user.spending_limit),
                     })

    return Response({
        'user_id': user.id,
        'username': user.username,
        'daily_generation_limit': user.daily_generation_limit,
        'monthly_generation_limit': user.monthly_generation_limit,
        'spending_limit': float(user.spending_limit),
        'daily_seconds_limit': user.daily_seconds_limit,
        'monthly_seconds_limit': user.monthly_seconds_limit,
        'updated_at': timezone.now().isoformat(),
    })


@api_view(['PATCH'])
@permission_classes([IsSuperAdmin])
def admin_user_status_view(request, user_id):
    """PATCH /api/v1/admin/users/:id/status"""
    try:
        user = User.objects.get(id=user_id)
    except User.DoesNotExist:
        return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)

    # 保护 admin 账号不被任何人禁用（包括自己，防误操作）
    if user.username == 'admin':
        return Response({'error': '不能禁用超级管理员账号'}, status=status.HTTP_403_FORBIDDEN)

    serializer = UserStatusSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    old_active = user.is_active
    old_disabled_by = user.disabled_by
    user.is_active = serializer.validated_data['is_active']
    if user.is_active:
        user.disabled_by = ''
    else:
        user.disabled_by = 'admin'
    user.save(update_fields=['is_active', 'disabled_by'])
    log_admin_action(request, 'user_status_toggle', 'user', target_id=user.id, target_name=user.username,
                     before={'is_active': old_active, 'disabled_by': old_disabled_by},
                     after={'is_active': user.is_active, 'disabled_by': user.disabled_by})

    return Response({
        'user_id': user.id,
        'username': user.username,
        'is_active': user.is_active,
        'updated_at': timezone.now().isoformat(),
    })


@api_view(['POST'])
@permission_classes([IsSuperAdmin])
def admin_reset_password_view(request, user_id):
    """POST /api/v1/admin/users/:id/reset-password"""
    try:
        user = User.objects.get(id=user_id)
    except User.DoesNotExist:
        return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)

    # 保护 admin 账号密码不被其他超管重置
    if user.username == 'admin' and request.user.username != 'admin':
        return Response({'error': '不能重置超级管理员的密码'}, status=status.HTTP_403_FORBIDDEN)

    new_password = request.data.get('new_password', '')
    if len(new_password) < 8:
        return Response({'error': '密码至少8位'}, status=status.HTTP_400_BAD_REQUEST)

    user.set_password(new_password)
    user.must_change_password = True
    user.save(update_fields=['password', 'must_change_password'])
    log_admin_action(request, 'user_password_reset', 'user', target_id=user.id, target_name=user.username)

    return Response({'message': f'已重置 {user.username} 的密码'})


@api_view(['PATCH'])
@permission_classes([IsSuperAdmin])
def admin_user_username_update_view(request, user_id):
    """PATCH /api/v1/admin/users/<id>/username — 超管修改任意用户的用户名。"""
    try:
        user = User.objects.get(id=user_id)
    except User.DoesNotExist:
        return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)

    # admin 账号保护:用户名不可修改(无论操作者是谁)
    if user.username == 'admin':
        return Response({'error': '不能修改超级管理员的用户名'}, status=status.HTTP_403_FORBIDDEN)

    new_username = (request.data.get('username') or '').strip()
    # 长度按 UTF-8 字节计:ASCII 1 字节、中文 3 字节;3-20 字节 ≈ 3-20 个英文字符 或 ~1-6 个中文字符
    new_bytes = len(new_username.encode('utf-8'))
    if not (3 <= new_bytes <= 20):
        return Response({'error': '用户名长度需 3-20 个字符'}, status=status.HTTP_400_BAD_REQUEST)
    if new_username == user.username:
        return Response({'error': '新用户名与原用户名相同'}, status=status.HTTP_400_BAD_REQUEST)
    if User.objects.filter(username=new_username).exclude(id=user.id).exists():
        return Response({'error': '该用户名已被占用'}, status=status.HTTP_400_BAD_REQUEST)

    from django.core.exceptions import ValidationError as DjangoValidationError
    old_username = user.username
    user.username = new_username
    try:
        user.full_clean(exclude=['password'])
    except DjangoValidationError:
        return Response({'error': '用户名包含非法字符'}, status=status.HTTP_400_BAD_REQUEST)
    user.save(update_fields=['username'])

    log_admin_action(
        request, 'user_username_update', 'user',
        target_id=user.id, target_name=new_username,
        before={'username': old_username},
        after={'username': new_username},
    )
    return Response({'user_id': user.id, 'username': user.username})


@api_view(['PATCH'])
@permission_classes([IsSuperAdmin])
def admin_user_observer_toggle_view(request, user_id):
    """PATCH /api/v1/admin/users/<id>/observer — 仅超管,把团管标记为观察者(或取消)。"""
    try:
        target = User.objects.get(id=user_id)
    except User.DoesNotExist:
        return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)

    # 只允许给「团队管理员」打观察者标记;超管/普通成员一律拒
    if target.is_staff and target.team_id is None:
        return Response({'error': '超级管理员无需设观察者'}, status=status.HTTP_400_BAD_REQUEST)
    if not (target.is_team_admin and target.team_id is not None):
        return Response({'error': '观察者标记只能给团队管理员'}, status=status.HTTP_400_BAD_REQUEST)

    is_observer = request.data.get('is_observer')
    if is_observer is None:
        return Response({'error': '请提供 is_observer 参数'}, status=status.HTTP_400_BAD_REQUEST)
    new_val = bool(is_observer)
    old_val = target.is_observer
    if old_val == new_val:
        return Response({'user_id': target.id, 'username': target.username, 'is_observer': new_val})

    target.is_observer = new_val
    target.save(update_fields=['is_observer'])

    log_admin_action(
        request, 'user_observer_toggle', 'user',
        target_id=target.id, target_name=target.username,
        before={'is_observer': old_val},
        after={'is_observer': new_val},
    )
    return Response({'user_id': target.id, 'username': target.username, 'is_observer': new_val})


@api_view(['POST'])
@permission_classes([IsSuperAdmin])
def admin_create_user_view(request):
    """POST /api/v1/admin/users/create — Super admin creates a user."""
    serializer = AdminCreateUserSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    username = serializer.validated_data['username']
    email = serializer.validated_data['email']

    if User.objects.filter(username=username).exists():
        return Response({'error': '用户名已存在'}, status=status.HTTP_400_BAD_REQUEST)
    if User.objects.filter(email=email).exists():
        return Response({'error': '邮箱已存在'}, status=status.HTTP_400_BAD_REQUEST)

    user = User.objects.create_user(
        username=username,
        email=email,
        password=serializer.validated_data['password'],
        daily_seconds_limit=serializer.validated_data['daily_seconds_limit'],
        monthly_seconds_limit=serializer.validated_data['monthly_seconds_limit'],
        daily_generation_limit=serializer.validated_data['daily_generation_limit'],
        monthly_generation_limit=serializer.validated_data['monthly_generation_limit'],
        is_staff=serializer.validated_data['is_staff'],
    )
    log_admin_action(request, 'user_create', 'user', target_id=user.id, target_name=user.username,
                     after={'username': user.username, 'email': user.email, 'is_staff': user.is_staff})

    return Response({
        'id': user.id,
        'username': user.username,
        'email': user.email,
        'is_active': user.is_active,
        'is_staff': user.is_staff,
        'daily_seconds_limit': user.daily_seconds_limit,
        'monthly_seconds_limit': user.monthly_seconds_limit,
        'daily_generation_limit': user.daily_generation_limit,
        'monthly_generation_limit': user.monthly_generation_limit,
        'created_at': timezone.now().isoformat(),
    }, status=status.HTTP_201_CREATED)


# ──────────────────────────────────────────────
# Admin: Consumption Records
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsSuperAdmin])
def admin_records_view(request):
    """GET /api/v1/admin/records"""
    page = _safe_int(request.query_params.get('page', 1), 1)
    # CSV export may request up to 10000 records
    page_size = min(_safe_int(request.query_params.get('page_size', 20), 20), 10000)
    search = request.query_params.get('search', '').strip()
    start_date = request.query_params.get('start_date', '').strip()
    end_date = request.query_params.get('end_date', '').strip()
    team_id = request.query_params.get('team_id', '').strip()

    qs = GenerationRecord.objects.select_related('user', 'user__team').order_by('-created_at')

    if search:
        qs = qs.filter(user__username__icontains=search)
    if start_date:
        qs = qs.filter(created_at__date__gte=start_date)
    if end_date:
        qs = qs.filter(created_at__date__lte=end_date)
    if team_id:
        qs = qs.filter(user__team_id=_safe_int(team_id))

    total = qs.count()
    offset = (page - 1) * page_size
    records = _eval_qs(qs[offset:offset + page_size])

    results = []
    for r in records:
        results.append({
            'id': r.id,
            'created_at': r.created_at.isoformat(),
            'completed_at': r.completed_at.isoformat() if r.completed_at else None,
            'user_id': r.user_id,
            'username': r.user.username,
            'team_name': r.user.team.name if r.user.team else None,
            'seconds_consumed': r.seconds_consumed,
            'tokens_consumed': r.tokens_consumed,
            'cost_amount': float(r.cost_amount),
            'base_cost_amount': float(r.base_cost_amount),
            'prompt': r.prompt,
            'mode': r.mode,
            'model': r.model,
            'aspect_ratio': r.aspect_ratio,
            'resolution': r.resolution,
            'status': r.status,
            'error_message': r.error_message or '',
            'raw_error': r.raw_error or '',
            'reference_urls': r.reference_urls or [],
            'duration': r.duration,
            'seed': r.seed,
            'ark_task_id': r.ark_task_id or '',
            'result_url': r.result_url or '',
            'thumbnail_url': r.thumbnail_url or '',
            'api_prompt': r.api_prompt or '',
        })

    return Response({
        'total': total,
        'page': page,
        'page_size': page_size,
        'results': results,
    })


# ──────────────────────────────────────────────
# Team Admin: Consumption Records
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsTeamAdmin])
def team_records_view(request):
    """GET /api/v1/team/records — 团管查看本团队消费记录"""
    team = request.user.team
    page = _safe_int(request.query_params.get('page', 1), 1)
    # CSV export may request up to 10000 records
    page_size = min(_safe_int(request.query_params.get('page_size', 20), 20), 10000)
    search = request.query_params.get('search', '').strip()
    start_date = request.query_params.get('start_date', '').strip()
    end_date = request.query_params.get('end_date', '').strip()

    qs = GenerationRecord.objects.filter(
        user__team=team
    ).select_related('user').order_by('-created_at')

    if search:
        qs = qs.filter(user__username__icontains=search)
    if start_date:
        qs = qs.filter(created_at__date__gte=start_date)
    if end_date:
        qs = qs.filter(created_at__date__lte=end_date)

    total = qs.count()
    offset = (page - 1) * page_size
    records = _eval_qs(qs[offset:offset + page_size])

    results = []
    for r in records:
        results.append({
            'id': r.id,
            'created_at': r.created_at.isoformat(),
            'completed_at': r.completed_at.isoformat() if r.completed_at else None,
            'user_id': r.user_id,
            'username': r.user.username,
            'seconds_consumed': r.seconds_consumed,
            'tokens_consumed': r.tokens_consumed,
            'cost_amount': float(r.cost_amount),
            'prompt': r.prompt,
            'mode': r.mode,
            'model': r.model,
            'aspect_ratio': r.aspect_ratio,
            'resolution': r.resolution,
            'status': r.status,
            'error_message': r.error_message or '',
            'raw_error': r.raw_error or '',
            'reference_urls': r.reference_urls or [],
            'duration': r.duration,
            'seed': r.seed,
            'ark_task_id': r.ark_task_id or '',
            'result_url': r.result_url or '',
            'thumbnail_url': r.thumbnail_url or '',
            'api_prompt': r.api_prompt or '',
        })

    return Response({
        'total': total,
        'page': page,
        'page_size': page_size,
        'results': results,
    })


# ──────────────────────────────────────────────
# Admin: System Settings
# ──────────────────────────────────────────────

def _settings_dict(config):
    """QuotaConfig → dict for API response."""
    return {
        'default_daily_seconds_limit': config.default_daily_seconds_limit,
        'default_monthly_seconds_limit': config.default_monthly_seconds_limit,
        'default_daily_generation_limit': config.default_daily_generation_limit,
        'default_monthly_generation_limit': config.default_monthly_generation_limit,
        'base_token_price': float(config.base_token_price),
        'base_token_price_video': float(config.base_token_price_video),
        'base_token_price_fast': float(config.base_token_price_fast),
        'base_token_price_fast_video': float(config.base_token_price_fast_video),
        'base_token_price_1080p': float(config.base_token_price_1080p),
        'base_token_price_1080p_video': float(config.base_token_price_1080p_video),
        'announcement': config.announcement,
        'announcement_enabled': config.announcement_enabled,
        'max_desktop_sessions': config.max_desktop_sessions,
        'max_mobile_sessions': config.max_mobile_sessions,
        'anomaly_detection_enabled': config.anomaly_detection_enabled,
        'r1_enabled_default': config.r1_enabled_default,
        'r2_enabled_default': config.r2_enabled_default,
        'r2_window_seconds': config.r2_window_seconds,
        'r3_enabled_default': config.r3_enabled_default,
        'r3_window_seconds': config.r3_window_seconds,
        'r3_max_count': config.r3_max_count,
        'r4_enabled_default': config.r4_enabled_default,
        'r4_window_seconds': config.r4_window_seconds,
        'r4_city_count': config.r4_city_count,
        'r5_enabled_default': config.r5_enabled_default,
        'r5_days': config.r5_days,
        'r5_country_count': config.r5_country_count,
        'feishu_alert_mobiles': config.feishu_alert_mobiles,
        'sms_alert_mobiles': config.sms_alert_mobiles,
        'alert_cooldown_seconds': config.alert_cooldown_seconds,
    }


@api_view(['GET', 'PUT'])
@permission_classes([IsSuperAdmin])
def admin_settings_view(request):
    """GET/PUT /api/v1/admin/settings"""
    config, _ = QuotaConfig.objects.get_or_create(pk=1)

    if request.method == 'GET':
        return Response(_settings_dict(config))

    serializer = SystemSettingsSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    before = _settings_dict(config)
    for field in serializer.validated_data:
        setattr(config, field, serializer.validated_data[field])
    config.save()
    log_admin_action(request, 'settings_update', 'settings', target_name='系统设置',
                     before=before, after=_settings_dict(config))

    result = _settings_dict(config)
    result['updated_at'] = config.updated_at.isoformat()
    return Response(result)


# ──────────────────────────────────────────────
# Admin: Anomaly Detection
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsSuperAdmin])
def admin_login_anomalies_view(request):
    """GET /api/v1/admin/anomalies — Login anomaly records list."""
    page = _safe_int(request.query_params.get('page', 1), 1)
    page_size = min(_safe_int(request.query_params.get('page_size', 20), 20), 100)
    team_id = request.query_params.get('team_id', '').strip()
    rule = request.query_params.get('rule', '').strip()
    level = request.query_params.get('level', '').strip()
    start_date = request.query_params.get('start_date', '').strip()
    end_date = request.query_params.get('end_date', '').strip()

    qs = LoginAnomaly.objects.select_related('team', 'user', 'login_record').all()

    if team_id:
        qs = qs.filter(team_id=_safe_int(team_id))
    if rule:
        qs = qs.filter(rule=rule)
    if level:
        qs = qs.filter(level=level)
    if start_date:
        qs = qs.filter(created_at__date__gte=start_date)
    if end_date:
        qs = qs.filter(created_at__date__lte=end_date)

    total = qs.count()
    offset = (page - 1) * page_size
    anomalies = list(qs[offset:offset + page_size])

    results = []
    for a in anomalies:
        record = a.login_record
        results.append({
            'id': a.id,
            'team_id': a.team_id,
            'team_name': a.team.name if a.team else '',
            'user_id': a.user_id,
            'username': a.user.username if a.user else '',
            'level': a.level,
            'rule': a.rule,
            'detail': a.detail,
            'alerted': a.alerted,
            'auto_disabled': a.auto_disabled,
            'disabled_target': a.disabled_target,
            'ip_address': record.ip_address if record else '',
            'geo_country': record.geo_country if record else '',
            'geo_province': record.geo_province if record else '',
            'geo_city': record.geo_city if record else '',
            'created_at': a.created_at.isoformat(),
        })

    return Response({
        'total': total,
        'page': page,
        'page_size': page_size,
        'total_pages': (total + page_size - 1) // page_size,
        'results': results,
    })


@api_view(['POST'])
@permission_classes([IsSuperAdmin])
def admin_test_feishu_view(request):
    """POST /api/v1/admin/test-feishu — Send a test Feishu alert."""
    mobile = request.data.get('mobile', '').strip()
    if not mobile:
        return Response({'error': '请输入手机号'}, status=status.HTTP_400_BAD_REQUEST)

    from utils.alert_service import send_feishu_test
    success, message = send_feishu_test(mobile)
    if success:
        return Response({'message': message})
    return Response({'error': message}, status=status.HTTP_400_BAD_REQUEST)


@api_view(['POST'])
@permission_classes([IsSuperAdmin])
def admin_test_sms_view(request):
    """POST /api/v1/admin/test-sms — Send a test SMS alert."""
    mobile = request.data.get('mobile', '').strip()
    if not mobile:
        return Response({'error': '请输入手机号'}, status=status.HTTP_400_BAD_REQUEST)

    from utils.alert_service import send_sms_test
    success, message = send_sms_test(mobile)
    if success:
        return Response({'message': message})
    return Response({'error': message}, status=status.HTTP_400_BAD_REQUEST)


@api_view(['POST'])
@permission_classes([IsSuperAdmin])
def admin_team_auto_learn_view(request, team_id):
    """POST /api/v1/admin/teams/<id>/auto-learn — Auto-learn expected regions from login history."""
    try:
        team = Team.objects.get(id=team_id)
    except Team.DoesNotExist:
        return Response({'error': '团队不存在'}, status=status.HTTP_404_NOT_FOUND)

    from apps.accounts.models import LoginRecord
    days = int(request.data.get('days', 30))
    min_count = int(request.data.get('min_count', 3))
    since = timezone.now() - timedelta(days=days)

    # Aggregate domestic cities with at least min_count logins
    from django.db.models import Count
    city_stats = (
        LoginRecord.objects.filter(
            team=team,
            created_at__gte=since,
            geo_country='中国',
        )
        .exclude(geo_city='')
        .values('geo_city')
        .annotate(cnt=Count('id'))
        .filter(cnt__gte=min_count)
        .order_by('-cnt')
    )

    cities = [row['geo_city'] for row in city_stats]

    return Response({
        'team_id': team.id,
        'team_name': team.name,
        'learned_cities': cities,
        'days': days,
        'min_count': min_count,
        'current_expected_regions': team.expected_regions,
    })


@api_view(['POST'])
@permission_classes([IsSuperAdmin])
def admin_team_apply_learned_regions_view(request, team_id):
    """POST /api/v1/admin/teams/<id>/apply-learned-regions — Apply auto-learned regions."""
    try:
        team = Team.objects.get(id=team_id)
    except Team.DoesNotExist:
        return Response({'error': '团队不存在'}, status=status.HTTP_404_NOT_FOUND)

    cities = request.data.get('cities', [])
    if not isinstance(cities, list):
        return Response({'error': 'cities 必须是数组'}, status=status.HTTP_400_BAD_REQUEST)

    before = team.expected_regions
    team.expected_regions = ','.join(cities)
    team.save(update_fields=['expected_regions'])
    log_admin_action(request, 'team_update', 'team', target_id=team.id, target_name=team.name,
                     before={'expected_regions': before},
                     after={'expected_regions': team.expected_regions})

    return Response({
        'team_id': team.id,
        'expected_regions': team.expected_regions,
    })


# ──────────────────────────────────────────────
# Admin: Audit Logs
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsSuperAdmin])
def admin_audit_logs_view(request):
    """GET /api/v1/admin/logs — Query admin audit logs."""
    page = _safe_int(request.query_params.get('page', 1), 1)
    page_size = min(_safe_int(request.query_params.get('page_size', 20), 20), 100)
    action = request.query_params.get('action', '').strip()
    operator = request.query_params.get('operator', '').strip()
    start_date = request.query_params.get('start_date', '').strip()
    end_date = request.query_params.get('end_date', '').strip()

    qs = AdminAuditLog.objects.select_related('operator').all()

    if action:
        qs = qs.filter(action=action)
    if operator:
        qs = qs.filter(operator_name__icontains=operator)
    if start_date:
        qs = qs.filter(created_at__date__gte=start_date)
    if end_date:
        qs = qs.filter(created_at__date__lte=end_date)

    total = qs.count()
    offset = (page - 1) * page_size
    logs = list(qs[offset:offset + page_size])

    results = []
    for log in logs:
        results.append({
            'id': log.id,
            'operator_name': log.operator_name,
            'action': log.action,
            'action_display': log.get_action_display(),
            'target_type': log.target_type,
            'target_id': log.target_id,
            'target_name': log.target_name,
            'before': log.before,
            'after': log.after,
            'ip_address': log.ip_address,
            'created_at': log.created_at.isoformat(),
        })

    return Response({
        'total': total,
        'page': page,
        'page_size': page_size,
        'total_pages': (total + page_size - 1) // page_size,
        'results': results,
    })


# ──────────────────────────────────────────────
# Public: Announcement
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def announcement_view(request):
    """GET /api/v1/announcement — 返回当前用户最新未读公告(从 Notification 表)。

    兼容老前端的响应结构(announcement/enabled/is_read)。
    """
    from apps.notifications.models import Notification
    latest = Notification.objects.filter(
        recipient=request.user, type='announcement', is_read=False
    ).order_by('-created_at').first()
    if not latest:
        return Response({
            'announcement': '',
            'enabled': False,
            'is_read': True,
            'notification_id': None,
        })
    return Response({
        'announcement': latest.content,
        'enabled': True,
        'is_read': False,
        'notification_id': latest.id,
        'updated_at': latest.created_at.isoformat(),
    })


@api_view(['POST'])
@permission_classes([IsAuthenticated])
def announcement_read_view(request):
    """POST /api/v1/announcement/read — 标记当前用户所有未读公告已读。"""
    from apps.notifications.models import Notification
    updated = Notification.objects.filter(
        recipient=request.user, type='announcement', is_read=False
    ).update(is_read=True)
    return Response({'ok': True, 'updated': updated})


@api_view(['POST'])
@permission_classes([IsSuperAdmin])
def admin_publish_announcement_view(request):
    """POST /api/v1/admin/announcement/publish — 超管点【发送公告】fan-out 给所有用户。

    Body: { "content": "<html>...</html>" }

    所有用户(含封禁,is_active=False 的用户解封后能看到累积的历史公告)。
    用 bulk_create(batch_size=500) 防大团队 OOM。
    同步把 content 写回 QuotaConfig.announcement 作为"当前最新公告"草稿存档,
    超管下次进设置页能看到上次发的内容(便于改动后再发)。
    """
    from apps.notifications.models import Notification
    User = get_user_model()

    content = (request.data.get('content') or '').strip()
    if not content:
        return Response({'error': '公告内容不能为空'}, status=status.HTTP_400_BAD_REQUEST)

    # 存档到 QuotaConfig(作为编辑器数据源,不再控制 fan-out)
    config, _ = QuotaConfig.objects.get_or_create(pk=1)
    config.announcement = content
    config.announcement_enabled = True  # 字段保留兼容,但前端不再读取
    config.save(update_fields=['announcement', 'announcement_enabled'])

    # fan-out 给所有用户(含封禁)
    user_ids = list(User.objects.all().values_list('id', flat=True))
    notifs = [Notification(
        recipient_id=uid,
        type='announcement',
        title='系统公告',
        content=content,
        link_url='',
        is_read=False,
    ) for uid in user_ids]
    Notification.objects.bulk_create(notifs, batch_size=500)

    log_admin_action(request, 'settings_update', 'system', target_id=0, target_name='announcement',
                     after={'recipients': len(notifs), 'content_preview': content[:80]})
    return Response({'sent_to': len(notifs), 'message': f'已发送给 {len(notifs)} 个用户'})


# ──────────────────────────────────────────────
# Team Admin: Team Management
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsTeamAdmin])
def team_info_view(request):
    """GET /api/v1/team/info — Team basic info for team admin."""
    team = request.user.team
    today = timezone.now().date()
    first_of_month = today.replace(day=1)

    monthly_used = GenerationRecord.objects.filter(
        user__team=team,
        created_at__date__gte=first_of_month,
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    monthly_spent = GenerationRecord.objects.filter(
        user__team=team,
        created_at__date__gte=first_of_month,
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    return Response({
        'id': team.id,
        'name': team.name,
        'total_seconds_pool': team.total_seconds_pool,
        'total_seconds_used': team.total_seconds_used,
        'remaining_seconds': team.remaining_seconds,
        'monthly_seconds_limit': team.monthly_seconds_limit,
        'monthly_seconds_used': monthly_used,
        'balance': float(team.balance),
        'total_spent': float(team.total_spent),
        'available_balance': float(team.available_balance),
        'monthly_spending_limit': float(team.monthly_spending_limit),
        'monthly_spent': float(monthly_spent),
        'frozen_amount': float(team.frozen_amount),
        'markup_percentage': float(team.markup_percentage),
        'daily_member_limit_default': team.daily_member_limit_default,
        'member_count': team.members.count(),
        'is_active': team.is_active,
    })


@api_view(['GET'])
@permission_classes([IsTeamAdmin])
def team_stats_view(request):
    """GET /api/v1/team/stats — Team consumption overview + member breakdown."""
    team = request.user.team
    today = timezone.now().date()
    first_of_month = today.replace(day=1)
    thirty_days_ago = today - timedelta(days=29)

    # Daily trend (cost + base_cost)
    daily_trend_qs = (
        GenerationRecord.objects
        .filter(user__team=team, created_at__date__gte=thirty_days_ago)
        .annotate(date=TruncDate('created_at'))
        .values('date')
        .annotate(
            seconds=Sum('seconds_consumed'),
            cost=Sum('cost_amount'),
            base_cost=Sum('base_cost_amount'),
        )
        .order_by('date')
    )
    trend_map = {str(item['date']): item for item in daily_trend_qs}
    daily_trend = []
    for i in range(30):
        d = thirty_days_ago + timedelta(days=i)
        item = trend_map.get(str(d), {})
        daily_trend.append({
            'date': str(d),
            'seconds': item.get('seconds') or 0,
            'cost': float(item.get('cost') or 0),
            'base_cost': float(item.get('base_cost') or 0),
        })

    # Member consumption this month
    members = team.members.annotate(
        seconds_this_month=Sum(
            'generation_records__seconds_consumed',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
        cost_this_month=Sum(
            'generation_records__cost_amount',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
        generations_this_month=Count(
            'generation_records',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
    ).filter(generations_this_month__gt=0).order_by('-cost_this_month')

    return Response({
        'daily_trend': daily_trend,
        'member_consumption': [
            {
                'user_id': m.id, 'username': m.username,
                'seconds_consumed': m.seconds_this_month or 0,
                'cost_consumed': float(m.cost_this_month or 0),
                'generation_count': m.generations_this_month or 0,
            }
            for m in members
        ],
    })


@api_view(['GET'])
@permission_classes([IsTeamAdmin])
def team_members_list_view(request):
    """GET /api/v1/team/members — List team members."""
    team = request.user.team
    today = timezone.now().date()
    first_of_month = today.replace(day=1)

    members = team.members.annotate(
        seconds_today=Sum(
            'generation_records__seconds_consumed',
            filter=Q(generation_records__created_at__date=today),
        ),
        seconds_this_month=Sum(
            'generation_records__seconds_consumed',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
        generations_today=Count(
            'generation_records',
            filter=Q(generation_records__created_at__date=today),
        ),
        generations_this_month=Count(
            'generation_records',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
        spent_today=Sum(
            'generation_records__cost_amount',
            filter=Q(generation_records__created_at__date=today),
        ),
        spent_this_month=Sum(
            'generation_records__cost_amount',
            filter=Q(generation_records__created_at__date__gte=first_of_month),
        ),
        is_online=Exists(ActiveSession.objects.filter(user_id=OuterRef('pk'))),
        total_spent_all=Sum(
            'generation_records__cost_amount',
            filter=Q(generation_records__status='completed'),
        ),
    ).order_by('-date_joined')

    return Response({
        'results': [{
            'id': m.id,
            'username': m.username,
            'email': m.email,
            'is_team_admin': m.is_team_admin,
            'is_team_owner': m.is_team_owner,
            'is_active': m.is_active,
            'daily_seconds_limit': m.daily_seconds_limit,
            'monthly_seconds_limit': m.monthly_seconds_limit,
            'daily_generation_limit': m.daily_generation_limit,
            'monthly_generation_limit': m.monthly_generation_limit,
            'seconds_today': m.seconds_today or 0,
            'seconds_this_month': m.seconds_this_month or 0,
            'generations_today': m.generations_today or 0,
            'generations_this_month': m.generations_this_month or 0,
            'spent_today': float(m.spent_today or 0),
            'spent_this_month': float(m.spent_this_month or 0),
            'spending_limit': float(m.spending_limit),
            'total_spent': float(m.total_spent_all or 0),
            'is_online': m.is_online,
            'date_joined': m.date_joined.isoformat(),
        } for m in members],
    })


@api_view(['POST'])
@permission_classes([IsTeamAdmin])
def team_member_create_view(request):
    """POST /api/v1/team/members/create — Team admin creates a member."""
    serializer = TeamMemberCreateSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    team = request.user.team
    username = serializer.validated_data['username']

    if User.objects.filter(username=username).exists():
        return Response({'error': '用户名已存在'}, status=status.HTTP_400_BAD_REQUEST)

    daily = serializer.validated_data.get('daily_seconds_limit', team.daily_member_limit_default)
    monthly = serializer.validated_data.get('monthly_seconds_limit', -1)

    # Generation count limits
    config = QuotaConfig.objects.get_or_create(pk=1)[0]
    daily_gen = serializer.validated_data.get('daily_generation_limit', config.default_daily_generation_limit)
    monthly_gen = serializer.validated_data.get('monthly_generation_limit', config.default_monthly_generation_limit)

    # Generate email from username (team members may not need real email)
    email = f'{username}@team.local'
    if User.objects.filter(email=email).exists():
        email = f'{username}_{team.id}@team.local'

    user = User.objects.create_user(
        username=username,
        email=email,
        password=serializer.validated_data['password'],
        team=team,
        is_team_admin=False,  # Cannot escalate privileges
        daily_seconds_limit=daily,
        monthly_seconds_limit=monthly,
        daily_generation_limit=daily_gen,
        monthly_generation_limit=monthly_gen,
    )
    log_admin_action(request, 'member_create', 'user', target_id=user.id, target_name=user.username,
                     after={'username': user.username, 'team': team.name})

    return Response({
        'id': user.id,
        'username': user.username,
        'team': team.name,
        'daily_seconds_limit': user.daily_seconds_limit,
        'monthly_seconds_limit': user.monthly_seconds_limit,
        'daily_generation_limit': user.daily_generation_limit,
        'monthly_generation_limit': user.monthly_generation_limit,
    }, status=status.HTTP_201_CREATED)


@api_view(['GET'])
@permission_classes([IsTeamAdmin])
def team_member_detail_view(request, member_id):
    """GET /api/v1/team/members/<id> — Member detail + recent records."""
    team = request.user.team
    try:
        member = team.members.get(id=member_id)
    except User.DoesNotExist:
        return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)

    today = timezone.now().date()
    first_of_month = today.replace(day=1)

    seconds_today = member.generation_records.filter(
        created_at__date=today
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    seconds_this_month = member.generation_records.filter(
        created_at__date__gte=first_of_month
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    generations_today = member.generation_records.filter(
        created_at__date=today
    ).count()

    generations_this_month = member.generation_records.filter(
        created_at__date__gte=first_of_month
    ).count()

    spent_today = member.generation_records.filter(
        created_at__date=today
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    spent_this_month = member.generation_records.filter(
        created_at__date__gte=first_of_month
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    recent_records = _eval_qs(member.generation_records.order_by('-created_at'), limit=20)

    return Response({
        'id': member.id,
        'username': member.username,
        'is_active': member.is_active,
        'is_team_admin': member.is_team_admin,
        'daily_seconds_limit': member.daily_seconds_limit,
        'monthly_seconds_limit': member.monthly_seconds_limit,
        'daily_generation_limit': member.daily_generation_limit,
        'monthly_generation_limit': member.monthly_generation_limit,
        'seconds_today': seconds_today,
        'seconds_this_month': seconds_this_month,
        'generations_today': generations_today,
        'generations_this_month': generations_this_month,
        'spent_today': float(spent_today),
        'spent_this_month': float(spent_this_month),
        'recent_records': [
            {
                'id': r.id,
                'created_at': r.created_at.isoformat(),
                'seconds_consumed': r.seconds_consumed,
                'tokens_consumed': r.tokens_consumed,
                'cost_amount': float(r.cost_amount),
                'prompt': r.prompt,
                'mode': r.mode,
                'model': r.model,
                'status': r.status,
                'error_message': r.error_message or '',
            }
            for r in recent_records
        ],
    })


@api_view(['PUT'])
@permission_classes([IsTeamAdmin])
def team_member_quota_view(request, member_id):
    """PUT /api/v1/team/members/<id>/quota — Set member daily/monthly limit."""
    team = request.user.team
    try:
        member = team.members.get(id=member_id)
    except User.DoesNotExist:
        return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)

    # 副管不能修改主管或其他副管的额度
    if not request.user.is_team_owner and member.is_team_admin:
        return Response({'error': '副管理员不能修改其他管理员的额度'}, status=status.HTTP_403_FORBIDDEN)

    serializer = MemberQuotaSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    before = {
        'daily_generation_limit': member.daily_generation_limit,
        'monthly_generation_limit': member.monthly_generation_limit,
        'spending_limit': float(member.spending_limit),
    }
    update_fields = ['daily_generation_limit', 'monthly_generation_limit']
    member.daily_generation_limit = serializer.validated_data['daily_generation_limit']
    member.monthly_generation_limit = serializer.validated_data['monthly_generation_limit']
    if 'spending_limit' in serializer.validated_data:
        member.spending_limit = serializer.validated_data['spending_limit']
        update_fields.append('spending_limit')
    member.save(update_fields=update_fields)
    log_admin_action(request, 'member_quota_update', 'user', target_id=member.id, target_name=member.username,
                     before=before,
                     after={
                         'daily_generation_limit': member.daily_generation_limit,
                         'monthly_generation_limit': member.monthly_generation_limit,
                         'spending_limit': float(member.spending_limit),
                     })

    return Response({
        'user_id': member.id,
        'username': member.username,
        'daily_generation_limit': member.daily_generation_limit,
        'monthly_generation_limit': member.monthly_generation_limit,
        'spending_limit': float(member.spending_limit),
        'daily_seconds_limit': member.daily_seconds_limit,
        'monthly_seconds_limit': member.monthly_seconds_limit,
    })


@api_view(['PATCH'])
@permission_classes([IsTeamAdmin])
def team_member_status_view(request, member_id):
    """PATCH /api/v1/team/members/<id>/status — Enable/disable member."""
    team = request.user.team
    try:
        member = team.members.get(id=member_id)
    except User.DoesNotExist:
        return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)

    # Cannot disable yourself
    if member.id == request.user.id:
        return Response({'error': '不能停用自己的账号'}, status=status.HTTP_400_BAD_REQUEST)
    # 副管不能禁用主管或其他副管
    if not request.user.is_team_owner and member.is_team_admin:
        return Response({'error': '副管理员不能操作其他管理员'}, status=status.HTTP_403_FORBIDDEN)
    # 主管不能被副管禁用
    if member.is_team_owner and not request.user.is_team_owner:
        return Response({'error': '不能停用主管理员'}, status=status.HTTP_403_FORBIDDEN)

    serializer = UserStatusSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    old_active = member.is_active
    member.is_active = serializer.validated_data['is_active']
    member.save(update_fields=['is_active'])
    log_admin_action(request, 'member_status_toggle', 'user', target_id=member.id, target_name=member.username,
                     before={'is_active': old_active}, after={'is_active': member.is_active})

    return Response({
        'user_id': member.id,
        'username': member.username,
        'is_active': member.is_active,
    })


@api_view(['PATCH'])
@permission_classes([IsTeamAdmin])
def team_member_role_view(request, member_id):
    """PATCH /api/v1/team/members/<id>/role — Owner toggles vice admin."""
    if not request.user.is_team_owner:
        return Response({'error': '只有主管理员可以设置副管理员'}, status=status.HTTP_403_FORBIDDEN)

    team = request.user.team
    try:
        member = team.members.get(id=member_id)
    except User.DoesNotExist:
        return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)

    if member.id == request.user.id:
        return Response({'error': '不能修改自己的角色'}, status=status.HTTP_400_BAD_REQUEST)
    if member.is_team_owner:
        return Response({'error': '不能修改主管理员的角色'}, status=status.HTTP_403_FORBIDDEN)

    is_admin = request.data.get('is_team_admin')
    if is_admin is None:
        return Response({'error': '请提供 is_team_admin 参数'}, status=status.HTTP_400_BAD_REQUEST)

    before = {'is_team_admin': member.is_team_admin}
    member.is_team_admin = bool(is_admin)
    member.save(update_fields=['is_team_admin'])
    log_admin_action(request, 'member_quota_update', 'user', target_id=member.id, target_name=member.username,
                     before=before, after={'is_team_admin': member.is_team_admin})

    return Response({
        'user_id': member.id,
        'username': member.username,
        'is_team_admin': member.is_team_admin,
    })


@api_view(['POST'])
@permission_classes([IsTeamAdmin])
def team_reset_member_password_view(request, member_id):
    """POST /api/v1/team/members/<id>/reset-password — 团管重置成员密码。

    权限矩阵(必须服务端硬校验,前端按钮只是 UX):
    - 主管 (is_team_owner=True): 可改同团队的「副管 + 成员」,不可改其他主管
    - 副管 (is_team_admin=True && !is_team_owner): 只能改同团队的「成员」,不可改副管/主管

    随机生成 8 位密码 + must_change_password=True(成员下次登录强制改密)。
    """
    import secrets
    import string

    team = request.user.team
    if team is None:
        return Response({'error': '当前用户没有团队'}, status=status.HTTP_400_BAD_REQUEST)

    try:
        target = team.members.get(id=member_id)
    except User.DoesNotExist:
        return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)

    operator = request.user

    # 防御性校验:即使 team.members 已过滤,operator/target 跨团队中转改动也兜底
    if target.team_id != operator.team_id:
        return Response({'error': '不在同一团队'}, status=status.HTTP_403_FORBIDDEN)

    # 自己不能重置自己(用修改密码功能)
    if target.id == operator.id:
        return Response({'error': '不能重置自己的密码,请用「修改密码」功能'}, status=status.HTTP_400_BAD_REQUEST)

    # 任何团管都不能改主管密码 — 主管密码必须超管重置(走 admin_reset_password_view)
    if target.is_team_owner:
        return Response({'error': '主管理员密码须由超级管理员重置'}, status=status.HTTP_403_FORBIDDEN)

    # 副管密码只有主管能重置,其他副管不行
    if target.is_team_admin and not operator.is_team_owner:
        return Response({'error': '只有主管理员能重置副管理员密码'}, status=status.HTTP_403_FORBIDDEN)

    # 走到这里:operator 是主管或副管;target 要么是副管(operator 必是主管) 要么是普通成员
    alphabet = string.ascii_letters + string.digits
    new_password = ''.join(secrets.choice(alphabet) for _ in range(8))

    target.set_password(new_password)
    target.must_change_password = True
    target.save(update_fields=['password', 'must_change_password'])

    log_admin_action(
        request, 'user_password_reset', 'user',
        target_id=target.id, target_name=target.username,
        after={'reset_by': 'team_admin', 'operator': operator.username},
    )

    return Response({
        'user_id': target.id,
        'username': target.username,
        'new_password': new_password,
        'message': f'已重置 {target.username} 的密码,下次登录需修改',
    })


@api_view(['PATCH'])
@permission_classes([IsTeamAdmin])
def team_member_username_update_view(request, member_id):
    """PATCH /api/v1/team/members/<id>/username — 团管修改本团队成员用户名。

    权限矩阵(同 team_reset_member_password_view):
    - 主管 (is_team_owner=True): 可改同团队的副管 + 成员的用户名
    - 副管 (is_team_admin=True && !is_team_owner): 只能改同团队成员的用户名
    """
    team = request.user.team
    if team is None:
        return Response({'error': '当前用户没有团队'}, status=status.HTTP_400_BAD_REQUEST)

    try:
        target = team.members.get(id=member_id)
    except User.DoesNotExist:
        return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)

    operator = request.user

    # 防御性兜底
    if target.team_id != operator.team_id:
        return Response({'error': '不在同一团队'}, status=status.HTTP_403_FORBIDDEN)
    if target.id == operator.id:
        return Response({'error': '不能修改自己的用户名'}, status=status.HTTP_400_BAD_REQUEST)
    if target.username == 'admin':
        return Response({'error': '不能修改超级管理员的用户名'}, status=status.HTTP_403_FORBIDDEN)
    if target.is_team_owner:
        return Response({'error': '不能修改主管理员的用户名'}, status=status.HTTP_403_FORBIDDEN)
    if target.is_team_admin and not operator.is_team_owner:
        return Response({'error': '只有主管理员能修改副管理员的用户名'}, status=status.HTTP_403_FORBIDDEN)

    new_username = (request.data.get('username') or '').strip()
    # 长度按 UTF-8 字节计:ASCII 1 字节、中文 3 字节;3-20 字节 ≈ 3-20 个英文字符 或 ~1-6 个中文字符
    new_bytes = len(new_username.encode('utf-8'))
    if not (3 <= new_bytes <= 20):
        return Response({'error': '用户名长度需 3-20 个字符'}, status=status.HTTP_400_BAD_REQUEST)
    if new_username == target.username:
        return Response({'error': '新用户名与原用户名相同'}, status=status.HTTP_400_BAD_REQUEST)
    if User.objects.filter(username=new_username).exclude(id=target.id).exists():
        return Response({'error': '该用户名已被占用'}, status=status.HTTP_400_BAD_REQUEST)

    from django.core.exceptions import ValidationError as DjangoValidationError
    old_username = target.username
    target.username = new_username
    try:
        target.full_clean(exclude=['password'])
    except DjangoValidationError:
        return Response({'error': '用户名包含非法字符'}, status=status.HTTP_400_BAD_REQUEST)
    target.save(update_fields=['username'])

    log_admin_action(
        request, 'user_username_update', 'user',
        target_id=target.id, target_name=new_username,
        before={'username': old_username},
        after={'username': new_username},
    )
    return Response({'user_id': target.id, 'username': target.username})


# ──────────────────────────────────────────────
# Profile: User's own consumption data
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def profile_overview_view(request):
    """GET /api/v1/profile/overview"""
    user = request.user
    today = timezone.now().date()
    first_of_month = today.replace(day=1)
    period = request.query_params.get('period', '7d')
    days = 30 if period == '30d' else 7
    start_date = today - timedelta(days=days - 1)

    daily_seconds_used = user.generation_records.filter(
        created_at__date=today
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    monthly_seconds_used = user.generation_records.filter(
        created_at__date__gte=first_of_month
    ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

    total_seconds_used = user.generation_records.aggregate(
        total=Sum('seconds_consumed')
    )['total'] or 0

    # Count-based usage
    daily_generation_used = user.generation_records.filter(
        created_at__date=today
    ).count()

    monthly_generation_used = user.generation_records.filter(
        created_at__date__gte=first_of_month
    ).count()

    # Spending
    daily_spent = user.generation_records.filter(
        created_at__date=today
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    monthly_spent = user.generation_records.filter(
        created_at__date__gte=first_of_month
    ).aggregate(total=Sum('cost_amount'))['total'] or 0

    total_spent = user.generation_records.aggregate(
        total=Sum('cost_amount')
    )['total'] or 0

    # Daily trend
    trend_qs = (
        user.generation_records
        .filter(created_at__date__gte=start_date)
        .annotate(date=TruncDate('created_at'))
        .values('date')
        .annotate(
            seconds=Sum('seconds_consumed'),
            cost=Sum('cost_amount'),
            count=Count('id'),
        )
        .order_by('date')
    )
    trend_map = {str(item['date']): item for item in trend_qs}
    daily_trend = []
    for i in range(days):
        d = start_date + timedelta(days=i)
        item = trend_map.get(str(d), {})
        daily_trend.append({
            'date': str(d),
            'seconds': item.get('seconds') or 0,
            'cost': float(item.get('cost') or 0),
            'count': item.get('count') or 0,
        })

    data = {
        'daily_seconds_limit': user.daily_seconds_limit,
        'daily_seconds_used': daily_seconds_used,
        'monthly_seconds_limit': user.monthly_seconds_limit,
        'monthly_seconds_used': monthly_seconds_used,
        'total_seconds_used': total_seconds_used,
        'daily_generation_limit': user.daily_generation_limit,
        'daily_generation_used': daily_generation_used,
        'monthly_generation_limit': user.monthly_generation_limit,
        'monthly_generation_used': monthly_generation_used,
        'daily_spent': float(daily_spent),
        'monthly_spent': float(monthly_spent),
        'total_spent': float(total_spent),
        'daily_trend': daily_trend,
    }

    # Include team info
    team = user.team
    if team:
        team_monthly_used = GenerationRecord.objects.filter(
            user__team=team,
            created_at__date__gte=first_of_month,
        ).aggregate(total=Sum('seconds_consumed'))['total'] or 0

        team_monthly_spent = GenerationRecord.objects.filter(
            user__team=team,
            created_at__date__gte=first_of_month,
        ).aggregate(total=Sum('cost_amount'))['total'] or 0

        data['team'] = {
            'name': team.name,
            'total_seconds_pool': team.total_seconds_pool,
            'total_seconds_used': team.total_seconds_used,
            'remaining_seconds': team.remaining_seconds,
            'monthly_seconds_limit': team.monthly_seconds_limit,
            'monthly_seconds_used': team_monthly_used,
            'balance': float(team.balance),
            'total_spent': float(team.total_spent),
            'available_balance': float(team.available_balance),
            'monthly_spending_limit': float(team.monthly_spending_limit),
            'monthly_spent': float(team_monthly_spent),
            'frozen_amount': float(team.frozen_amount),
        }

    return Response(data)


@api_view(['GET'])
@permission_classes([IsAuthenticated])
def profile_records_view(request):
    """GET /api/v1/profile/records"""
    user = request.user
    page = _safe_int(request.query_params.get('page', 1), 1)
    page_size = min(_safe_int(request.query_params.get('page_size', 20), 20), 100)

    qs = user.generation_records.order_by('-created_at')
    total = qs.count()
    offset = (page - 1) * page_size
    records = _eval_qs(qs[offset:offset + page_size])

    results = []
    for r in records:
        results.append({
            'id': r.id,
            'created_at': r.created_at.isoformat(),
            'completed_at': r.completed_at.isoformat() if r.completed_at else None,
            'seconds_consumed': r.seconds_consumed,
            'tokens_consumed': r.tokens_consumed,
            'cost_amount': float(r.cost_amount),
            'prompt': r.prompt,
            'mode': r.mode,
            'model': r.model,
            'aspect_ratio': r.aspect_ratio,
            'resolution': r.resolution,
            'status': r.status,
            'error_message': r.error_message or '',
            'result_url': r.result_url or '',
            'thumbnail_url': r.thumbnail_url or '',
        })

    return Response({
        'total': total,
        'page': page,
        'page_size': page_size,
        'results': results,
    })


# ──────────────────────────────────────────────
# Admin: Content Assets (hierarchical view)
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsSuperAdminOrObserver])
def admin_assets_overview(request):
    """GET /api/v1/admin/assets/overview — Global stats + per-team video/seconds summary."""
    from apps.accounts.models import Team

    teams = Team.objects.all().order_by('name')
    team_data = []
    total_videos = 0
    total_seconds = 0

    for team in teams:
        team_records = GenerationRecord.objects.filter(
            user__team=team, status='completed'
        )
        video_count = team_records.count()
        seconds_consumed = team_records.aggregate(total=Sum('seconds_consumed'))['total'] or 0
        total_videos += video_count
        total_seconds += seconds_consumed
        team_data.append({
            'id': team.id,
            'name': team.name,
            'video_count': video_count,
            'seconds_consumed': seconds_consumed,
            'member_count': team.members.count(),
            'is_active': team.is_active,
        })

    # Also count videos from users without a team
    no_team_records = GenerationRecord.objects.filter(
        user__team__isnull=True, status='completed'
    )
    no_team_count = no_team_records.count()
    no_team_seconds = no_team_records.aggregate(total=Sum('seconds_consumed'))['total'] or 0
    total_videos += no_team_count
    total_seconds += no_team_seconds

    return Response({
        'total_videos': total_videos,
        'total_seconds': total_seconds,
        'total_teams': teams.count(),
        'teams': team_data,
        'no_team': {
            'video_count': no_team_count,
            'seconds_consumed': no_team_seconds,
        },
    })


@api_view(['GET'])
@permission_classes([IsSuperAdminOrObserver])
def admin_assets_team_members(request, team_id):
    """GET /api/v1/admin/assets/team/<id>/members — Members of a team with video/seconds stats."""
    from apps.accounts.models import Team
    try:
        team = Team.objects.get(id=team_id)
    except Team.DoesNotExist:
        return Response({'error': '团队不存在'}, status=status.HTTP_404_NOT_FOUND)

    members = team.members.all().order_by('username')
    member_data = []
    total_videos = 0
    total_seconds = 0

    for member in members:
        records = member.generation_records.filter(status='completed')
        video_count = records.count()
        seconds_consumed = records.aggregate(total=Sum('seconds_consumed'))['total'] or 0
        total_videos += video_count
        total_seconds += seconds_consumed
        member_data.append({
            'id': member.id,
            'username': member.username,
            'is_team_admin': member.is_team_admin,
            'video_count': video_count,
            'seconds_consumed': seconds_consumed,
            'is_online': ActiveSession.objects.filter(user=member).exists(),
        })

    return Response({
        'team_id': team.id,
        'team_name': team.name,
        'total_videos': total_videos,
        'total_seconds': total_seconds,
        'member_count': len(member_data),
        'members': member_data,
    })


@api_view(['GET'])
@permission_classes([IsSuperAdminOrObserver])
def admin_assets_user_videos(request, user_id):
    """GET /api/v1/admin/assets/user/<id>/videos — Completed videos for a user (paginated)."""
    try:
        target_user = User.objects.get(id=user_id)
    except User.DoesNotExist:
        return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)

    page = _safe_int(request.query_params.get('page', 1), 1)
    page_size = min(_safe_int(request.query_params.get('page_size', 30), 30), 100)

    qs = target_user.generation_records.filter(status='completed').order_by('-created_at')
    total = qs.count()
    offset = (page - 1) * page_size
    records = _eval_qs(qs[offset:offset + page_size])

    results = []
    for r in records:
        results.append({
            'id': r.id,
            'task_id': str(r.task_id),
            'prompt': r.prompt,
            'result_url': r.result_url or '',
            'thumbnail_url': r.thumbnail_url or '',
            'duration': r.duration,
            'seconds_consumed': r.seconds_consumed,
            'aspect_ratio': r.aspect_ratio,
            'resolution': r.resolution,
            'reference_urls': r.reference_urls or [],
            'created_at': r.created_at.isoformat(),
        })

    return Response({
        'user_id': target_user.id,
        'username': target_user.username,
        'total': total,
        'page': page,
        'page_size': page_size,
        'results': results,
    })


# ──────────────────────────────────────────────
# Team Admin: Content Assets
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsTeamAdmin])
def team_assets_overview(request):
    """GET /api/v1/team/assets/overview — Team stats + per-member video/seconds summary."""
    team = request.user.team
    members = team.members.all().order_by('username')
    member_data = []
    total_videos = 0
    total_seconds = 0

    for member in members:
        records = member.generation_records.filter(status='completed')
        video_count = records.count()
        seconds_consumed = records.aggregate(total=Sum('seconds_consumed'))['total'] or 0
        total_videos += video_count
        total_seconds += seconds_consumed
        member_data.append({
            'id': member.id,
            'username': member.username,
            'is_team_admin': member.is_team_admin,
            'video_count': video_count,
            'seconds_consumed': seconds_consumed,
            'is_online': ActiveSession.objects.filter(user=member).exists(),
        })

    return Response({
        'team_id': team.id,
        'team_name': team.name,
        'total_videos': total_videos,
        'total_seconds': total_seconds,
        'member_count': len(member_data),
        'members': member_data,
    })


@api_view(['GET'])
@permission_classes([IsTeamAdmin])
def team_assets_member_videos(request, member_id):
    """GET /api/v1/team/assets/member/<id>/videos — Completed videos for a team member (paginated)."""
    team = request.user.team
    try:
        member = team.members.get(id=member_id)
    except User.DoesNotExist:
        return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND)

    page = _safe_int(request.query_params.get('page', 1), 1)
    page_size = min(_safe_int(request.query_params.get('page_size', 30), 30), 100)

    qs = member.generation_records.filter(status='completed').order_by('-created_at')
    total = qs.count()
    offset = (page - 1) * page_size
    records = _eval_qs(qs[offset:offset + page_size])

    results = []
    for r in records:
        results.append({
            'id': r.id,
            'task_id': str(r.task_id),
            'prompt': r.prompt,
            'result_url': r.result_url or '',
            'thumbnail_url': r.thumbnail_url or '',
            'duration': r.duration,
            'seconds_consumed': r.seconds_consumed,
            'aspect_ratio': r.aspect_ratio,
            'resolution': r.resolution,
            'reference_urls': r.reference_urls or [],
            'created_at': r.created_at.isoformat(),
        })

    return Response({
        'user_id': member.id,
        'username': member.username,
        'total': total,
        'page': page,
        'page_size': page_size,
        'results': results,
    })


# ──────────────────────────────────────────────
# Admin: Login Records
# ──────────────────────────────────────────────

@api_view(['GET'])
@permission_classes([IsSuperAdmin])
def admin_login_records_view(request):
    """GET /api/v1/admin/login-records"""
    page = _safe_int(request.query_params.get('page', 1), 1)
    page_size = min(_safe_int(request.query_params.get('page_size', 20), 20), 100)
    search = request.query_params.get('search', '').strip()
    team_id = request.query_params.get('team_id', '').strip()
    start_date = request.query_params.get('start_date', '').strip()
    end_date = request.query_params.get('end_date', '').strip()
    city = request.query_params.get('city', '').strip()

    qs = LoginRecord.objects.select_related('user', 'team').order_by('-created_at')

    if search:
        qs = qs.filter(user__username__icontains=search)
    if team_id:
        qs = qs.filter(team_id=_safe_int(team_id))
    if start_date:
        qs = qs.filter(created_at__date__gte=start_date)
    if end_date:
        qs = qs.filter(created_at__date__lte=end_date)
    if city:
        qs = qs.filter(geo_city__icontains=city)

    total = qs.count()
    offset = (page - 1) * page_size
    records = list(qs[offset:offset + page_size])

    results = []
    for r in records:
        results.append({
            'id': r.id,
            'username': r.user.username,
            'user_id': r.user_id,
            'team_name': r.team.name if r.team else None,
            'ip_address': r.ip_address or '',
            'geo_country': r.geo_country,
            'geo_province': r.geo_province,
            'geo_city': r.geo_city,
            'geo_source': r.geo_source,
            'user_agent': r.user_agent,
            'created_at': r.created_at.isoformat(),
        })

    return Response({
        'total': total,
        'page': page,
        'page_size': page_size,
        'results': results,
    })


# ──────────────────────────────────────────────
# Virtual Avatar Asset Library
# ──────────────────────────────────────────────

def _assets_api_call(func, *args, **kwargs):
    """Safely call an assets_client function; returns (result, error_response).

    If ASSETS_API_ENABLED is False the remote call is skipped and an empty
    placeholder is returned so that local DB records are still created.
    """
    from django.conf import settings as django_settings
    if not django_settings.ASSETS_API_ENABLED:
        return None, None
    try:
        from utils.assets_client import AssetsAPIError
        result = func(*args, **kwargs)
        return result, None
    except AssetsAPIError as e:
        logger.warning('Assets API error: %s', e)
        return None, Response(
            {'error': 'assets_api_error', 'message': e.user_message},
            status=status.HTTP_502_BAD_GATEWAY,
        )
    except Exception as e:
        logger.exception('Assets API unexpected error')
        return None, Response(
            {'error': 'assets_api_error', 'message': f'素材 API 调用失败: {e}'},
            status=status.HTTP_502_BAD_GATEWAY,
        )


def _detect_asset_type(file):
    """Detect asset type from file content_type. Returns ('Image'|'Video'|'Audio', error_response|None)."""
    ct = (file.content_type or '').lower()
    if ct.startswith('video/'):
        if ct not in ('video/mp4', 'video/quicktime'):
            return None, Response({'error': '仅支持 MP4 和 MOV 格式的视频'}, status=status.HTTP_400_BAD_REQUEST)
        if file.size > MAX_VIDEO_SIZE:
            return None, Response({'error': '视频文件不能超过 50MB'}, status=status.HTTP_400_BAD_REQUEST)
        return 'Video', None
    elif ct.startswith('audio/'):
        if ct not in ('audio/mpeg', 'audio/wav'):
            return None, Response({'error': '仅支持 MP3 和 WAV 格式的音频'}, status=status.HTTP_400_BAD_REQUEST)
        if file.size > MAX_AUDIO_SIZE:
            return None, Response({'error': '音频文件不能超过 15MB'}, status=status.HTTP_400_BAD_REQUEST)
        return 'Audio', None
    else:
        ext = file.name.rsplit('.', 1)[-1].lower() if '.' in file.name else ''
        if ext and ext not in ALLOWED_IMAGE_EXTS:
            return None, Response({'error': f'不支持的图片格式: {ext}'}, status=status.HTTP_400_BAD_REQUEST)
        if file.size > MAX_IMAGE_SIZE:
            return None, Response({'error': '图片文件不能超过 30MB'}, status=status.HTTP_400_BAD_REQUEST)
        return 'Image', None


@api_view(['GET', 'POST'])
@permission_classes([IsTeamMember])
@parser_classes([MultiPartParser, JSONParser])
def asset_groups_view(request):
    """GET  /api/v1/assets/groups — list groups for current team.
       POST /api/v1/assets/groups — create a group with an initial asset (image/video/audio).
    """
    team = request.user.team

    if request.method == 'GET':
        groups = list(
            AssetGroup.objects
            .filter(team=team)
            .annotate(asset_count=Count('assets'))
            .order_by('-created_at')
        )

        # 从火山同步素材组名字（一次 API 调用）
        remote_ids = [g.remote_group_id for g in groups if g.remote_group_id]
        if remote_ids:
            try:
                from utils.assets_client import list_asset_groups
                remote_items, _ = list_asset_groups(page=1, page_size=100)
                remote_name_map = {item['Id']: item.get('Name', '') for item in remote_items if 'Id' in item}
                for g in groups:
                    if g.remote_group_id and g.remote_group_id in remote_name_map:
                        remote_name = remote_name_map[g.remote_group_id]
                        if remote_name and remote_name != g.name:
                            g.name = remote_name
                            g.save(update_fields=['name'])
            except Exception:
                pass  # 同步失败不影响列表展示

        results = []
        for g in groups:
            results.append({
                'id': g.id,
                'name': g.name,
                'thumbnail_url': g.thumbnail_url if g.asset_count > 0 else '',
                'asset_count': g.asset_count,
                'remote_group_id': g.remote_group_id,
                'created_at': g.created_at.isoformat(),
            })
        return Response({'results': results})

    # ── POST: create group + first asset ──
    name = request.data.get('name', '').strip()
    if not name:
        return Response({'error': '请输入角色名称'}, status=status.HTTP_400_BAD_REQUEST)

    file = request.FILES.get('file')

    # Validate file BEFORE creating group (prevent orphan records)
    asset_type = None
    if file:
        asset_type, err = _detect_asset_type(file)
        if err:
            return err
        if asset_type == 'Image':
            try:
                from PIL import Image
                img = Image.open(file)
                w, h = img.size
                if w < 300 or h < 300:
                    return Response(
                        {'error': f'图片太小了（当前 {w}x{h}），宽高需在 300~6000 像素之间'},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
                if w > 6000 or h > 6000:
                    return Response(
                        {'error': f'图片太大了（当前 {w}x{h}），宽高需在 300~6000 像素之间'},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
                file.seek(0)
            except ImportError:
                pass
            except Exception:
                pass

    # Create remote group
    from utils import assets_client
    remote_group_id = ''
    result, err = _assets_api_call(assets_client.create_asset_group, name)
    if err:
        return err
    if result is not None:
        remote_group_id = result

    # Local DB group
    group = AssetGroup.objects.create(
        team=team,
        remote_group_id=remote_group_id,
        name=name,
        description='',
        thumbnail_url='',
        created_by=request.user,
    )

    # If file provided, create first asset (validation already done above)
    if file and asset_type:
        folder = 'assets' if asset_type == 'Image' else asset_type.lower()
        try:
            tos_url = tos_upload(file, folder=folder)
        except Exception as e:
            logger.exception('TOS upload failed for asset')
            return Response(
                {'error': '文件上传失败，请稍后重试'},
                status=status.HTTP_500_INTERNAL_SERVER_ERROR,
            )

        remote_asset_id = ''
        if remote_group_id:
            result, err = _assets_api_call(assets_client.create_asset, remote_group_id, tos_url, name, asset_type=asset_type)
            if err:
                return err
            if result is not None:
                remote_asset_id = result

        asset_obj = Asset.objects.create(
            group=group,
            remote_asset_id=remote_asset_id,
            name=name,
            url=tos_url,
            asset_type=asset_type,
            status='processing' if remote_asset_id else 'active',
            error_message='',
        )
        # Set group thumbnail for images; video/audio thumbnails extracted async
        if asset_type == 'Image':
            group.thumbnail_url = tos_url
        group.save(update_fields=['thumbnail_url'])
        # Async: extract thumbnail + duration for video/audio
        if asset_type in ('Video', 'Audio'):
            from apps.generation.tasks import process_asset_media
            process_asset_media.delay(asset_obj.id)

    return Response({
        'id': group.id,
        'name': group.name,
        'thumbnail_url': group.thumbnail_url,
        'remote_group_id': group.remote_group_id,
        'asset_count': Asset.objects.filter(group=group).count(),
        'created_at': group.created_at.isoformat(),
    }, status=status.HTTP_201_CREATED)


@api_view(['GET', 'PUT', 'DELETE'])
@permission_classes([IsTeamMember])
@parser_classes([JSONParser])
def asset_group_detail_view(request, group_id):
    """GET /api/v1/assets/groups/<id> — group info + assets.
       PUT /api/v1/assets/groups/<id> — update name/description.
       DELETE /api/v1/assets/groups/<id> — delete entire group + all assets.
    """
    team = request.user.team
    try:
        group = AssetGroup.objects.get(pk=group_id, team=team)
    except AssetGroup.DoesNotExist:
        return Response({'error': '素材组不存在'}, status=status.HTTP_404_NOT_FOUND)

    if request.method == 'DELETE':
        from utils import assets_client
        from utils.assets_client import AssetsAPIError
        if group.remote_group_id:
            try:
                assets_client.delete_asset_group(group.remote_group_id)
            except AssetsAPIError as e:
                # 火山那边已经没了（比如被后台手动删了）就继续清本地，保证幂等
                if e.code != 'NotFound.group_id':
                    logger.warning('Failed to delete remote group %s: %s', group.remote_group_id, e)
                    return Response(
                        {'error': 'assets_api_error', 'message': e.user_message},
                        status=status.HTTP_502_BAD_GATEWAY,
                    )
                logger.info('Remote group %s already gone, cleaning local only', group.remote_group_id)
        Asset.objects.filter(group=group).delete()
        group.delete()
        return Response({'message': '素材组已删除'})

    if request.method == 'GET':
        # 同步火山端的素材组名字
        if group.remote_group_id:
            try:
                from utils.assets_client import get_asset_group
                remote = get_asset_group(group.remote_group_id)
                remote_name = remote.get('Name', '')
                if remote_name and remote_name != group.name:
                    group.name = remote_name
                    group.save(update_fields=['name'])
            except Exception:
                pass  # 查不到就用本地名字

        assets_qs = Asset.objects.filter(group=group).order_by('-created_at')
        asset_list = []
        for a in assets_qs:
            asset_list.append({
                'id': a.id,
                'name': a.name,
                'url': a.url,
                'asset_type': a.asset_type,
                'thumbnail_url': a.thumbnail_url,
                'duration': a.duration,
                'status': a.status,
                'remote_asset_id': a.remote_asset_id,
                'error_message': a.error_message,
                'created_at': a.created_at.isoformat(),
            })
        return Response({
            'id': group.id,
            'name': group.name,
            'description': group.description,
            'thumbnail_url': group.thumbnail_url,
            'remote_group_id': group.remote_group_id,
            'created_at': group.created_at.isoformat(),
            'assets': asset_list,
        })

    # ── PUT ──
    new_name = request.data.get('name')
    new_desc = request.data.get('description')

    if new_name is None and new_desc is None:
        return Response({'error': '请提供要更新的字段'}, status=status.HTTP_400_BAD_REQUEST)

    # Update remote
    if group.remote_group_id:
        from utils import assets_client
        _, err = _assets_api_call(
            assets_client.update_asset_group,
            group.remote_group_id, name=new_name, description=new_desc,
        )
        if err:
            return err

    update_fields = []
    if new_name is not None:
        group.name = new_name
        update_fields.append('name')
    if new_desc is not None:
        group.description = new_desc
        update_fields.append('description')
    group.save(update_fields=update_fields)

    return Response({
        'id': group.id,
        'name': group.name,
        'description': group.description,
        'thumbnail_url': group.thumbnail_url,
        'remote_group_id': group.remote_group_id,
    })


@api_view(['POST'])
@permission_classes([IsTeamMember])
@parser_classes([MultiPartParser])
def asset_group_add_asset_view(request, group_id):
    """POST /api/v1/assets/groups/<id>/assets — add an asset (image/video/audio) to a group."""
    team = request.user.team
    try:
        group = AssetGroup.objects.get(pk=group_id, team=team)
    except AssetGroup.DoesNotExist:
        return Response({'error': '素材组不存在'}, status=status.HTTP_404_NOT_FOUND)

    file = request.FILES.get('file')
    if not file:
        return Response({'error': '请上传文件'}, status=status.HTTP_400_BAD_REQUEST)

    # Detect asset type and validate format/size
    asset_type, err = _detect_asset_type(file)
    if err:
        return err

    # Validate image dimensions (only for images)
    if asset_type == 'Image':
        try:
            from PIL import Image
            img = Image.open(file)
            w, h = img.size
            if w < 300 or h < 300:
                return Response(
                    {'error': f'图片太小了（当前 {w}x{h}），宽高需在 300~6000 像素之间'},
                    status=status.HTTP_400_BAD_REQUEST,
                )
            if w > 6000 or h > 6000:
                return Response(
                    {'error': f'图片太大了（当前 {w}x{h}），宽高需在 300~6000 像素之间'},
                    status=status.HTTP_400_BAD_REQUEST,
                )
            file.seek(0)
        except ImportError:
            pass
        except Exception:
            pass

    name = request.data.get('name', '').strip() or file.name

    # Upload to TOS
    folder = 'assets' if asset_type == 'Image' else asset_type.lower()
    try:
        tos_url = tos_upload(file, folder=folder)
    except Exception as e:
        logger.exception('TOS upload failed for asset')
        return Response(
            {'error': '文件上传失败，请稍后重试'},
            status=status.HTTP_500_INTERNAL_SERVER_ERROR,
        )

    # Create remote asset
    from utils import assets_client
    remote_asset_id = ''
    if group.remote_group_id:
        result, err = _assets_api_call(
            assets_client.create_asset, group.remote_group_id, tos_url, name, asset_type=asset_type,
        )
        if err:
            return err
        if result is not None:
            remote_asset_id = result

    asset = Asset.objects.create(
        group=group,
        remote_asset_id=remote_asset_id,
        name=name,
        url=tos_url,
        asset_type=asset_type,
        status='processing' if remote_asset_id else 'active',
        error_message='',
    )

    # Atomic: set group thumbnail only if still empty (concurrent-safe)
    if asset_type == 'Image':
        from django.db import transaction
        with transaction.atomic():
            locked_group = AssetGroup.objects.select_for_update().get(pk=group.id)
            if not locked_group.thumbnail_url:
                locked_group.thumbnail_url = tos_url
                locked_group.save(update_fields=['thumbnail_url'])

    # Async: extract thumbnail + duration for video/audio
    if asset_type in ('Video', 'Audio'):
        from apps.generation.tasks import process_asset_media
        process_asset_media.delay(asset.id)

    return Response({
        'id': asset.id,
        'name': asset.name,
        'url': asset.url,
        'asset_type': asset.asset_type,
        'thumbnail_url': asset.thumbnail_url,
        'duration': asset.duration,
        'status': asset.status,
        'remote_asset_id': asset.remote_asset_id,
        'created_at': asset.created_at.isoformat(),
    }, status=status.HTTP_201_CREATED)


@api_view(['PUT', 'DELETE'])
@permission_classes([IsTeamMember])
@parser_classes([JSONParser])
def asset_update_view(request, asset_id):
    """PUT /api/v1/assets/<id> — rename an asset. DELETE — delete an asset."""
    team = request.user.team
    try:
        asset = Asset.objects.select_related('group').get(pk=asset_id, group__team=team)
    except Asset.DoesNotExist:
        return Response({'error': '素材不存在'}, status=status.HTTP_404_NOT_FOUND)

    if request.method == 'DELETE':
        # Delete from Volcano first
        if asset.remote_asset_id:
            from utils import assets_client
            try:
                assets_client.delete_asset(asset.remote_asset_id)
            except Exception as e:
                logger.warning('Failed to delete remote asset %s: %s', asset.remote_asset_id, e)

        group = asset.group
        asset.delete()

        # Update group thumbnail: prefer Image > Video (with thumbnail) > empty
        remaining_img = Asset.objects.filter(group=group, asset_type='Image').exclude(status='failed').first()
        remaining_vid = Asset.objects.filter(group=group, asset_type='Video').exclude(status='failed').exclude(thumbnail_url='').first()
        if remaining_img:
            new_thumb = remaining_img.url
        elif remaining_vid:
            new_thumb = remaining_vid.thumbnail_url
        else:
            new_thumb = ''
        if group.thumbnail_url != new_thumb:
            group.thumbnail_url = new_thumb
            group.save(update_fields=['thumbnail_url'])

        return Response({'message': '素材已删除'})

    # PUT — rename
    new_name = request.data.get('name')
    if not new_name:
        return Response({'error': '请提供素材名称'}, status=status.HTTP_400_BAD_REQUEST)

    if asset.remote_asset_id:
        from utils import assets_client
        _, err = _assets_api_call(assets_client.update_asset, asset.remote_asset_id, name=new_name)
        if err:
            return err

    asset.name = new_name
    asset.save(update_fields=['name'])

    return Response({
        'id': asset.id,
        'name': asset.name,
        'url': asset.url,
        'status': asset.status,
    })


@api_view(['GET'])
@permission_classes([IsTeamMember])
def asset_search_view(request):
    """GET /api/v1/assets/search?q=... — search individual assets for @ popup."""
    team = request.user.team
    q = request.query_params.get('q', '').strip()[:100]  # 限制搜索长度
    if not q:
        return Response({'results': []})

    assets = (
        Asset.objects
        .filter(group__team=team, name__icontains=q, status='active')
        .select_related('group')
        .order_by('-created_at')[:20]
    )
    results = []
    for a in assets:
        results.append({
            'id': a.id,
            'name': a.name,
            'url': a.url,
            'asset_type': a.asset_type,
            'group_name': a.group.name,
            'remote_asset_id': a.remote_asset_id,
            'thumbnail_url': a.thumbnail_url,
            'duration': a.duration,
        })
    return Response({'results': results})


@api_view(['GET'])
@permission_classes([IsTeamMember])
def asset_poll_status_view(request, asset_id):
    """GET /api/v1/assets/<id>/status — poll remote processing status."""
    team = request.user.team
    try:
        asset = Asset.objects.select_related('group').get(pk=asset_id, group__team=team)
    except Asset.DoesNotExist:
        return Response({'error': '素材不存在'}, status=status.HTTP_404_NOT_FOUND)

    # 已经 active 且有 URL 的素材跳过远程查询（避免跨项目素材被误删）
    if asset.remote_asset_id and asset.status != 'active':
        from utils import assets_client
        from utils.assets_client import AssetsAPIError
        try:
            result = assets_client.get_asset(asset.remote_asset_id)
            remote_status = result.get('Status', '')
            if remote_status == 'Active':
                asset.status = 'active'
                asset.url = result.get('Url', asset.url)
            elif remote_status == 'Failed':
                asset.status = 'failed'
                asset.error_message = result.get('ErrorMessage', '')
            else:
                asset.status = 'processing'
            asset.save(update_fields=['status', 'url', 'error_message'])
        except AssetsAPIError as e:
            error_str = str(e)
            # 火山返回素材不存在 → 删除本地记录
            if 'not found' in error_str.lower() or 'NotFound' in e.code or 'NotExist' in e.code:
                asset.delete()
                return Response({'status': 'deleted', 'message': '素材在云端已被删除'})
            return Response(
                {'error': 'assets_api_error', 'message': '素材状态查询失败，请稍后重试'},
                status=status.HTTP_502_BAD_GATEWAY,
            )
        except Exception as e:
            error_str = str(e)
            # 空响应 / JSON 解析失败 = 火山已删除该素材
            if 'Expecting value' in error_str or 'JSONDecodeError' in type(e).__name__:
                logger.info('Asset %s appears deleted on remote (empty response), removing local record', asset.remote_asset_id)
                asset.delete()
                return Response({'status': 'deleted', 'message': '素材在云端已被删除'})
            logger.exception('Assets API unexpected error in poll')
            return Response(
                {'error': 'assets_api_error', 'message': '素材服务异常，请稍后重试'},
                status=status.HTTP_502_BAD_GATEWAY,
            )

    return Response({
        'id': asset.id,
        'name': asset.name,
        'url': asset.url,
        'status': asset.status,
        'error_message': asset.error_message,
    })
