import asyncio
from pathlib import Path

import botocore
import httpx
import modal
from dotenv import dotenv_values
from watchdog.events import DirMovedEvent

from BowongModalFunctions.utils.ModalUtils import ModalUtils

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

app = modal.App(
    name="ffmpeg_app",
    image=ffmpeg_worker_image,
    include_source=False)

with ffmpeg_worker_image.imports():
    import shutil, os, backoff, sentry_sdk
    from typing import List, Optional, Tuple, Dict, Any, Union, Set
    from loguru import logger
    from modal import current_function_call_id
    from ffmpeg.asyncio import FFmpeg
    from BowongModalFunctions.utils.SentryUtils import SentryUtils
    from BowongModalFunctions.utils.PathUtils import FileUtils
    from BowongModalFunctions.utils.VideoUtils import VideoUtils
    from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
    from BowongModalFunctions.models.media_model import MediaSources, MediaSource, MediaProtocol
    from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, FFMPEGResult, \
        WebhookMethodEnum, BaseFFMPEGTaskStatusResponse, TaskStatus
    from BowongModalFunctions.config import WorkerConfig

    config = WorkerConfig()

    s3_mount = config.S3_mount_dir
    output_path_prefix = config.local_output_mount

    sentry_sdk.init(
        dsn="https://75cca970bfcc3d45d24361e7c0f1833c@sentry.bowongai.com/4",
        send_default_pii=True,
        add_full_stack=True,
        shutdown_timeout=2,
        traces_sample_rate=1.0,
        environment=config.modal_environment,
    )


    @backoff.on_exception(wait_gen=backoff.constant, exception=Exception, max_tries=5, max_time=60,
                          raise_on_giveup=True)
    def local_copy_to_s3(local_outputs: List[str]) -> List[str]:
        s3_outputs = []
        for output in local_outputs:
            out_s3 = output.replace(output_path_prefix, s3_mount)
            out_s3_dir = os.path.dirname(out_s3)
            logger.info(out_s3_dir)
            os.makedirs(out_s3_dir, exist_ok=True)
            shutil.copy(output, out_s3)
            s3_outputs.append(
                out_s3.replace(s3_mount + '/', f"s3://{config.S3_region}/{config.S3_bucket_name}/"))
        return s3_outputs


    @app.function(
        timeout=1800,
        cloud="aws",
        # region='ap-northeast',
        max_containers=config.ffmpeg_worker_concurrency,
        volumes={
            s3_mount: 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 ffmpeg_concat_medias(medias: MediaSources,
                                   sentry_trace: Optional[SentryTransactionInfo] = None,
                                   webhook: Optional[WebhookNotify] = None) -> Tuple[
        FFMPEGResult, Optional[SentryTransactionInfo]]:

        fn_id = current_function_call_id()

        @SentryUtils.sentry_tracker(name="视频合并任务", op="ffmpeg.concat", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_process(media_sources: MediaSources, output_filepath: str) -> FFMPEGResult:
            input_videos = [f"{s3_mount}/{media_source.cache_filepath}" for media_source in media_sources.inputs]
            local_output_path, metadata = await VideoUtils.ffmpeg_concat_medias(media_paths=input_videos,
                                                                                output_path=output_filepath)
            s3_outputs = local_copy_to_s3([local_output_path])
            return FFMPEGResult(urn=s3_outputs[0], metadata=metadata,
                                content_length=FileUtils.get_file_size(local_output_path), )

        output_path = f"{output_path_prefix}/{config.modal_environment}/concat/outputs/{fn_id}/output.mp4"
        result = await ffmpeg_process(media_sources=medias, output_filepath=output_path)
        if not sentry_trace:
            sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
                                                 x_baggage=sentry_sdk.get_baggage())
        return result, sentry_trace


    @app.function(
        timeout=900,
        cloud="aws",
        cpu=(0.5, 64),
        max_containers=config.ffmpeg_slice_worker_concurrency,
        volumes={
            s3_mount: 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 ffmpeg_slice_media(media: MediaSource,
                                 markers: List[FFMpegSliceSegment],
                                 sentry_trace: Optional[SentryTransactionInfo] = None,
                                 webhook: Optional[WebhookNotify] = None) -> Tuple[
        List[FFMPEGResult], Optional[SentryTransactionInfo]]:
        fn_id = current_function_call_id()

        @SentryUtils.sentry_tracker(name="视频文件切割任务", op="ffmpeg.slice.media", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_slice_process(media_source: MediaSource, media_markers: List[FFMpegSliceSegment],
                                       fn_id: str) -> List[FFMPEGResult]:
            cache_filepath = f"{s3_mount}/{media_source.cache_filepath}"
            logger.info(f"从{media_source.urn}切割")
            for i, marker in enumerate(media_markers):
                logger.info(f"[{i}] {marker.start.toFormatStr()} --> {marker.end.toFormatStr()}")
            segments = await VideoUtils.ffmpeg_slice_media(media_path=cache_filepath,
                                                           media_markers=media_markers,
                                                           output_path=f"{output_path_prefix}/{config.modal_environment}/slice/outputs/{fn_id}/output.mp4")
            return [FFMPEGResult(urn=local_copy_to_s3([segment[0]])[0], metadata=segment[1],
                                 content_length=FileUtils.get_file_size(segment[0])) for segment in segments]

        @SentryUtils.sentry_tracker(name="直播视频切割任务", op="ffmpeg.slice.stream", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_hls_slice_process(media_source: MediaSource,
                                           media_markers: List[FFMpegSliceSegment],
                                           fn_id: str) -> List[FFMPEGResult]:
            hls_m3u8_url = media_source.path
            segments = await VideoUtils.ffmpeg_slice_stream_media_multithread(media_path=hls_m3u8_url,
                                                                              media_markers=media_markers,
                                                                              output_path=f"{output_path_prefix}/{config.modal_environment}/slice/outputs/{fn_id}/output.mp4")
            return [FFMPEGResult(urn=local_copy_to_s3([segment[0]])[0], metadata=segment[1],
                                 content_length=FileUtils.get_file_size(segment[0])) for segment in segments]

        match media.protocol:
            case MediaProtocol.hls:
                outputs = await ffmpeg_hls_slice_process(media_source=media,
                                                         media_markers=markers,
                                                         fn_id=fn_id)
            # case MediaProtocol.s3:
            #     outputs = await ffmpeg_slice_process(media_source=media,
            #                                          media_markers=markers,
            #                                          fn_id=fn_id)
            case _:  # webhook不会报错，需要确认
                raise NotImplementedError("暂不支持的协议")

        return [result for result in outputs], sentry_trace


    @app.function(timeout=600, cloud="aws",
                  max_containers=config.ffmpeg_worker_concurrency,
                  volumes={
                      s3_mount: 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 ffmpeg_extract_audio(media_source: MediaSource,
                                   sentry_trace: Optional[SentryTransactionInfo] = None,
                                   webhook: Optional[WebhookNotify] = None) -> Tuple[
        FFMPEGResult, Optional[SentryTransactionInfo]]:

        fn_id = current_function_call_id()

        @SentryUtils.sentry_tracker(name="音频提取任务", op="ffmpeg.extract.audio", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_process(media: MediaSource, fn_id: str) -> FFMPEGResult:
            cache_filepath = f"{s3_mount}/{media.cache_filepath}"
            output_path = f"{output_path_prefix}/{config.modal_environment}/extract_audio/outputs/{fn_id}/output.wav"
            output_path, metadata = await VideoUtils.ffmpeg_extract_audio_async(cache_filepath, output_path)
            s3_outputs = local_copy_to_s3([output_path])
            return FFMPEGResult(urn=s3_outputs[0], metadata=metadata,
                                content_length=FileUtils.get_file_size(output_path), )

        result = await ffmpeg_process(media_source, fn_id=fn_id)
        return result, sentry_trace


    @app.function(timeout=600, cloud="aws",
                  max_containers=config.ffmpeg_worker_concurrency,
                  volumes={
                      s3_mount: 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 ffmpeg_corner_mirror(media: MediaSource,
                                   mirror_scale_down_size: int = 6,
                                   mirror_from_right: bool = True,
                                   mirror_position: tuple[float, float] = (40, 40),
                                   sentry_trace: Optional[SentryTransactionInfo] = None,
                                   webhook: Optional[WebhookNotify] = None) -> Tuple[
        FFMPEGResult, Optional[SentryTransactionInfo]]:
        fn_id = current_function_call_id()

        @SentryUtils.sentry_tracker(name="视频镜像去重", op="ffmpeg.mirror", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_process(media: MediaSource, func_id: str, mirror_scale_down_size: int = 6,
                                 mirror_from_right: bool = True,
                                 mirror_position: tuple[float, float] = (40, 40)) -> FFMPEGResult:
            media_filepath = f"{s3_mount}/{media.cache_filepath}"
            output_path = f"{output_path_prefix}/{config.modal_environment}/corner_mirror/outputs/{func_id}/output.mp4"
            local_output_filepath, metadata = await VideoUtils.ffmpeg_corner_mirror(media_path=media_filepath,
                                                                                    output_path=output_path,
                                                                                    mirror_from_right=mirror_from_right,
                                                                                    mirror_position=mirror_position,
                                                                                    mirror_scale_down_size=mirror_scale_down_size)

            s3_outputs = local_copy_to_s3([local_output_filepath])
            return FFMPEGResult(urn=s3_outputs[0], metadata=metadata,
                                content_length=FileUtils.get_file_size(local_output_filepath), )

        result = await ffmpeg_process(media=media, mirror_scale_down_size=mirror_scale_down_size, func_id=fn_id,
                                      mirror_from_right=mirror_from_right, mirror_position=mirror_position)
        if not sentry_trace:
            sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
                                                 x_baggage=sentry_sdk.get_baggage())
        return result, sentry_trace


    @app.function(timeout=600, cloud="aws",
                  max_containers=config.ffmpeg_worker_concurrency,
                  volumes={
                      s3_mount: 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 ffmpeg_overlay_gif(media: MediaSource, gif: MediaSource,
                                 sentry_trace: Optional[SentryTransactionInfo] = None,
                                 webhook: Optional[WebhookNotify] = None) -> Tuple[
        FFMPEGResult, Optional[SentryTransactionInfo]]:
        fn_id = current_function_call_id()

        @SentryUtils.sentry_tracker(name="视频叠加GIF特效", op="ffmpeg.overlay", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_process(media: MediaSource, func_id: str, gif: MediaSource) -> FFMPEGResult:
            media_filepath = f"{s3_mount}/{media.cache_filepath}"
            gif_filepath = f"{s3_mount}/{gif.cache_filepath}"
            output_path = f"{output_path_prefix}/{config.modal_environment}/overlay/outputs/{func_id}/output.mp4"
            local_output_filepath, metadata = await VideoUtils.ffmpeg_overlay_gif(media_path=media_filepath,
                                                                                  output_path=output_path,
                                                                                  overlay_gif_path=gif_filepath)
            s3_outputs = local_copy_to_s3([local_output_filepath])
            return FFMPEGResult(urn=s3_outputs[0], metadata=metadata,
                                content_length=FileUtils.get_file_size(local_output_filepath), )

        result = await ffmpeg_process(media=media, func_id=fn_id, gif=gif)
        if not sentry_trace:
            sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
                                                 x_baggage=sentry_sdk.get_baggage())
        return result, sentry_trace


    @app.function(timeout=600, cloud="aws",
                  max_containers=config.ffmpeg_worker_concurrency,
                  volumes={
                      s3_mount: 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 ffmpeg_zoom_loop(media: MediaSource, duration: int = 6, zoom: float = 0.1,
                               sentry_trace: Optional[SentryTransactionInfo] = None,
                               webhook: Optional[WebhookNotify] = None) -> Tuple[
        FFMPEGResult, Optional[SentryTransactionInfo]]:
        fn_id = current_function_call_id()

        @SentryUtils.sentry_tracker(name="视频放大缩小去重", op="ffmpeg.zoom.loop", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_process(media: MediaSource, func_id: str, duration: int = 6,
                                 zoom: float = 0.1) -> FFMPEGResult:
            media_filepath = f"{s3_mount}/{media.cache_filepath}"
            output_path = f"{output_path_prefix}/{config.modal_environment}/zoom_loop/outputs/{func_id}/output.mp4"
            local_output_filepath, metadata = await VideoUtils.ffmpeg_zoom_loop(media_path=media_filepath,
                                                                                output_path=output_path,
                                                                                duration=duration,
                                                                                zoom=zoom)
            s3_outputs = local_copy_to_s3([local_output_filepath])
            return FFMPEGResult(urn=s3_outputs[0], metadata=metadata,
                                content_length=FileUtils.get_file_size(local_output_filepath), )

        result = await ffmpeg_process(media=media, duration=duration, zoom=zoom, func_id=fn_id)
        if not sentry_trace:
            sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
                                                 x_baggage=sentry_sdk.get_baggage())
        return result, sentry_trace


    @app.function(timeout=600, cloud="aws",
                  max_containers=config.ffmpeg_worker_concurrency,
                  volumes={
                      s3_mount: 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 ffmpeg_bgm_nosie_reduce(media: MediaSource, bgm: MediaSource, noise_sample: Optional[MediaSource] = None,
                                      video_volume: float = 1.4, music_volume: float = 0.1,
                                      sentry_trace: Optional[SentryTransactionInfo] = None,
                                      webhook: Optional[WebhookNotify] = None) -> Tuple[
        FFMPEGResult, Optional[SentryTransactionInfo]]:
        fn_id = current_function_call_id()

        @SentryUtils.sentry_tracker(name="视频混合BGM降噪", op="ffmpeg.bgm.nosie.reduce", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_process(video: MediaSource, bgm: MediaSource, func_id: str,
                                 video_volume: float = 1.4, music_volume: float = 0.1,
                                 noise_sample: Optional[MediaSource] = None) -> FFMPEGResult:
            local_input_filepath = f"{s3_mount}/{video.cache_filepath}"
            bgm_filepath = f"{s3_mount}/{bgm.cache_filepath}"
            noise_sample_path = f"{s3_mount}/{noise_sample.cache_filepath}" if noise_sample else None
            temp_audio_path = f"{output_path_prefix}/{config.modal_environment}/bgm_nosie_reduce/outputs/{func_id}/input.wav"
            output_path = f"{output_path_prefix}/{config.modal_environment}/bgm_nosie_reduce/outputs/{func_id}/output.mp4"
            local_output_filepath, metadata = await VideoUtils.ffmpeg_mix_bgm_with_noise_reduce(
                media_path=local_input_filepath,
                bgm_audio_path=bgm_filepath,
                video_volume=video_volume,
                music_volume=music_volume,
                noise_sample_path=noise_sample_path,
                temp_audio_path=temp_audio_path,
                output_path=output_path
            )

            s3_outputs = local_copy_to_s3([local_output_filepath])
            return FFMPEGResult(urn=s3_outputs[0], metadata=metadata,
                                content_length=FileUtils.get_file_size(local_output_filepath), )

        result = await ffmpeg_process(video=media, bgm=bgm, video_volume=video_volume, func_id=fn_id,
                                      music_volume=music_volume, noise_sample=noise_sample)

        if not sentry_trace:
            sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
                                                 x_baggage=sentry_sdk.get_baggage())
        return result, sentry_trace


    @app.function(timeout=600, cloud="aws",
                  max_containers=config.ffmpeg_worker_concurrency,
                  volumes={
                      s3_mount: 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 ffmpeg_subtitle_apply(media: MediaSource, subtitle: MediaSource,
                                    fonts: List[MediaSource], sentry_trace: Optional[SentryTransactionInfo] = None,
                                    webhook: Optional[WebhookNotify] = None) -> Tuple[
        FFMPEGResult, Optional[SentryTransactionInfo]]:
        fn_id = current_function_call_id()

        @SentryUtils.sentry_tracker(name="视频叠加字幕", op="ffmpeg.subtitle.apply", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_process(video: MediaSource, subtitle: MediaSource,
                                 fonts: List[MediaSource], func_id: str) -> FFMPEGResult:
            media_path = f"{s3_mount}/{video.cache_filepath}"
            subtitle_path = f"{s3_mount}/{subtitle.cache_filepath}"
            output_path = f"{output_path_prefix}/{config.modal_environment}/subtitle_apply/{func_id}/output.mp4"
            font_dir = f"{output_path_prefix}/{config.modal_environment}/subtitle_apply/{func_id}/fonts"
            os.makedirs(font_dir, exist_ok=True)
            local_fonts = [f"{s3_mount}/{font.cache_filepath}" for font in fonts]
            for font in local_fonts:
                font_filename = os.path.basename(font)
                shutil.copy(font, f"{font_dir}/{font_filename}")
            local_output, metadata = await VideoUtils.ffmpeg_subtitle_apply(media_path=media_path,
                                                                            subtitle_path=subtitle_path,
                                                                            font_dir=font_dir, output_path=output_path)
            s3_outputs = local_copy_to_s3([local_output])
            return FFMPEGResult(urn=s3_outputs[0], metadata=metadata,
                                content_length=FileUtils.get_file_size(local_output), )

        result = await ffmpeg_process(video=media, subtitle=subtitle, fonts=fonts, func_id=fn_id)
        if not sentry_trace:
            sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
                                                 x_baggage=sentry_sdk.get_baggage())
        return result, sentry_trace


    @app.function(timeout=600, cloud="aws",
                  max_containers=config.ffmpeg_worker_concurrency,
                  volumes={
                      s3_mount: 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 ffmpeg_loop_fill(media: MediaSource, audio: MediaSource,
                               sentry_trace: Optional[SentryTransactionInfo] = None,
                               webhook: Optional[WebhookNotify] = None) -> Tuple[
        FFMPEGResult, Optional[SentryTransactionInfo]]:
        fn_id = current_function_call_id()

        @SentryUtils.sentry_tracker(name="视频匹配音频长度", op="ffmpeg.loop.fill", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_process(media: MediaSource, audio: MediaSource, func_id: str) -> FFMPEGResult:
            video_path = f"{s3_mount}/{media.cache_filepath}"
            audio_path = f"{s3_mount}/{audio.cache_filepath}"

            local_output, metadata = await VideoUtils.ffmpeg_fill_longest(video_path=video_path,
                                                                          audio_path=audio_path,
                                                                          output_path=f"{output_path_prefix}/{config.modal_environment}/loop_fill/{func_id}/output.mp4")
            s3_outputs = local_copy_to_s3([local_output])
            return FFMPEGResult(urn=s3_outputs[0], metadata=metadata,
                                content_length=FileUtils.get_file_size(local_output), )

        result = await ffmpeg_process(media=media, audio=audio, func_id=fn_id)

        if not sentry_trace:
            sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
                                                 x_baggage=sentry_sdk.get_baggage())

        return result, sentry_trace


    @app.function(timeout=1800, cloud="aws",
                  max_containers=config.ffmpeg_worker_concurrency,
                  cpu=(0.5, 64),
                  volumes={
                      s3_mount: 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 ffmpeg_convert_stream(media: MediaSource,
                                    sentry_trace: Optional[SentryTransactionInfo] = None,
                                    webhook: Optional[WebhookNotify] = None) -> Tuple[
        FFMPEGResult, Optional[SentryTransactionInfo]]:
        fn_id = current_function_call_id()

        @SentryUtils.sentry_tracker(name="处理直播流转换为MP4视频任务", op="ffmpeg.convert.stream", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_process(media_stream: MediaSource, func_id: str) -> FFMPEGResult:
            match media_stream.protocol:
                case MediaProtocol.hls:
                    stream_url = media_stream.path
                case MediaProtocol.s3:
                    stream_url = media_stream.cache_filepath
                case _:
                    raise NotImplementedError(f"暂不支持使用{media_stream.protocol.value}协议")
            output_path = f"{output_path_prefix}/{config.modal_environment}/convert_stream/{func_id}/output.mp4"

            local_output, metadata = await VideoUtils.ffmpeg_convert_stream_media(media_stream_url=stream_url,
                                                                                  output_path=output_path)
            s3_outputs = local_copy_to_s3([local_output])

            return FFMPEGResult(urn=s3_outputs[0], metadata=metadata,
                                content_length=FileUtils.get_file_size(local_output), )

        result = await ffmpeg_process(media_stream=media, func_id=fn_id)

        if not sentry_trace:
            sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
                                                 x_baggage=sentry_sdk.get_baggage())

        return result, sentry_trace


    @app.function(timeout=1800, cloud="aws",
                  max_containers=config.ffmpeg_worker_concurrency,
                  volumes={
                      s3_mount: 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 ffmpeg_extract_frame(media: MediaSource, frame_index: int,
                                   sentry_trace: Optional[SentryTransactionInfo] = None,
                                   webhook: Optional[WebhookNotify] = None) -> Tuple[
        FFMPEGResult, Optional[SentryTransactionInfo]]:
        fn_id = current_function_call_id()

        @SentryUtils.sentry_tracker(name="获取指定的视频帧画面输出为图片", op="ffmpeg.extract.frame", fn_id=fn_id,
                                    sentry_trace_id=sentry_trace.x_trace_id if sentry_trace else None,
                                    sentry_baggage=sentry_trace.x_baggage if sentry_trace else None)
        @SentryUtils.webhook_handler(webhook=webhook, func_id=fn_id)
        async def ffmpeg_process(media: MediaSource, frame_index: int, func_id: str) -> FFMPEGResult:
            match media.protocol:
                case MediaProtocol.hls:
                    video_path = f"{media.path}"
                case MediaProtocol.s3:
                    video_path = f"{s3_mount}/{media.cache_filepath}"
                case MediaProtocol.vod:
                    video_path = f"{s3_mount}/{media.cache_filepath}"
                case _:
                    raise NotImplementedError(f"暂不支持此协议")
            output_path = f"{output_path_prefix}/{config.modal_environment}/extract_frame/{func_id}/output.jpg"
            local_output, metadata = await VideoUtils.ffmpeg_extract_frame_image(video_path=video_path,
                                                                                 frame_index=frame_index,
                                                                                 output_path=output_path)
            s3_outputs = local_copy_to_s3([local_output])
            return FFMPEGResult(urn=s3_outputs[0], metadata=metadata,
                                content_length=FileUtils.get_file_size(local_output), )

        result = await ffmpeg_process(media=media, frame_index=frame_index, func_id=fn_id)
        if not sentry_trace:
            sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
                                                 x_baggage=sentry_sdk.get_baggage())
        return result, sentry_trace


    hls_recording_volume = modal.Volume.from_name("stream_records", create_if_missing=True)
    hls_recording_mount_point = "/mnt/stream_records"

    from watchdog.events import FileMovedEvent, FileCreatedEvent, FileSystemEventHandler
    from watchdog.observers import Observer
    import boto3
    from botocore.client import BaseClient, Config


    class PlaylistEventHandler(FileSystemEventHandler):
        update_counter: int = 0
        fn_id: str
        webhook: Optional[WebhookNotify] = None
        s3_mount_output_dir: str
        s3_root_output_dir: str
        boto3_client: BaseClient

        def __init__(self, fn_id: str, s3_mount_output_dir: str, webhook: Optional[WebhookNotify] = None, *args,
                     **kwargs):
            super().__init__(*args, **kwargs)
            self.webhook = webhook
            self.fn_id = fn_id
            self.s3_mount_output_dir = s3_mount_output_dir
            self.boto3_client = boto3.client("s3",
                                             aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
                                             aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
                                             region_name=config.S3_region,
                                             endpoint_url="https://s3-accelerate.amazonaws.com",
                                             config=Config(
                                                 s3={'addressing_style': 'virtual'},
                                                 signature_version='s3v4', )
                                             )
            self.s3_root_output_dir = s3_mount_output_dir.replace(s3_mount, "")


        def on_created(self, event: FileCreatedEvent) -> None:
            logger.info(f"[created] {event.src_path}")
            if event.src_path.endswith(".ts"):
                filename = os.path.basename(event.src_path)
                mount_path = f"{self.s3_mount_output_dir}/{filename}"
                # 将Volume内的ts文件复制到S3挂载点
                shutil.copy(event.src_path, mount_path)
                logger.info(f"[copy] {event.src_path} -> {mount_path}")
            else:
                return

        def on_moved(self, event: FileMovedEvent) -> None:
            logger.info(f"[moved] {event.src_path} -> {event.dest_path}")
            if not event.dest_path.endswith(".m3u8"):
                return
            filename = os.path.basename(event.dest_path)
            filename.replace('.tmp', '')
            mount_path = f"{self.s3_mount_output_dir}/{filename}"
            # 将Volume内的playlist.m3u8.tmp复制到S3挂载点的playlist.m3u8文件
            try:
                shutil.copy(event.dest_path, mount_path)
            except Exception as e:
                logger.exception(e)
                filename = os.path.basename(event.dest_path)
                self.boto3_client.upload_file(event.dest_path, f"{self.s3_root_output_dir}/{filename}")
            logger.info(f"[copy] {event.dest_path} -> {mount_path}")
            self.update_counter += 1
            if self.webhook and self.update_counter == 1:
                logger.info("[Start] webhook trigger")
                try:
                    self.webhook_on_start()
                except Exception as e:
                    logger.exception(e)

        @backoff.on_exception(exception=Exception, wait_gen=backoff.constant,
                              max_time=5, max_tries=5, raise_on_giveup=True)
        def webhook_on_start(self):
            """
            开始录制第一次更新时回调
            """
            webhook = self.webhook
            if webhook.method is not WebhookMethodEnum.POST:
                logger.warning(f"webhook method {webhook.method.value} not supported")
            body = BaseFFMPEGTaskStatusResponse(taskId=self.fn_id,
                                                task_type="ffmpeg_stream_record_as_hls",
                                                status=TaskStatus.running).model_dump()
            response = httpx.post(url=webhook.endpoint.__str__(),
                                  json=body,
                                  headers=webhook.headers)
            response.raise_for_status()
            logger.info(f"[Start] webhook {response.status_code} {response.text}")


    @app.function(timeout=43200 + 300,  # 最长处理12h的录制任务 + 5分钟的清理缓存工作
                  cloud="aws",
                  # todo: 暂时不限制最大同时存在的录制数量
                  # max_containers=config.ffmpeg_worker_concurrency,
                  volumes={
                      s3_mount: modal.CloudBucketMount(
                          bucket_name=config.S3_bucket_name,
                          secret=modal.Secret.from_name("aws-s3-secret",
                                                        environment_name=config.modal_environment),
                      ),
                      hls_recording_mount_point: hls_recording_volume
                  }, )
    @modal.concurrent(max_inputs=5)
    async def ffmpeg_stream_record_as_hls(stream_url: str, segment_duration: int, recording_timeout: int,
                                          webhook: Optional[WebhookNotify] = None, monitor_timeout: int = 36000,
                                          sentry_trace: Optional[SentryTransactionInfo] = None, ) -> Tuple[
        FFMPEGResult, Optional[SentryTransactionInfo]]:
        fn_id = current_function_call_id()
        output_dir = f"{config.modal_environment}/records/hls/{fn_id}"
        s3_mount_output_dir = f"{s3_mount}/{output_dir}"
        os.makedirs(s3_mount_output_dir, exist_ok=True)
        volume_output_dir = f"{hls_recording_mount_point}/{output_dir}"
        os.makedirs(volume_output_dir, exist_ok=True)
        logger.info(f"manifest = {volume_output_dir}/playlist.m3u8")

        @backoff.on_exception(exception=Exception, wait_gen=backoff.constant,
                              max_time=5, max_tries=5, raise_on_giveup=False)
        async def webhook_on_end(fn_id: str, webhook: WebhookNotify, result: FFMPEGResult):
            if webhook.method is not WebhookMethodEnum.POST:
                logger.warning(f"webhook method {webhook.method.value} not supported")
            body = BaseFFMPEGTaskStatusResponse(taskId=fn_id,
                                                task_type="ffmpeg_stream_record_as_hls",
                                                status=TaskStatus.success,
                                                results=[result])
            response = httpx.post(url=webhook.endpoint.__str__(),
                                  json=body.model_dump(),
                                  headers=webhook.headers)
            logger.info(f"[End] webhook {response.status_code} {response.text}")

        @backoff.on_exception(exception=Exception, wait_gen=backoff.constant,
                              max_time=5, max_tries=5, raise_on_giveup=False)
        async def webhook_on_error(fn_id: str, webhook: WebhookNotify, error: str):
            if webhook.method is not WebhookMethodEnum.POST:
                logger.warning(f"webhook method {webhook.method.value} not supported")
            body = BaseFFMPEGTaskStatusResponse(taskId=fn_id,
                                                task_type="ffmpeg_stream_record_as_hls",
                                                error=error,
                                                status=TaskStatus.failed).model_dump()
            response = httpx.post(url=webhook.endpoint.__str__(),
                                  json=body,
                                  headers=webhook.headers)
            logger.info(f"[End] webhook {response.status_code} {response.text}")

        playlist_handler = PlaylistEventHandler(webhook=webhook, fn_id=fn_id, s3_mount_output_dir=s3_mount_output_dir)
        playlist_observer = Observer()
        os.makedirs(volume_output_dir, exist_ok=True)
        # 监控本地Volume下录制缓存目录
        playlist_observer.schedule(playlist_handler, path=volume_output_dir, recursive=False)
        playlist_observer.start()
        try:
            await VideoUtils.ffmpeg_stream_record_as_hls(stream_url=stream_url,
                                                         segment_duration=segment_duration,
                                                         stream_content_timeout=recording_timeout,
                                                         stream_monitor_timeout=monitor_timeout,
                                                         segments_output_dir=volume_output_dir,
                                                         playlist_output_dir=volume_output_dir)
        except Exception as e:
            logger.exception(e)
            playlist_observer.stop()
            if webhook:
                await webhook_on_error(webhook=webhook, fn_id=fn_id,
                                       error=e.message if hasattr(e, 'message') else str(e))
            raise e
        playlist_observer.stop()
        content_length = FileUtils.get_folder_size(volume_output_dir)
        metadata = VideoUtils.ffprobe_media_metadata(media_path=f"{volume_output_dir}/playlist.m3u8")
        result = FFMPEGResult(urn=f"s3://{config.S3_region}/{config.S3_bucket_name}/{output_dir}/playlist.m3u8",
                              metadata=metadata,
                              content_length=content_length, )
        if webhook:
            await webhook_on_end(webhook=webhook, fn_id=fn_id, result=result)
        if not sentry_trace:
            sentry_trace = SentryTransactionInfo(x_trace_id=sentry_sdk.get_traceparent(),
                                                 x_baggage=sentry_sdk.get_baggage())
        shutil.rmtree(volume_output_dir)
        return result, sentry_trace


    @app.function(timeout=43200 + 300,  # 最长处理12h的录制任务 + 5分钟的清理缓存工作
                  cloud="aws",
                  volumes={
                      s3_mount: modal.CloudBucketMount(
                          bucket_name=config.S3_bucket_name,
                          secret=modal.Secret.from_name("aws-s3-secret",
                                                        environment_name=config.modal_environment),
                      ),
                      hls_recording_mount_point: hls_recording_volume
                  }, )
    @modal.concurrent(max_inputs=1)
    async def ffmpeg_stream_record_restore(fn_id: str):

        def get_files_set(directory: str) -> Set[str]:
            """
            获取目录下所有文件的相对路径集合

            Args:
                directory (str): 目录路径

            Returns:
                Set[str]: 文件相对路径的集合
            """
            directory_path = Path(directory)
            return {str(f.relative_to(directory_path)) for f in directory_path.rglob("*") if f.is_file()}

        def copy_directory_contents(src_dir: str, dst_dir: str):
            """
            使用集合操作优化文件复制，跳过已存在的文件

            Args:
                src_dir (str): 源目录路径
                dst_dir (str): 目标目录路径
            """
            src_path = Path(src_dir)
            dst_path = Path(dst_dir)

            # 确保目标目录存在
            dst_path.mkdir(parents=True, exist_ok=True)

            # 获取源目录和目标目录的文件集合
            src_files = get_files_set(src_dir)
            dst_files = get_files_set(dst_dir) if dst_path.exists() else set()

            # 计算需要复制的文件（源目录有但目标目录没有的文件）
            files_to_copy = src_files - dst_files

            # 复制文件
            for rel_path in files_to_copy:
                src_file = src_path / rel_path
                dst_file = dst_path / rel_path

                # 确保目标文件的父目录存在
                dst_file.parent.mkdir(parents=True, exist_ok=True)

                # 复制文件
                shutil.copy(src_file, dst_file)
                logger.info(f"已复制: {rel_path}")

            logger.info(f"\n复制完成:")
            logger.info(f"源目录文件数: {len(src_files)}")
            logger.info(f"目标目录文件数: {len(dst_files)}")
            logger.info(f"本次复制文件数: {len(files_to_copy)}")

        output_dir = f"{config.modal_environment}/records/hls/{fn_id}"
        s3_mount_output_dir = f"{s3_mount}/{output_dir}"
        volume_output_dir = f"{hls_recording_mount_point}/{output_dir}"

        copy_directory_contents(volume_output_dir, s3_mount_output_dir)
        shutil.copy(f"{volume_output_dir}/playlist.m3u8", f"{s3_mount_output_dir}/playlist.m3u8")

        playlist_handler = PlaylistEventHandler(webhook=None, fn_id=fn_id,
                                                s3_mount_output_dir=s3_mount_output_dir)
        playlist_observer = Observer()
        # 监控本地Volume下录制缓存目录
        playlist_observer.schedule(playlist_handler, path=volume_output_dir, recursive=False)
        status = await ModalUtils.get_modal_task_status(fn_id)
        if status.status == TaskStatus.running:
            playlist_observer.start()
            while (status.status == TaskStatus.running):
                await asyncio.sleep(5)
                status = await ModalUtils.get_modal_task_status(fn_id)
            playlist_observer.stop()
            logger.info("Stream restore end")
