import subprocess

import modal
from asyncio import sleep
from dotenv import dotenv_values

downloader_image = (
    modal.Image
    .debian_slim(python_version="3.11")
    .apt_install('ffmpeg')
    .pip_install_from_pyproject("../pyproject.toml")
    .apt_install("ffmpeg")
    .env(dotenv_values("../.runtime.env"))
    .add_local_python_source('cluster')
    .add_local_python_source('BowongModalFunctions')
)

app = modal.App(
    name="media_app",
    image=downloader_image,
    include_source=False,
    secrets=[
        modal.Secret.from_name("cf-kv-secret"),
    ])

with downloader_image.imports():
    import os, httpx, crcmod, json, time, requests
    import sentry_sdk
    from sentry_sdk.integrations.loguru import LoguruIntegration
    from tqdm import tqdm
    from typing import Tuple, List
    from loguru import logger
    from datetime import datetime, UTC, timedelta, timezone
    from modal import current_function_call_id
    from ffmpy import FFmpeg, FFprobe
    from fastapi import HTTPException
    from httpx import Timeout
    from starlette import status

    from tencentcloud.common.credential import Credential
    from tencentcloud.vod.v20180717.vod_client import VodClient
    from tencentcloud.vod.v20180717 import models as vod_request_models

    from BowongModalFunctions.config import WorkerConfig
    from BowongModalFunctions.utils.KVCache import MediaSourceKVCache, LiveProductKVCache
    from BowongModalFunctions.models.media_model import MediaSource, MediaCacheStatus, MediaProtocol
    from BowongModalFunctions.models.web_model import SentryTransactionInfo, MonitorLiveRoomProductRequest, LiveProduct, \
    LiveProductCaches
    from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
    from BowongModalFunctions.utils.TimeUtils import TimeDelta

    config = WorkerConfig()

    sentry_sdk.init(dsn="https://85632fdcd62f699c2f88af6ca489e9ec@sentry.bowongai.com/3",
                    send_default_pii=True,
                    traces_sample_rate=1.0,
                    profiles_sample_rate=1.0,
                    add_full_stack=True,
                    shutdown_timeout=2,
                    integrations=[LoguruIntegration()],
                    environment=config.modal_environment,
                    )

    cf_account_id = os.environ.get("CF_ACCOUNT_ID")
    cf_kv_api_token = os.environ.get("CF_KV_API_TOKEN")
    cf_kv_namespace_id = os.environ.get("CF_KV_NAMESPACE_ID")

    modal_kv_cache = MediaSourceKVCache(kv_name=config.modal_kv_name,
                                        environment=config.modal_environment)

    modal_kv_product_cache = LiveProductKVCache(kv_name=config.modal_product_kv_name,
                                                environment=config.modal_environment)


    @sentry_sdk.trace
    def batch_update_cloudflare_kv(caches: List[MediaSource]):
        with httpx.Client() as client:
            try:
                response = client.put(
                    f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/storage/kv/namespaces/{cf_kv_namespace_id}/bulk",
                    headers={"Authorization": f"Bearer {cf_kv_api_token}"},
                    json=[
                        {
                            "based64": False,
                            "key": cache.urn,
                            "value": cache.model_dump_json(),
                        }
                        for cache in caches
                    ]
                )
                response.raise_for_status()
            except httpx.RequestError as e:
                logger.error(f"An error occurred while put kv to cloudflare")
                raise e
            except httpx.HTTPStatusError as e:
                logger.error(f"HTTP error occurred while get kv from cloudflare {str(e)}")
                raise e
            except Exception as e:
                logger.error(f"An unexpected error occurred: {str(e)}")
                raise e


    @sentry_sdk.trace
    def batch_remove_cloudflare_kv(caches: List[MediaSource]):
        with httpx.Client() as client:
            try:
                response = client.post(
                    f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/storage/kv/namespaces/{cf_kv_namespace_id}/bulk/delete",
                    headers={"Authorization": f"Bearer {cf_kv_api_token}"},
                    json=[cache.urn for cache in caches]
                )
                response.raise_for_status()
            except httpx.RequestError as e:
                logger.error(f"An error occurred while put kv to cloudflare")
                raise e
            except httpx.HTTPStatusError as e:
                logger.error(f"HTTP error occurred while get kv from cloudflare {str(e)}")
                raise e
            except Exception as e:
                logger.error(f"An unexpected error occurred: {str(e)}")
                raise e


    @app.function(cpu=1, timeout=1800,
                  cloud="aws",
                  max_containers=config.video_downloader_concurrency,
                  volumes={
                      "/mntS3": modal.CloudBucketMount(
                          bucket_name=config.S3_bucket_name,
                          secret=modal.Secret.from_name("aws-s3-secret", environment_name=config.modal_environment),
                      ),
                  },
                  secrets=[modal.Secret.from_name("tencent-cloud-secret", environment_name=config.modal_environment)])
    @modal.concurrent(max_inputs=10)
    async def cache_submit(media: MediaSource, sentry_trace: SentryTransactionInfo) -> MediaSource:
        def vod_init():
            tencent_secret_id = os.environ["VOD_SECRET_ID"]
            tencent_secret_key = os.environ["VOD_SECRET_KEY"]
            cred = Credential(secret_id=tencent_secret_id, secret_key=tencent_secret_key)
            return VodClient(credential=cred, region='ap-shanghai')

        def vod_info(media: MediaSource) -> Tuple[str, str, str]:
            logger.info(f"Downloading media {media}")
            request = vod_request_models.DescribeMediaInfosRequest()
            request.SubAppId = int(media.bucket)
            # 兼容fileId带文件类型的格式和不带文件类型的格式
            request.FileIds = [media.path.split('.')[0] if '.' in media.path else media.path]
            response = vod_client.DescribeMediaInfos(request)
            if len(response.MediaInfoSet) > 0:
                media_info = response.MediaInfoSet[0].BasicInfo
                logger.info(f"VOD info = {media_info}")
                file_extension = media_info.Type
                cache_dir = f"{config.S3_mount_dir}/{media.protocol.value}/{media.endpoint}/{media.bucket}"
                cache_file = media.path if '.' in media.path else f"{media.path}.{file_extension}"
                return cache_dir, cache_file, media_info.MediaUrl
            else:
                raise FileNotFoundError(
                    f"FileId : {media.path} not found in SubAppId: {media.bucket} at {media.endpoint}")

        def vod_download(media: MediaSource, on_progress_update: callable(float) = None) -> str:
            cache_dir, cache_file, url = vod_info(media)
            local_cache_filepath = os.path.join(cache_dir, cache_file)
            download_large_file(url=url, output_path=local_cache_filepath,
                                on_progress_callback=on_progress_update)
            return local_cache_filepath

        def download_large_file(url: str, output_path: str, protocol: MediaProtocol = MediaProtocol.vod,
                                on_progress_callback: callable(float) = None) -> None:
            # 配置日志
            logger.info(f"Starting download from {url}")
            try:
                # 使用 httpx 发送 HEAD 请求获取文件大小
                # 设置请求头，支持断点续传
                headers = {'Range': 'bytes=0-'}
                with httpx.Client() as client:
                    match protocol:
                        case MediaProtocol.vod:
                            head_response = client.head(url)
                            file_size = int(head_response.headers.get('content-length', 0))
                            remote_crc64 = int(head_response.headers.get('X-Cos-Hash-Crc64ecma', 0))
                            logger.info(f"File size: {file_size / (1024 * 1024 * 1024):.2f} GB")

                            if os.path.exists(output_path):
                                local_file_size = os.path.getsize(output_path)
                                logger.info(f"File size match check {local_file_size} = {file_size}")
                                if local_file_size == file_size:
                                    logger.info(f"Check file CRC64...")
                                    # CRC64使用ECMA-182标准校验 ref: https://cloud.tencent.com/document/product/436/40334#python-sdk
                                    c64 = crcmod.mkCrcFun(0x142F0E1EBA9EA3693, initCrc=0, xorOut=0xffffffffffffffff,
                                                          rev=True)
                                    with open(output_path, "rb") as local_file:
                                        local_crc64 = c64(local_file.read())
                                    logger.info(f"File crc64 check {local_crc64} = {remote_crc64}")
                                    if local_crc64 == remote_crc64:
                                        logger.success("File size verification passed!")
                                    return
                            os.makedirs(os.path.dirname(output_path), exist_ok=True)

                    logger.info(f"Downloading {url}...")
                    # 发起流式请求
                    with client.stream('GET', url, headers=headers) as response:
                        response.raise_for_status()
                        file_size = int(response.headers.get('content-length', 0))
                        # 设置进度条
                        progress_bar = tqdm(
                            total=file_size,
                            unit='iB',
                            unit_scale=True,
                            desc='Downloading'
                        )
                        # 以二进制写模式打开文件
                        with open(output_path, 'wb') as file:
                            # 分块下载，每次读取1MB
                            chunk_size = 1024 * 1024  # 1MB
                            downloaded_size = 0
                            for chunk in response.iter_bytes(chunk_size=chunk_size):
                                if chunk:
                                    file.write(chunk)
                                    downloaded_size += len(chunk)
                                    if on_progress_callback:
                                        on_progress_callback(downloaded_size / file_size)
                                    progress_bar.update(len(chunk))
                                    # 每下载100MB记录一次日志
                                    if downloaded_size % (100 * 1024 * 1024) == 0:
                                        logger.info(
                                            f"Downloaded: {downloaded_size / (1024 * 1024 * 1024):.2f} GB")

                        progress_bar.close()

                        # 验证下载是否完成
                        if os.path.exists(output_path):
                            final_size = os.path.getsize(output_path)
                        logger.info(f"Download completed successfully!")
                        logger.info(f"Final file size: {final_size / (1024 * 1024 * 1024):.2f} GB")
                        logger.info(f"File saved to: {os.path.abspath(output_path)}")
                        # 验证文件大小是否匹配
                        if final_size == file_size:
                            logger.info("File size verification passed!")
                        else:
                            logger.warning(f"File size mismatch! Expected: {file_size}, Got: {final_size}")
            except httpx.RequestError as e:
                logger.error(f"An error occurred while requesting {url}: {str(e)}")
                raise e
            except httpx.HTTPStatusError as e:
                logger.error(f"HTTP error occurred: {str(e)}")
                raise e
            except Exception as e:
                logger.error(f"An unexpected error occurred: {str(e)}")
                raise e
            finally:
                if 'progress_bar' in locals():
                    progress_bar.close()

        vod_client = vod_init()
        modal_kv = modal_kv_cache
        fn_id = current_function_call_id()

        with sentry_sdk.continue_trace(environ_or_headers={"sentry-trace": sentry_trace.x_trace_id,
                                                           "baggage": sentry_trace.x_baggage, }) as transaction:
            transaction.set_context("runtime_environment", {
                "MODAL_CLOUD_PROVIDER": os.environ.get('MODAL_CLOUD_PROVIDER', 'unknown'),
                "MODAL_ENVIRONMENT": config.modal_environment,
                "MODAL_IMAGE_ID": os.environ.get('MODAL_IMAGE_ID', 'unknown'),
                "MODAL_IS_REMOTE": os.environ.get('MODAL_IS_REMOTE', 'unknown'),
                "MODAL_REGION": os.environ.get('MODAL_REGION', 'unknown'),
                "MODAL_TASK_ID": os.environ.get('MODAL_TASK_ID', 'unknown'),
                "MODAL_IDENTITY_TOKEN": os.environ.get('MODAL_IDENTITY_TOKEN', 'unknown'),
            })
            with transaction.start_child(name="收到缓存视频任务", op="queue.receive") as receive_span:
                receive_span.set_data("messaging.message.id", fn_id)
                receive_span.set_data("messaging.destination.name", "video-downloader.cache_submit")
                receive_span.set_data("messaging.message.retry.count", 0)
                receive_span.set_data("cache.key", media.urn)
                with receive_span.start_child(name="处理缓存视频任务", op="queue.process") as process_span:
                    process_span.set_data("messaging.message.id", fn_id)
                    process_span.set_data("messaging.destination.name", "video-downloader.cache_submit")
                    process_span.set_data("messaging.message.retry.count", 0)
                    process_span.set_data("cache.key", media.urn)
                    volume_cache_path = None
                    match media.protocol:
                        case MediaProtocol.vod:
                            try:
                                volume_cache_path = vod_download(media)
                                process_span.set_status("success")
                            except Exception as e:
                                logger.exception(e)
                                media.status = MediaCacheStatus.failed
                                modal_kv.set_cache(media)
                                batch_update_cloudflare_kv([media])
                                process_span.set_status("failed")
                                raise e
                        case MediaProtocol.http:
                            try:
                                cache_filepath = f"{config.S3_mount_dir}/{media.protocol.value}/{media.cache_filepath}"
                                os.makedirs(os.path.dirname(cache_filepath), exist_ok=True)
                                download_large_file(url=media.__str__(), output_path=cache_filepath)
                            except Exception as e:
                                logger.exception(e)
                                media.status = MediaCacheStatus.failed
                                modal_kv.set_cache(media)
                                batch_update_cloudflare_kv([media])
                                process_span.set_status("failed")
                                raise e
                        case MediaProtocol.s3:
                            # 本地挂载缓存
                            if media.protocol == MediaProtocol.s3 and media.endpoint == config.S3_region and media.bucket == config.S3_bucket_name:
                                volume_cache_path = f"{config.S3_mount_dir}/{media.cache_filepath}"
                            else:
                                logger.error("protocol not yet supported")
                        case _:
                            process_span.set_status("failed")
                            raise NotImplementedError("protocol not yet supported")
                    media.downloader_id = fn_id
                    media.status = MediaCacheStatus.ready if volume_cache_path else MediaCacheStatus.failed
                    media.progress = 1 if volume_cache_path else 0
                    media.expired_at = datetime.now(UTC) + timedelta(days=7) if volume_cache_path else None
                    modal_kv.set_cache(media)
                    batch_update_cloudflare_kv([media])
        return media


    @app.function(cpu=1, timeout=300,
                  max_containers=config.video_downloader_concurrency,
                  volumes={
                      "/mntS3": modal.CloudBucketMount(
                          bucket_name=config.S3_bucket_name,
                          secret=modal.Secret.from_name("aws-s3-secret", environment_name=config.modal_environment),
                      ),
                  })
    @modal.concurrent(max_inputs=10)
    async def cache_delete(cache: MediaSource) -> MediaSource:
        if os.path.exists(cache.cache_filepath):
            os.remove(cache.cache_filepath)
            cache.status = MediaCacheStatus.deleted
        else:
            cache.status = MediaCacheStatus.missing
        return cache

    @app.function(cpu=(0.5,64), timeout=1800,
                  max_containers=config.video_downloader_concurrency,
                  volumes={
                      config.S3_mount_dir: modal.CloudBucketMount(
                          bucket_name=config.S3_bucket_name,
                          secret=modal.Secret.from_name("aws-s3-secret", environment_name=config.modal_environment),
                      ),
                  })
    @modal.concurrent(max_inputs=1)
    async def video_hls_slice_inference(media_hls_url:str,
                                        google_api_key:str,
                                        product_list: List[str],
                                        start_time:str,
                                        end_time:str,
                                        sentry_trace: SentryTransactionInfo
                                        ):

        def calculate_target_dimensions(original_width, original_height, target_height=1280):
            """计算保持宽高比的目标尺寸，高度固定为1280"""
            aspect_ratio = original_width / original_height
            target_width = int(target_height * aspect_ratio)

            # 确保宽度是偶数（H.265编码要求）
            if target_width % 2 != 0:
                target_width -= 1

            return target_width, target_height

        def convert_video(input_file, output_file=None):
            """将视频转换为30fps、720p、H.264编码(CRF=16)"""
            if not output_file:
                # 生成默认输出文件名
                base_name, ext = os.path.splitext(input_file)
                base_name = base_name.split(os.sep)[-1]
                output_file = f"/root/{base_name}_720p_h265.mp4"

            try:
                # 第一步：获取原始视频宽高
                ffprobe = FFprobe(
                    inputs={input_file: ['-v', 'error', '-select_streams', 'v:0',
                                   '-show_entries', 'stream=width,height',
                                   '-of', 'csv=p=0']}
                )
                info = ffprobe.run(stdout=subprocess.PIPE, stderr=subprocess.PIPE)[0].decode('utf-8').strip()
                width, height = map(int, info.split(','))

                # 计算目标尺寸
                target_width, target_height = calculate_target_dimensions(width, height)

                # 第二步：执行视频转换
                ff = FFmpeg(
                    inputs={input_file: None},
                    outputs={output_file: [
                        '-vf', f'scale={target_width}:{target_height}',
                        '-r', '30',  # 帧率设置为30fps
                        '-c:v', 'libx264',  # 使用H.264编码
                        '-crf', '16',  # 质量设置（CRF 16）
                        '-preset', 'fast',  # 编码预设
                        '-c:a', 'copy',  # 音频编码复制
                        '-y'  # 覆盖已存在文件
                    ]}
                )

                logger.info(f"开始转换视频: {input_file}")
                logger.info(f"目标分辨率: {target_width}x{target_height}")
                logger.info(f"输出文件: {output_file}")

                ff.run()
                logger.success("视频转换完成!")
                return output_file

            except Exception as e:
                logger.exception(f"视频转换失败 {e}")
                return

        def upload(file_path):
            with open(file_path, "rb") as video:
                video = video.read()
            content_length = len(video)
            content_type = f"video/{file_path.split('.')[-1]}"
            filename = file_path.split("\\")[-1]
            if content_type not in ['video/mp4', 'video/mpeg', 'video/mov', 'video/avi', 'video/x-flv', 'video/mpg',
                                    'video/webm', 'video/wmv', 'video/3gpp']:
                raise HTTPException(status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)
            logger.info(
                f"Uploading name = {filename}, size = {content_length}, type = {content_type} to google file")
            with httpx.Client(timeout=1800) as client:
                pre_upload_response = client.post(
                    url=f"https://generativelanguage.googleapis.com/upload/v1beta/files?key={google_api_key}",
                    headers={
                        "X-Goog-Upload-Protocol": "resumable",
                        "X-Goog-Upload-Command": "start",
                        "X-Goog-Upload-Header-Content-Length": str(content_length),
                        "X-Goog-Upload-Header-Content-Type": content_type
                    },
                    json={
                        "file": {
                            "display_name": filename.split(".")[0],
                        }
                    })
                pre_upload_response.raise_for_status()

                upload_url = pre_upload_response.headers.get("X-Goog-Upload-Url")

                upload_response = client.post(url=upload_url, content=video, headers={
                    "X-Goog-Upload-Offset": "0",
                    "X-Goog-Upload-Command": "upload, finalize",
                    "Content-Type": content_type
                })
                upload_response.raise_for_status()

            return upload_response.json(), upload_response.status_code

        # 1、首先获取全量商品列表
        # try:
        logger.info("1、获取直播间商品列表")
        #     product_list = modal_kv_product_cache.get_cache(room_id)
        #     if not product_list:
        #         raise Exception(f"room_id {room_id} 商品列表缓存为空, 请先发起请求缓存商品列表")
        #     product_list = product_list.product_list
        #     logger.success(f"获取商品列表成功, 长度{len(product_list)}")
        # except Exception as e:
        #     logger.exception(f"获取商品列表失败：{e}")
        #     raise
        if len(product_list) == 0:
            logger.error("商品列表为空, 退出推理")
            raise Exception("商品列表为空, 退出推理")
        # 2、切20分钟的条
        logger.info("2、开始截取指定视频")
        slice_fn = modal.Function.from_name(config.modal_app_name,"ffmpeg_slice_media", environment_name=config.modal_environment)
        slice_result,sentry_trace = await slice_fn.remote.aio(MediaSource.from_str(media_hls_url),[FFMpegSliceSegment(
            start=TimeDelta.from_format_string(start_time),end=TimeDelta.from_format_string(end_time))], sentry_trace)
        video = MediaSource.from_str(slice_result[0].urn)
        logger.success("截取完成")
        # 3、转换为720p h265
        logger.info("3、转换视频为720p h264")
        video_path = convert_video(os.path.join(config.S3_mount_dir,video.path))
        # 4、上传到gemini
        logger.info("4、视频文件开始上传到Gemini")
        video_gemini, code = upload(video_path)
        if code == 200:
            video_gemini_uri = video_gemini["file"]["uri"]
        else:
            logger.error("视频文件上传Gemini失败")
            raise Exception("视频文件上传Gemini失败")
        # 5、检查文件是否已处理完成
        def check():
            with httpx.Client(timeout=Timeout(timeout=30)) as client:
                file = video_gemini_uri.split("/")[-1]
                response = client.get(url=f"https://generativelanguage.googleapis.com/v1beta/files/{file}?key={google_api_key}")
                response.raise_for_status()
            if response.status_code == 200:
                if response.json()["state"]=="ACTIVE":
                    return True
            return False
        check_num = 60
        logger.info("5、开始检查Gemini文件是否处理完成")
        while check_num > 0:
            logger.info(f"检查中, 剩余检查次数{check_num}")
            ret = check()
            if ret:
                break
            else:
                check_num -= 1
                time.sleep(5)
        if check_num <= 0:
            raise Exception("Gemini文件上传处理状态检查超时")
        logger.success("Gemini文件处理完成")
        # 6、执行Gemini推理操作
        def inference_api():
            try:
                resp = requests.post(
                    "https://gateway.ai.cloudflare.com/v1/67720b647ff2b55cf37ba3ef9e677083/bowong-dev/google-ai-studio/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent",
                    headers={
                        "Content-Type": "application/json",
                        "X-Goog-Api-Key": f"{google_api_key}"
                    },
                    json={
                        "safetySettings": [
                            {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
                            {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
                            {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
                            {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
                            {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}
                        ],
                        "contents": [
                            {
                                "parts": [
                                    {
                                        "file_data": {
                                            "mime_type": "video/mp4",
                                            "file_uri": f"{video_gemini_uri}"
                                        }
                                    },
                                    {
                                        "text": """<prompt>
                                      <instruction>
                                      我上传的是一个主播直播卖货的片段，帮我分析这个视频里的画面、语音、字幕，然后归纳其中的核心讲解的商品，注意不要包含任何不安全或色情的内容。
                                      按照以下json格式返回：
                                      [{"timeline":["核心商品所属时间段落"],"product":"核心商品名"}]
                                      </instruction>
    
                                      <datas>
                                          <!-- 核心商品列表 -->
                                          <data>{"product_name":%s}</data>
                                      </datas>
    
                                      <rules>
                                          <!-- 核心商品判断标准 -->
                                          <rule>参考（核心商品列表）里的商品名称信息（必须从product_name里的列表中取值，顺序靠前的商品更有可能是刚讲过的核心商品），找出他们中谁在这段视频中有过介绍。</rule>
                                          <rule>配合核心商品展示搭配介绍的商品，应该归类至核心商品的内容，而不是这个搭配的商品单独成为核心商品。</rule>
                                          <rule>一个核心商品可以存在多个timeline，请确保这个核心商品的所有出现过的时间都被识别出来，可以合并的碎片时间段尽量合并。</rule>
                                          <rule>如果一个片段，有主播不在画面内的内容，需要从这个核心商品片段中去除。</rule>
    
                                          <!-- 切换商品的判断条件 -->
                                          <rule>请精准区分核心商品的开始结束时刻，一定要确保核心商品的介绍内容完整。</rule>
                                          <rule>结合画面、语音综合判断商品是否转换，不要只从某个维度进行判断。</rule>
    
                                          <!-- timeline时间段规则 -->
                                          <rule>确保核心商品输出的timeline时间段落必须是该格式(小时:分钟:秒.毫秒)，精确到毫秒（例：00:00:00.000 - 00:00:10.012）。</rule>
                                      </rules>
    
                                      <examples>
                                          <!-- 正确的输出格式 -->
                                          <example>
                                          [{"timeline":["00:00:00.000 - 00:00:10.012","00:01:20.856 - 00:05:10.072"],"product":"蓝色连衣裙"}]
                                          </example>
                                          <reason>正确的保留了时间到毫秒，并且将核心商品的多个片段都展示出来了。</reason>
                                      </examples>
                                  </prompt>""" % ([i for i in product_list],)
                                    }
                                ]
                            }
                        ]
                    }, timeout=900
                )
            except Exception as e:
                logger.exception(f"Gemini推理请求失败: {e}")
                raise e
            if resp.status_code == 200:
                return resp.json()
            else:
                logger.error(f"Gemini推理失败, 状态码{resp.status_code}")
                raise
        logger.info("6、发起Gemini推理")
        target_json = inference_api()
        logger.info(f"推理完成JSON \n{json.dumps(target_json, indent=4, ensure_ascii=False)}")
        try:
            reason = target_json["candidates"][0]["finishReason"]
            if reason=="STOP":
                parts:str = target_json["candidates"][0]["content"]["parts"][0]["text"]
                parts = parts.replace("```","").replace("json\n","").replace("\n","").replace("\\","")
                parts = json.loads(parts)
                return parts, sentry_trace
            else:
                raise Exception(f"Gemini推理失败, 失败原因: {reason}")
        except Exception as e:
            logger.exception(f"推理失败, {e}")
            raise Exception(f"推理失败, {e}")
        finally:
            logger.info("7、清除Gemini临时文件")
            with httpx.Client(timeout=Timeout(timeout=120)) as client:
                resp = client.delete(f'https://bowongai-{config.modal_environment}--{config.modal_app_name}-fastapi-webapp.modal.run/google/delete',
                            params={"filename":video_gemini_uri.split("/")[-1]}, headers={"x-google-api-key":google_api_key})
                resp.raise_for_status()
            if resp.status_code == 200:
                logger.success("Gemini临时文件清除成功")
            else:
                logger.warning("Gemini临时文件清除失败, 请自行清除")

    @app.function(max_containers=config.video_downloader_concurrency, timeout=130)
    @modal.concurrent(max_inputs=50)
    async def monitor_live_room_product_trigger(cookie:str, room_id:str, author_id:str) -> int:
        def get_product_list():
            with httpx.Client(timeout=Timeout(timeout=120)) as client:
                resp = client.get(f'https://bowongai-{config.modal_environment}--{config.modal_app_name}-fastapi-webapp-tikhub.modal.run/douyin/web/fetch_live_room_product_result',
                            params={"cookie":cookie,"room_id":room_id,"author_id":author_id})
                resp.raise_for_status()
            if resp.status_code == 200:
                if resp.json()["data"]["total"] >= 0:
                    return 0, resp.json()["data"]["promotions"]
                elif resp.json()["data"]["total"] == -1:
                    # 直播结束
                    return 1, []
                elif resp.json()["data"]["total"] == -2:
                    # IP风控
                    return 2, []
            # 其他错误
            return 3, []
        try:
            logger.info(f"room_id {room_id} author_id {author_id} 触发监控商品...")
            is_live, product_list = get_product_list()
            if is_live == 1:
                logger.warning(f"room_id {room_id} author_id {author_id} 直播已结束, 停止监控商品, 删除缓存")
                modal_kv_product_cache.pop(room_id, raise_exception=False)
                modal_kv_product_cache.batch_remove_cloudflare_kv([room_id])
                return is_live
            elif is_live == 2:
                logger.warning(f"room_id {room_id} author_id {author_id} 获取商品出现风控")
                return is_live
            elif is_live == 3:
                logger.warning(f"room_id {room_id} author_id {author_id} 网络请求出现错误")
                return is_live
            last_cache = modal_kv_product_cache.get_cache(room_id)
            if last_cache is not None:
                for product in last_cache.product_list:
                    for new_product in product_list:
                        if product.title == new_product["title"]:
                            product_list.remove(new_product)
                if len(product_list) > 0:
                    logger.success(f"room_id {room_id} author_id {author_id} 检测到商品变化, 增量刷新缓存")
                    # 最新的插入在最前
                    for new_product in product_list[::-1]:
                        last_cache.product_list.insert(0, LiveProduct(title=new_product["title"],
                                                                   leaf_category=new_product["leaf_category"],
                                                                   shop_id=new_product["shop_id"],
                                                                   product_id=new_product["product_id"],
                                                                   cover=new_product["cover"],
                                                                   detail_url=new_product["detail_url"]))
            else:
                logger.success(f"room_id {room_id} author_id {author_id} 新建缓存")
                last_cache = LiveProductCaches(room_id=room_id, author_id=author_id, product_list=[
                    LiveProduct(title=new_product["title"],
                               leaf_category=new_product["leaf_category"],
                               shop_id=new_product["shop_id"],
                               product_id=new_product["product_id"],
                               cover=new_product["cover"],
                               detail_url=new_product["detail_url"]) for new_product in product_list])
            last_cache.update_time = datetime.now(timezone(timedelta(hours=8))).strftime("%Y-%m-%d %H:%M:%S")
            last_cache.count = len(last_cache.product_list)
            modal_kv_product_cache.set_cache(last_cache)
            modal_kv_product_cache.batch_update_cloudflare_kv({last_cache.room_id: last_cache.model_dump_json()})
            return is_live
        except Exception as e:
            logger.exception(f"room_id {room_id} author_id {author_id} 触发监控商品发生错误 {e}")
            return 4

