"""Video upload application on modal."""

import concurrent
import math
import json
import logging
import os
import time
from collections import namedtuple
from typing import Literal

import boto3
import ffmpeg
import modal
import requests
from datadog import initialize, statsd

from suno_utils.worker.video_utils.video_sprite import process_video_sprite
from suno_utils.utils.image_extraction import ImageExtractionError
from suno_utils.worker.schema import QueueItem
from suno_utils.worker.settings import s3_client
from suno_utils.worker.utils import retry_decorator
from suno_utils.worker.modal_base import get_modal_base_image

logger = logging.getLogger(__name__)
logging.basicConfig()
logger.setLevel(logging.INFO)

############## CHANGE THESE ##############
DEPLOYMENT_TYPE = "dev"

##########################################
DEPLOYMENT_TYPES = {"dev", "prod"}
assert DEPLOYMENT_TYPE in DEPLOYMENT_TYPES
# orchestrator talks to studio api
APP_NAME = f"upload-video-{DEPLOYMENT_TYPE}"
assert APP_NAME.endswith(DEPLOYMENT_TYPE)
MOUNT_PATH = "/suno/models"
UPLOADS_S3_BUCKET = "suno-data-uploads"
RAW_UPLOADS_S3_BUCKET = "suno-uploads"
retry_s3_download = retry_decorator(3, wait_seconds=20)(s3_client.download_file)
retry_s3_upload = retry_decorator(3, wait_seconds=20)(s3_client.upload_file)
Dimensions = namedtuple("Dimensions", ["width", "height"])
PreviewFormat = Literal["gif", "mp4"]
DEFAULT_PREVIEW_FORMAT: PreviewFormat = "mp4"
PREVIEW_DURATIONS = {"gif": 2, "mp4": 10}

# for maximum video upload duration
MAX_SCENES_UPLOAD_DURATION = 31
MAX_COVER_VIDEO_UPLOAD_DURATION = 11
MAX_HOOK_VIDEO_UPLOAD_DURATION = 121
# arbitrary number, just for better experience
MIN_UPLOAD_DURATION = 1

aws_secret = modal.Secret.from_name("studio-aws")
SECRETS = [
    aws_secret,
    modal.Secret.from_dict(
        {
            "DD_SITE": "datadoghq.com",
            "DD_ENV": DEPLOYMENT_TYPE,
            "DD_SERVICE": DEPLOYMENT_TYPE,
            "DD_LOGS_ENABLED": "false",
            "DD_TRACE_ENABLED": "true" if DEPLOYMENT_TYPE == "dev" else "false",
        },
    ),
    modal.Secret.from_name("datadog-metrics"),
    modal.Secret.from_name("api-callback-token"),
    modal.Secret.from_name("hive-secret"),
]


def download_model_wrapper_b() -> None:
    pass


base_image = get_modal_base_image().add_local_python_source("suno_utils", copy=False)

app = modal.App(APP_NAME, image=base_image)


# TODO experiment with the parameters
@app.cls(
    cpu=1,
    secrets=SECRETS,
    timeout=240,
    min_containers=1,
    scaledown_window=600,
    retries=modal.Retries(
        max_retries=1,
        backoff_coefficient=2.0,
        initial_delay=5.0,
    ),
    cloud="aws",
    region="us-east",
    buffer_containers=0 if DEPLOYMENT_TYPE == "dev" else 1,
)
@modal.concurrent(max_inputs=4)
class VideoUploadStub:
    """Video upload stub and functions."""

    def __init__(self):
        """Set up VideoUploadStub."""
        self.modal_f_image_description = modal.Cls.lookup(
            f"image-extraction-{DEPLOYMENT_TYPE}",
            "ImageExtractionStub",
        )().generate_image_description

        options = {"statsd_host": "127.0.0.1", "statsd_port": 8125}
        initialize(**options)

    @modal.method()
    def trigger_mediaconvert_job(self, queue_item_json: str):
        """Loop video for audio duration, upload to mediaconvert source bucket"""
        item = QueueItem(**json.loads(queue_item_json))
        audio_s3_id = item.metadata.get("audio_s3_id", None)
        video_key = item.metadata.get("video_key", None)
        video_duration = item.metadata.get("video_duration", None)
        audio_duration = item.metadata.get("audio_duration", None)

        # download video_key from s3
        temp_input_path = f"/tmp/tmp_{video_key.split('/')[-1]}"
        retry_s3_download(UPLOADS_S3_BUCKET, f"studio/uploads/{video_key}.mp4", temp_input_path)
        print(f"Downloaded video from {video_key}. ID {item.id}")

        # Calculate how many times we need to loop the video
        if not all([video_duration, audio_duration]):
            print(f"Missing required duration parameters. ID {item.id}")
            return {"success": False, "error": "Missing duration parameters"}

        # Calculate loop count (rounded up)
        loop_count = math.ceil(audio_duration / video_duration)

        # Create output path for the looped video
        looped_video_path = f"/tmp/looped_{video_key.split('/')[-1]}.mp4"

        try:
            # Create a file with repeated inputs
            input_list_file = "/tmp/input_list.txt"
            with open(input_list_file, "w") as f:
                for _ in range(loop_count):
                    f.write(f"file '{temp_input_path}'\n")

            # Use ffmpeg concat demuxer to loop the video
            (
                ffmpeg.input(input_list_file, format="concat", safe=0)
                .output(looped_video_path, c="copy", t=str(audio_duration))
                .run(capture_stdout=True, capture_stderr=True)
            )

            # Upload to mediaconvert source bucket
            first_two = item.id[:2]
            second_two = item.id[2:4]
            dest_key = f"{first_two}/{second_two}/{item.id}.mp4"

            # Upload to the mediaconvert source bucket
            s3_client = boto3.client("s3")
            s3_client.upload_file(
                looped_video_path,
                "suno-media-sour",  # Mediaconvert source bucket
                dest_key,
                ExtraArgs={
                    "Metadata": {
                        "audio-file-bucket": "suno-data-uploads",
                        "audio-file-key": f"studio/uploads/{audio_s3_id}.mp3",
                        "environment": DEPLOYMENT_TYPE,
                        "callback-url": item.callback_url,
                    }
                },
            )

            print(f"Successfully uploaded looped video to mediaconvert source bucket. ID {item.id}")
            return {"success": True, "dest_key": dest_key}

        except ffmpeg.Error as e:
            print(
                f"FFmpeg error while looping video: {e.stderr.decode() if e.stderr else str(e)}. ID {item.id}"
            )
            return {"success": False, "error": "FFmpeg error"}
        except Exception as e:
            print(f"Error processing video loop: {str(e)}. ID {item.id}")
            return {"success": False, "error": str(e)}

    @modal.method()
    def process_uploaded_video(self, queue_item_json: str):
        """Transcode, moderate and upload video to S3, then generate the description."""
        t0 = time.time()
        item = QueueItem(**json.loads(queue_item_json))
        upload_id = item.id
        video_cover_clip_id = item.metadata.get("clip_id", None)
        is_clip_video_cover = video_cover_clip_id is not None or item.metadata.get(
            "is_video_cover", False
        )
        require_video_sprite = item.metadata.get("require_video_sprite", False)
        fail_task_on_moderation = item.metadata.get("fail_task_on_moderation", True)
        video_upload_type = item.metadata.get("video_upload_type", None)
        title = item.title
        if not title:
            title = "Uploaded Video"
        # this is the key on s3 that maps to the uploaded video file
        upload_key = item.metadata.get("upload_key", "")
        print(f"upload_key: {upload_key} \n Upload id {upload_id}")

        # Define the S3 path for the uploaded file
        try:
            s3_client.get_object(Bucket=RAW_UPLOADS_S3_BUCKET, Key=upload_key)
        except:
            print(
                f"File {upload_id} doesn't exist on s3. Did the upload finish? Upload id {upload_id}\n"
            )
            item.notify_progress(
                {
                    "id": item.id,
                    "request_id": upload_id,
                    "type": "upload_video",
                    "ok": 0,
                    "error_type": "upload_failure_not_found",
                    "error_message": "Can't fetch the uploaded video.",
                },
                blocking=True,
            )
            return {}

        # Download the file from S3 to a temporary location
        temp_input_path = f"/tmp/tmp_{upload_key.split('/')[-1]}"
        retry_s3_download(RAW_UPLOADS_S3_BUCKET, upload_key, temp_input_path)
        print(f"Downloaded {upload_key} in {time.time() - t0:.3f}s  Upload id {upload_id}\n")
        result = {
            "success": False,
            "duration": -1,
            "s3_id": None,
            "image_s3_ids": [],
            "video_dimensions": None,
        }

        # Read video metadata, fetch duration and dimensions
        try:
            probe = ffmpeg.probe(temp_input_path)

            video_dimensions = self._get_video_dimensions(probe)
            result["video_dimensions"] = video_dimensions

            result["duration"] = float(probe["format"]["duration"])
        except Exception as e:
            print(f"Error getting duration: {e} Upload id {upload_id}\n")
            return result

        max_duration = self._get_max_duration(video_upload_type, is_clip_video_cover)
        if result["duration"] > max_duration or result["duration"] < MIN_UPLOAD_DURATION:
            print(
                f"Video duration {result['duration']} is not within the allowed range. Upload id {upload_id}\n"
            )
            item.notify_progress(
                {
                    "id": item.id,
                    "request_id": upload_id,
                    "type": "upload_video",
                    "ok": 0,
                    "error_type": "upload_failure_video_length",
                    "error_message": f"Video too {'long' if result['duration'] > max_duration else 'short'}",
                },
                blocking=True,
            )
            return result

        duration = int(result["duration"])
        video_dimensions = result["video_dimensions"]
        thumbnail_dimensions = self.get_thumbnail_dimensions(video_dimensions)
        # get duration bucket by 10s
        duration_bucket = (
            (duration + 9) // 10 * 10
        )  # round up to nearest 10 - buckets will be 10, 20, 30

        with concurrent.futures.ThreadPoolExecutor() as executor:
            # Submit the functions to the executor
            future_image_lyrics = None
            future_process_snapshot = None
            future_process_preview = None
            task = "scene"
            if video_upload_type == "video_hook":
                task = "hook"
                future_process_snapshot = executor.submit(
                    self._process_cover_image_from_video, upload_id, temp_input_path, video_dimensions
                )
            elif is_clip_video_cover:
                task = "cover"
                preview_format = item.metadata.get("preview_format", None) or DEFAULT_PREVIEW_FORMAT
                future_process_snapshot = executor.submit(
                    self._process_cover_image_from_video, upload_id, temp_input_path, video_dimensions
                )
                future_process_preview = executor.submit(
                    self._process_cover_preview_from_video,
                    upload_id,
                    temp_input_path,
                    thumbnail_dimensions,
                    duration,
                    preview_format,
                )
            else:  # scenes
                future_image_lyrics = executor.submit(
                    self._process_image_to_text,
                    upload_id,
                    temp_input_path,
                    duration,
                    duration_bucket,
                    thumbnail_dimensions,
                )

            maintain_audio = video_upload_type == "video_hook"
            future_transcode = executor.submit(
                self._process_video_transcode,
                upload_id,
                upload_key,
                temp_input_path,
                duration_bucket,
                maintain_audio,
            )
            future_nsfw_check = executor.submit(
                self._detect_nsfw, RAW_UPLOADS_S3_BUCKET, upload_key, upload_id, duration_bucket
            )

            if require_video_sprite:
                future_video_sprites = executor.submit(
                    process_video_sprite,
                    upload_id,
                    temp_input_path,
                    duration,
                    duration_bucket,
                    video_dimensions,
                    retry_s3_upload,
                )

        with statsd.timed(
            "video_processing",
            tags=[
                f"env:{DEPLOYMENT_TYPE}",
                f"video_upload_type:{video_upload_type}",
                "sub_task:transcode",
            ],
        ):
            # Wait for the results
            transcode_result = future_transcode.result()

        with statsd.timed(
            "video_processing",
            tags=[
                f"env:{DEPLOYMENT_TYPE}",
                f"video_upload_type:{video_upload_type}",
                "sub_task:moderation",
            ],
        ):
            # Handle NSFW check with error handling
            try:
                nsfw_result = future_nsfw_check.result()
            except Exception as e:
                logger.error(f"NSFW check failed for upload {upload_id}: {e}")
                # Default to safe (not NSFW) if check fails
                nsfw_result = False

        with statsd.timed(
            "video_processing",
            tags=[f"env:{DEPLOYMENT_TYPE}", f"video_upload_type:{video_upload_type}", "sub_task:sprite"],
        ):
            video_sprite_result = future_video_sprites.result() if require_video_sprite else None

        image_lyrics_result = None
        snapshot_process_result = None
        image_s3_id = None
        gif_s3_id = None
        video_preview_s3_id = None
        description = None

        # wait for image-to-text - for scenes
        if future_image_lyrics is not None:
            with statsd.timed(
                "video_processing",
                tags=[
                    f"env:{DEPLOYMENT_TYPE}",
                    f"video_upload_type:{video_upload_type}",
                    "sub_task:image_lyrics",
                ],
            ):
                image_lyrics_result = future_image_lyrics.result()
                image_s3_id = image_lyrics_result["image_s3_id"]
                description = image_lyrics_result["lyrics"]
        # wait for processing snapshot / cover image - for clip video cover
        else:
            with statsd.timed(
                "video_processing",
                tags=[
                    f"env:{DEPLOYMENT_TYPE}",
                    f"video_upload_type:{video_upload_type}",
                    "sub_task:snapshot",
                ],
            ):
                if future_process_snapshot is not None:
                    snapshot_process_result = future_process_snapshot.result()
                    image_s3_id = snapshot_process_result["image_s3_id"]
                if future_process_preview is not None:
                    preview_process_result = future_process_preview.result()
                    gif_s3_id = preview_process_result.get("gif_s3_id")
                    video_preview_s3_id = preview_process_result.get("mp4_s3_id")

        if video_sprite_result and video_sprite_result.success:
            item.notify_progress(
                {
                    "id": item.id,
                    "request_id": upload_id,
                    "type": "upload_video",
                    "ok": 1,
                    "status": "passed_video_sprite_generation",
                },
                blocking=True,
            )
        elif video_sprite_result and not video_sprite_result.success:
            item.notify_progress(
                {
                    "id": item.id,
                    "request_id": upload_id,
                    "type": "upload_video",
                    "ok": 0,
                    "error_type": "upload_failure_sprite",
                    "error_message": video_sprite_result.error_message,
                },
                blocking=True,
            )
            return {}  # stop processing

        if nsfw_result:
            item.notify_progress(
                {
                    "id": item.id,
                    "request_id": upload_id,
                    "type": "upload_video",
                    "ok": 0,
                    "error_type": "upload_failure_nsfw",
                    "error_message": "Video is not safe for work.",
                },
                blocking=True,
            )
            if fail_task_on_moderation:
                return {}
        else:
            item.notify_progress(
                {
                    "id": item.id,
                    "request_id": upload_id,
                    "type": "upload_video",
                    "ok": 1,
                    "status": "passed_nsfw_check",
                },
                blocking=True,
            )

        if transcode_result["success"]:
            # send a notification for success if get here
            item.notify_progress(
                {
                    "id": item.id,
                    "request_id": upload_id,
                    "type": "upload_video",
                    "ok": 1,
                    "status": "passed_video_processing",
                },
                blocking=True,
            )
        else:
            if not transcode_result["s3_id"]:
                item.notify_progress(
                    {
                        "id": item.id,
                        "request_id": upload_id,
                        "type": "upload_video",
                        "ok": 0,
                        "error_type": "upload_failure_decode_video",
                        "error_message": "Can't parse uploaded video. Source is corrupted.",
                    },
                    blocking=True,
                )
                return {}

        if image_lyrics_result is not None and not image_lyrics_result["success"]:
            item.notify_progress(
                {
                    "id": item.id,
                    "request_id": upload_id,
                    "type": "upload_video",
                    "ok": 0,
                    "error_type": "upload_failure_extract_lyrics",
                    "error_message": "Can't extract lyrics from video.",
                },
                blocking=True,
            )
            return {}

        if snapshot_process_result is not None and not snapshot_process_result["success"]:
            item.notify_progress(
                {
                    "id": item.id,
                    "request_id": upload_id,
                    "type": "upload_video",
                    "ok": 0,
                    "error_type": "upload_failure_snapshot",
                    "error_message": "Can't extract cover snapshot from video.",
                },
                blocking=True,
            )

        print(f"Dimensions are {video_dimensions} Upload id {upload_id}\n")
        # send back the information
        item.notify_progress(
            {
                "id": item.id,
                "request_id": upload_id,
                "type": "upload_video",
                "s3_id": transcode_result["s3_id"],
                "image_s3": image_s3_id,
                "gif_s3": gif_s3_id,
                "mp4_preview_s3": video_preview_s3_id,
                "title": title,
                "description": description,
                "duration": result["duration"],
                "is_description_lyrics": True,
                "video_cover_clip_id": video_cover_clip_id,
                "video_width": video_dimensions.width if video_dimensions else None,
                "video_height": video_dimensions.height if video_dimensions else None,
                "video_sprite": video_sprite_result.sprite_schema.to_dict()
                if video_sprite_result
                else None,
                "is_nsfw": nsfw_result,
            },
            blocking=True,
        )
        statsd.distribution(
            "video_processing.total_time",
            time.time() - t0,
            tags=[
                f"duration_bucket:{duration_bucket}",
                f"task:{task}",
                f"video_upload_type:{video_upload_type}",
            ],
        )
        print(f"Done with {upload_id}, {time.time() - t0:.3f}s \n Upload id {upload_id}")
        if image_lyrics_result:
            print(
                f"Generated lyrics for {upload_id}, {image_lyrics_result['lyrics']} Upload id {upload_id}\n"
            )
        else:
            print(f"Skipped generating lyrics for clip video cover. Upload id {upload_id}\n")
        return {}

    def _get_max_duration(self, video_upload_type: str, is_clip_video_cover: bool) -> int:
        if video_upload_type == "video_hook":
            return MAX_HOOK_VIDEO_UPLOAD_DURATION

        # reuse the logic for clip video cover for now, we can change this logic once we has a separate util function to collect all different config params
        if is_clip_video_cover:
            return MAX_COVER_VIDEO_UPLOAD_DURATION

        return MAX_SCENES_UPLOAD_DURATION

    def _get_video_dimensions(self, probe) -> Dimensions | None:
        """
        Gets the video dimensions from the output of an ffprobe
        """
        video_stream = next(
            (stream for stream in probe["streams"] if stream["codec_type"] == "video"), None
        )
        if video_stream:
            width = int(video_stream["width"])
            height = int(video_stream["height"])

            # sometimes width and height are swapped because the video is "rotated" - correct this before continuing
            rotation = next(
                (
                    side_data.get("rotation", 0)
                    for side_data in video_stream.get("side_data_list", [])
                    if side_data.get("side_data_type") == "Display Matrix"
                ),
                0,
            )
            if abs(rotation) in [90, 270]:
                width, height = height, width

            return Dimensions(width, height)

        return None

    def _process_video_transcode(
        self,
        upload_id: str,
        upload_key: str,
        temp_input_path: str,
        duration_bucket: int,
        maintain_audio: bool,
    ):
        """Transcode and upload video to S3"""
        print("\ninside process transcode\n")
        t0 = time.time()
        result = {"success": False, "s3_id": None}

        s3_id = f"video_upload_{upload_id}"
        s3_new_path = f"studio/uploads/{s3_id}.mp4"

        file_extension = temp_input_path.split(".")[-1].lower()
        if file_extension not in ["mp4", "mov", "avi", "mkv", "wmv", "webm"]:
            print(f"Unsupported file extension: {file_extension} Upload id {upload_id}\n")
            return result

        new_video_file_path = f"/tmp/{upload_id}.mp4"
        if file_extension == "mp4":
            # Remove audio and ensure reasonable bitrate (unless it's a hook video)
            try:
                if not maintain_audio:
                    # Remove audio completely
                    (
                        ffmpeg.input(temp_input_path)
                        .output(
                            new_video_file_path,
                            vcodec="libx264",
                            video_bitrate="2250k",  # Target bitrate of 2.25 Mbps
                            maxrate="2700k",  # Maximum bitrate (1.2× target)
                            bufsize="5400k",  # Buffer size (2× target)
                            preset="ultrafast",
                            an=None,  # Remove audio completely
                        )
                        .run(capture_stdout=True, capture_stderr=True)
                    )
                else:
                    # keep audio
                    (
                        ffmpeg.input(temp_input_path)
                        .output(
                            new_video_file_path,
                            vcodec="libx264",
                            video_bitrate="2250k",  # Target bitrate of 2.25 Mbps
                            maxrate="2700k",  # Maximum bitrate (1.2× target)
                            bufsize="5400k",  # Buffer size (2× target)
                            preset="ultrafast",
                            # audio settings
                            acodec="aac",
                            audio_bitrate="192k",
                            ar="44100",
                        )
                        .run(capture_stdout=True, capture_stderr=True)
                    )
            except ffmpeg.Error as e:
                print(
                    f"Error occurred during mp4 processing: {e.stderr.decode()} Upload id {upload_id}\n"
                )
                return result

        else:
            # Convert non mp4 file into mp4
            try:
                (
                    ffmpeg.input(temp_input_path)
                    .output(
                        new_video_file_path,
                        format="mp4",
                        vcodec="libx264",
                        acodec="aac",
                        audio_bitrate="192k",
                        preset="ultrafast",
                        ar="44100",
                    )
                    .run(capture_stdout=True, capture_stderr=True)
                )
                print(f"Conversion successful: {new_video_file_path}")
            except ffmpeg.Error as e:
                print(f"Error occurred during transcoding: {e.stderr.decode()} Upload id {upload_id}\n")
                return result
        try:
            retry_s3_upload(
                new_video_file_path,
                UPLOADS_S3_BUCKET,
                s3_new_path,
                ExtraArgs={
                    "ContentType": "video/mp4",
                },
            )
            # Log new s3 path for Modal logs.
            print(f"upload_id: {upload_id}, s3_new_path: {s3_new_path}")
            result["s3_id"] = s3_id
        except Exception as e:
            print(f"upload video: {upload_id} failed with error: {e}")
            return result

        result["success"] = True
        end_time = time.time()
        print(f"Done with transcode {end_time - t0:.3f}s Upload id {upload_id}\n")
        statsd.distribution(
            "video_processing.transcode", end_time - t0, tags=[f"duration_bucket:{duration_bucket}"]
        )
        return result

    def _process_cover_preview_from_video(
        self,
        upload_id: str,
        temp_input_path: str,
        image_dimensions: Dimensions | None,
        duration: int,
        format: PreviewFormat,
    ):
        """Generates and uploads a GIF preview for an uploaded video."""
        t0 = time.time()
        result = {"success": False, "gif_s3_id": "", "mp4_s3_id": ""}
        preview_s3_id = f"video_upload_{upload_id}_preview.{format}"
        preview_s3_without_ext = f"video_upload_{upload_id}_preview"
        output_path = f"/tmp/{preview_s3_id}"

        # Use the shorter of PREVIEW_DURATION or actual video duration
        preview_duration = min(PREVIEW_DURATIONS[format], duration)

        def _ffmpeg_gif():
            """Scale video and limit to 3fps"""
            (
                ffmpeg.input(temp_input_path, t=preview_duration)
                .filter("fps", fps=3)
                .filter(
                    "scale",
                    w=image_dimensions.width if image_dimensions else -1,
                    h=image_dimensions.height if image_dimensions else -1,
                )
                .output(output_path)
                .run(capture_stdout=True, capture_stderr=True)
            )

        def _ffmpeg_mp4():
            """Scale video, rip out audio"""
            (
                ffmpeg.input(temp_input_path, t=preview_duration)
                .filter(
                    "scale",
                    w=image_dimensions.width if image_dimensions else -1,
                    h=image_dimensions.height if image_dimensions else -1,
                )
                .filter("pad", "ceil(iw/2)*2", "ceil(ih/2)*2", 0, 0)
                .output(output_path, crf=23, an=None)
                .run(capture_stdout=True, capture_stderr=True)
            )

        try:
            # Generate preview using ffmpeg
            if format == "gif":
                _ffmpeg_gif()
            else:
                _ffmpeg_mp4()

            # Upload preview to S3
            s3_output_path = f"studio/uploads/{preview_s3_id}"
            content_types_by_format = {"gif": "image/gif", "mp4": "video/mp4"}
            retry_s3_upload(
                output_path,
                UPLOADS_S3_BUCKET,
                s3_output_path,
                ExtraArgs={"ContentType": f"{content_types_by_format[format]}"},
            )
            result["success"] = True
            result[f"{format}_s3_id"] = preview_s3_without_ext

        except Exception as e:
            print(
                f"Preview generation for video {upload_id} with format {format} failed with error: {e}, {e.stderr.decode() if hasattr(e, 'stderr') and e.stderr is not None else ''}"
            )

        print(
            f"Done with preview generation {time.time() - t0:.3f}s with format {format} Upload id {upload_id}\n"
        )
        return result

    def _process_cover_image_from_video(
        self,
        upload_id: str,
        temp_input_path: str,
        image_dimensions: Dimensions | None,
        t: int = 0,
    ):
        """Generates and uploads the cover image snapshot (thumbnail + large) for an uploaded video."""
        result = {"success": False, "image_s3_id": ""}
        t, image_s3_id, image_file, thumbnail_s3_id, thumbnail_file = self.process_time_slice(
            t, upload_id, temp_input_path, image_dimensions
        )
        if thumbnail_s3_id is None:
            return result

        # Upload thumbnail and large image to s3
        try:
            for s3_id, file in [(image_s3_id, image_file), (thumbnail_s3_id, thumbnail_file)]:
                s3_image_path = f"studio/uploads/{s3_id}.jpeg"
                retry_s3_upload(file, UPLOADS_S3_BUCKET, s3_image_path)
            result["image_s3_id"] = thumbnail_s3_id
        except Exception as e:
            print(f"upload video: {upload_id} failed with error: {e} Upload id {upload_id}\n")
            return result

        else:
            result["success"] = True
            result["image_s3_id"] = thumbnail_s3_id
        return result

    def get_thumbnail_dimensions(
        self, dimensions, short_side_length=360, long_side_length=-1
    ) -> Dimensions:
        width = dimensions.width
        height = dimensions.height

        return (
            Dimensions(short_side_length, long_side_length)
            if width <= height
            else Dimensions(long_side_length, short_side_length)
        )

    def process_time_slice(self, t, upload_id, temp_input_path, dimensions):
        """
        Returns an image s3 id and reference to local image file to upload for slice {t} of the video
        Also, for the first slice, returns an s3 id and reference to local image file for a thumbnail
        """
        # use base with prefix image_ for thumbnail and image_large for full size
        image_s3_base = f"video_upload_{upload_id}_snapshot_{t}s"

        image_s3_id = f"image_large_{image_s3_base}"
        image_file = f"/tmp/{image_s3_id}.jpeg"
        thumbnail_s3_id = None
        thumbnail_file = None

        try:
            ffmpeg.input(temp_input_path, ss=t).output(image_file, preset="ultrafast", vframes=1).run(
                capture_stdout=True, capture_stderr=True
            )

            # for the first slice, generate a thumbnail
            if t == 0 and dimensions:
                thumbnail_s3_id = f"image_{image_s3_base}"
                thumbnail_file = f"/tmp/{thumbnail_s3_id}.jpeg"
                # thumbnail_width, thumbnail_height = self.get_thumbnail_dimensions(dimensions)
                thumbnail_width, thumbnail_height = dimensions.width, dimensions.height
                ffmpeg.input(image_file).filter("scale", thumbnail_width, thumbnail_height).output(
                    thumbnail_file
                ).run(capture_stdout=True, capture_stderr=True)

        except ffmpeg.Error as e:
            print(
                f"ffmpeg error generating snapshot or thumbnail for {t}s: {e.stderr.decode()} Upload id {upload_id}\n"
            )
            return t, None, None, None, None
        except Exception as e:
            print(
                f"non-ffmpeg error generating snapshot or thumbnail for {t}s: {str(e)} Upload id {upload_id}\n"
            )
            return t, None, None, None, None

        return t, image_s3_id, image_file, thumbnail_s3_id, thumbnail_file

    def _process_image_to_text(
        self,
        upload_id: str,
        temp_input_path: str,
        duration: int,
        duration_bucket: int,
        image_dimensions: Dimensions | None,
    ):
        """Generate lyrics based on a list of images"""
        t0 = time.time()

        image_s3_ids = []
        image_files = []
        result = {"success": False, "lyrics": ""}

        start_time = time.time()
        # start processing slices
        try:
            with concurrent.futures.ThreadPoolExecutor() as executor:
                futures = [
                    executor.submit(
                        self.process_time_slice, t, upload_id, temp_input_path, image_dimensions
                    )
                    for t in range(0, duration, 6)
                ]
                results = {t: None for t in range(0, duration, 6)}
                thumbnail_result = None
                for future in concurrent.futures.as_completed(futures):
                    t, image_s3_id, image_file, thumbnail_s3_id, thumbnail_file = future.result()
                    if image_s3_id and image_file:
                        results[t] = (image_s3_id, image_file)
                    if thumbnail_s3_id and thumbnail_file:
                        thumbnail_result = (thumbnail_s3_id, thumbnail_file)
                for t in range(0, duration, 6):
                    if results[t]:
                        image_s3_id, image_file = results[t]
                        image_s3_ids.append(image_s3_id)
                        image_files.append(image_file)
                if thumbnail_result:
                    image_s3_ids.append(thumbnail_result[0])
                    image_files.append(thumbnail_result[1])
        except ffmpeg.Error as e:
            print(f"Error generating snapshots: {e.stderr.decode()} Upload id {upload_id}\n")
            return result

        end_image_extraction_time = time.time()

        print(f"Done with image extraction {time.time() - t0:.3f}s Upload id {upload_id}\n")
        try:
            for image_s3_id, image_file in zip(image_s3_ids, image_files):
                s3_image_path = f"studio/uploads/{image_s3_id}.jpeg"
                retry_s3_upload(image_file, UPLOADS_S3_BUCKET, s3_image_path)
            result["image_s3_id"] = image_s3_id
        except Exception as e:
            print(f"upload video: {upload_id} failed with error: {e} Upload id {upload_id}\n")
            return result

        end_snapshot_upload_time = time.time()

        print(f"Done with image upload {time.time() - t0:.3f}s Upload id {upload_id}\n")
        try:
            image_lyrics = self.modal_f_image_description.remote(
                QueueItem(
                    id=upload_id,
                    metadata={
                        "image_to_song_s3_ids": image_s3_ids,
                        "user_context": "",
                    },
                ).model_dump_json(),
                image_moderation_threshold=0,
            )
        # network errors in image extraction pipeline
        except Exception as e:
            print(f"Error in image extraction pipeline: {e} Upload id {upload_id}\n")
            return result

        end_image_lyrics_time = time.time()
        statsd.distribution(
            "video_processing.snapshot_creation",
            end_image_extraction_time - start_time,
            tags=[f"duration_bucket:{duration_bucket}"],
        )
        statsd.distribution(
            "video_processing.snapshot_upload",
            end_snapshot_upload_time - end_image_extraction_time,
            tags=[f"duration_bucket:{duration_bucket}"],
        )
        statsd.distribution(
            "video_processing.generate_image_descriptions",
            end_image_lyrics_time - end_snapshot_upload_time,
            tags=[f"duration_bucket:{duration_bucket}"],
        )
        statsd.distribution(
            "video_processing.image_to_text",
            end_image_lyrics_time - start_time,
            tags=[f"duration_bucket:{duration_bucket}"],
        )

        # system or user error
        if isinstance(image_lyrics, ImageExtractionError):
            print(f"Error in image extraction pipeline: {image_lyrics} Upload id {upload_id}\n")
            return None

        result["lyrics"] = image_lyrics
        result["success"] = True
        print(f"Done with image to text {time.time() - t0:.3f}s Upload id {upload_id}\n")
        return result

    def _detect_nsfw(
        self, bucket_name: str, upload_key: str, upload_id: str, duration_bucket: int
    ) -> bool:
        """Detect if the video is nsfw."""
        print(f"Starting NSFW check Upload id {upload_id}\n")
        video_moderation_threshold: float = 0.9
        s3_client = boto3.client("s3")
        presigned_url = s3_client.generate_presigned_url(
            "get_object",
            Params={"Bucket": bucket_name, "Key": upload_key},
            ExpiresIn=300,  # 5 minutes
        )

        headers = {"Authorization": f"Token {os.environ['HIVE_EAST_API_KEY']}"}
        data = {"url": presigned_url}

        start_time = time.time()
        model_response = requests.post(
            "https://api-va1.thehive.ai/api/v2/task/sync", headers=headers, data=data
        )

        # Record the latency
        end_time = time.time()
        total_time = end_time - start_time
        statsd.distribution(
            "video_processing.hive_moderation", total_time, tags=[f"duration_bucket:{duration_bucket}"]
        )

        # Parse the Hive API response
        response_dict = model_response.json()
        mod_classes = (
            response_dict.get("status", [{}])[0]
            .get("response", {})
            .get("output", [{}])[0]
            .get("classes", [])
        )
        print(
            f"Got NSFW check response from Hive, status is {response_dict.get('status', [{}])[0].get('status', None)} Upload id {upload_id}\n"
        )
        # Handle unexpected Hive API output
        if len(mod_classes) == 0:
            raise ValueError(f"No moderation classes found for video {upload_key}")
        # Check the Hive API `general_nsfw` class likelihood score
        general_nsfw_class = list(filter(lambda cls: cls["class"] == "general_nsfw", mod_classes))
        general_nsfw_score = general_nsfw_class[0].get("score", None) if general_nsfw_class else None
        # Handle unexpected Hive API output
        if general_nsfw_score is None:
            raise ValueError(f"No moderation score found for video {upload_key}")
        if general_nsfw_score > video_moderation_threshold:
            return True
        # If the likelihood is below the threshold, it means the video is safe for work
        else:
            return False


def _test_process_uploaded_video(model: VideoUploadStub):
    """
    Make sure you've got AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY set or these checks will fail
    """
    thumbnail_key = "studio/uploads/image_video_upload_testprocessuploadedvideo_snapshot_0s.jpeg"
    large_image_key = "studio/uploads/image_large_video_upload_testprocessuploadedvideo_snapshot_0s.jpeg"
    sprite_key = "studio/uploads/video_upload_sprite_testprocessuploadedvideo.jpeg"

    queue_item = (
        QueueItem(
            id="testprocessuploadedvideo",
            metadata={
                "upload_key": "test/d78819dd-4735-475c-9a28-8ae8f1cd98ca.mp4",
                "require_video_sprite": True,
            },
        )
    ).json()
    # make sure we clean up the s3 resources before running the test
    try:
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=thumbnail_key)
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=large_image_key)
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=sprite_key)
    except Exception as e:
        pass

    model.process_uploaded_video.remote(queue_item)

    # check for thumbnail and large image in the suno-data-uploads bucket
    try:
        s3_client.head_object(Bucket=UPLOADS_S3_BUCKET, Key=thumbnail_key)
        s3_client.head_object(Bucket=UPLOADS_S3_BUCKET, Key=large_image_key)
        # Check whether video sprite exists
        s3_client.head_object(Bucket=UPLOADS_S3_BUCKET, Key=sprite_key)
    except Exception as e:
        print(f"Headobject failed with error {e}")
        assert False

    print("Found snapshot and thumbnail on s3. Cleaning them up now.")

    # clean up resources before next test run
    try:
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=thumbnail_key)
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=large_image_key)
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=sprite_key)
    except Exception as e:
        print(
            f"Delete object failed with error {e} - make sure you clean up s3 resources or this test won't work :)"
        )

    return


def _test_process_uploaded_clip_video_cover(model: VideoUploadStub):
    """
    Make sure you've got AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY set or these checks will fail
    """
    queue_item = (
        QueueItem(
            id="testprocessuploadedclipvideocover",
            metadata={
                "upload_key": "test/cc86aba8-c118-4f30-875b-6d94d8ae15ef.mp4",
                "is_video_cover": True,
            },
        )
    ).json()

    model.process_uploaded_video.remote(queue_item)

    thumbnail_key = (
        "studio/uploads/image_video_upload_testprocessuploadedclipvideocover_snapshot_0s.jpeg"
    )
    large_image_key = (
        "studio/uploads/image_large_video_upload_testprocessuploadedclipvideocover_snapshot_0s.jpeg"
    )
    gif_key = "studio/uploads/video_upload_testprocessuploadedclipvideocover.gif"
    mp4_key = "studio/uploads/video_upload_testprocessuploadedclipvideocover_preview.mp4"

    # check for thumbnail and large image in the suno-data-uploads bucket
    try:
        s3_client.head_object(Bucket=UPLOADS_S3_BUCKET, Key=thumbnail_key)
        s3_client.head_object(Bucket=UPLOADS_S3_BUCKET, Key=large_image_key)
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=gif_key)
        s3_client.head_object(Bucket=UPLOADS_S3_BUCKET, Key=mp4_key)
    except Exception as e:
        print(f"Headobject failed with error {e}")
        assert False

    print("Found snapshot and thumbnail on s3. Cleaning them up now.")

    # clean up resources before next test run
    try:
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=thumbnail_key)
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=gif_key)
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=large_image_key)

    except Exception as e:
        print(
            f"Delete object failed with error {e} - make sure you clean up s3 resources or this test won't work :)"
        )

    return


def _test_process_uploaded_clip_video_cover_gif_preview(model: VideoUploadStub):
    """
    Make sure you've got AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY set or these checks will fail
    """
    queue_item = (
        QueueItem(
            id="testprocessuploadedclipvideocover",
            metadata={
                "upload_key": "test/cc86aba8-c118-4f30-875b-6d94d8ae15ef.mp4",
                "is_video_cover": True,
                "preview_format": "gif",
            },
        )
    ).json()

    model.process_uploaded_video.remote(queue_item)

    thumbnail_key = (
        "studio/uploads/image_video_upload_testprocessuploadedclipvideocover_snapshot_0s.jpeg"
    )
    large_image_key = (
        "studio/uploads/image_large_video_upload_testprocessuploadedclipvideocover_snapshot_0s.jpeg"
    )
    gif_key = "studio/uploads/video_upload_testprocessuploadedclipvideocover_preview.gif"

    # check for thumbnail and large image in the suno-data-uploads bucket
    try:
        s3_client.head_object(Bucket=UPLOADS_S3_BUCKET, Key=thumbnail_key)
        s3_client.head_object(Bucket=UPLOADS_S3_BUCKET, Key=large_image_key)
        s3_client.head_object(Bucket=UPLOADS_S3_BUCKET, Key=gif_key)
    except Exception as e:
        print(f"Headobject failed with error {e}")
        assert False

    print("Found snapshot and thumbnail on s3. Cleaning them up now.")

    # clean up resources before next test run
    try:
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=thumbnail_key)
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=large_image_key)
        s3_client.delete_object(Bucket=UPLOADS_S3_BUCKET, Key=gif_key)
    except Exception as e:
        print(
            f"Delete object failed with error {e} - make sure you clean up s3 resources or this test won't work :)"
        )

    return


@app.local_entrypoint()
def main():
    model = VideoUploadStub()
    _test_process_uploaded_video(model)
    _test_process_uploaded_clip_video_cover(model)
    _test_process_uploaded_clip_video_cover_gif_preview(model)
