use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tracing::{error, info};

use crate::core::config::{OUTPUT_DIR, PYTHON_PATH, SCRIPTS_DIR};
use crate::core::db::{MonitorJob, PostgresDb, ProcessorJobStatus, ProcessorType, RedisClient};
use crate::core::processor;
use crate::core::processor::asr::AsrResult;
use crate::core::processor::asrx::AsrxResult;
use crate::core::processor::cut::CutResult;
use crate::core::processor::face::FaceResult;
use crate::core::processor::ocr::OcrResult;
use crate::core::processor::pose::PoseResult;
use crate::core::processor::scene_classification::SceneClassificationResult;
use crate::core::processor::visual_chunk::VisualChunkResult;
use crate::core::processor::yolo::YoloResult;

#[derive(Debug)]
struct ProcessorOutput {
    data: serde_json::Value,
    chunks_produced: i32,
    frames_processed: i32,
}

#[derive(Debug, Clone)]
pub struct ProcessorTask {
    pub job: MonitorJob,
    pub processor_type: ProcessorType,
    pub processor_result_id: i32,
}

pub struct ProcessorPool {
    db: Arc<PostgresDb>,
    redis: Arc<RedisClient>,
    max_concurrent: usize,
    running: Arc<RwLock<HashMap<i32, ProcessorHandle>>>,
    running_count: Arc<RwLock<usize>>,
}

struct ProcessorHandle {
    #[allow(dead_code)]
    processor_type: ProcessorType,
    cancel_tx: mpsc::Sender<()>,
}

impl ProcessorPool {
    pub fn new(db: Arc<PostgresDb>, redis: Arc<RedisClient>, max_concurrent: usize) -> Self {
        Self {
            db,
            redis,
            max_concurrent,
            running: Arc::new(RwLock::new(HashMap::new())),
            running_count: Arc::new(RwLock::new(0)),
        }
    }

    pub async fn can_start(&self) -> bool {
        let count = *self.running_count.read().await;
        count < self.max_concurrent
    }

    pub async fn start_processor(&self, task: ProcessorTask) -> Result<()> {
        let (cancel_tx, cancel_rx) = mpsc::channel(1);
        let job_id = task.job.id;
        let processor_type = task.processor_type;

        {
            let mut count = self.running_count.write().await;
            if *count >= self.max_concurrent {
                anyhow::bail!("Max concurrent processors reached");
            }
            *count += 1;
        }

        let running = self.running.clone();
        let running_count = self.running_count.clone();
        running.write().await.insert(
            job_id,
            ProcessorHandle {
                processor_type,
                cancel_tx,
            },
        );

        let db = self.db.clone();
        let redis = self.redis.clone();
        let job = task.job.clone();
        let processor_result_id = task.processor_result_id;
        let processor_name = processor_type.as_str().to_string();

        tokio::spawn(async move {
            info!("Starting processor {} for job {}", processor_name, job.uuid);

            let _ = db
                .update_processor_result(
                    processor_result_id,
                    ProcessorJobStatus::Running,
                    None,
                    None,
                )
                .await;

            let _ = redis
                .update_worker_processor_status(&job.uuid, &processor_name, "running", None)
                .await;

            let result = Self::run_processor(&db, &redis, &job, processor_type, cancel_rx).await;

            {
                let mut running_guard = running.write().await;
                running_guard.remove(&job_id);
                let mut count_guard = running_count.write().await;
                *count_guard -= 1;
            }

            match result {
                Ok(output) => {
                    info!(
                        "Processor {} completed for job {} ({} chunks, {} frames)",
                        processor_name, job.uuid, output.chunks_produced, output.frames_processed
                    );
                    if let Err(e) = db
                        .update_processor_result_with_stats(
                            processor_result_id,
                            ProcessorJobStatus::Completed,
                            None,
                            Some(&output.data),
                            output.chunks_produced,
                            output.frames_processed,
                        )
                        .await
                    {
                        error!("Failed to update processor result to completed: {}", e);
                    }

                    if let Err(e) = redis
                        .update_worker_processor_status(
                            &job.uuid,
                            &processor_name,
                            "completed",
                            None,
                        )
                        .await
                    {
                        error!("Failed to update Redis processor status: {}", e);
                    }
                }
                Err(e) => {
                    error!(
                        "Processor {} failed for job {}: {}",
                        processor_name, job.uuid, e
                    );
                    if let Err(db_err) = db
                        .update_processor_result_with_stats(
                            processor_result_id,
                            ProcessorJobStatus::Failed,
                            Some(&e.to_string()),
                            None,
                            0,
                            0,
                        )
                        .await
                    {
                        error!("Failed to update processor result to failed: {}", db_err);
                    }

                    if let Err(redis_err) = redis
                        .update_worker_processor_status(
                            &job.uuid,
                            &processor_name,
                            "failed",
                            Some(&e.to_string()),
                        )
                        .await
                    {
                        error!("Failed to update Redis processor status: {}", redis_err);
                    }
                }
            }
        });

        Ok(())
    }

    async fn run_processor(
        db: &PostgresDb,
        _redis: &RedisClient,
        job: &MonitorJob,
        processor_type: ProcessorType,
        _cancel_rx: mpsc::Receiver<()>,
    ) -> Result<ProcessorOutput> {
        let video_path = job.video_path.as_ref().context("No video path in job")?;

        // Generate output path
        let output_dir = PathBuf::from(OUTPUT_DIR.as_str());
        let output_path = output_dir.join(format!(
            "job_{}_{}_{}.json",
            job.id,
            processor_type.as_str(),
            chrono::Utc::now().timestamp_millis()
        ));

        // Ensure output directory exists
        if let Some(parent) = output_path.parent() {
            tokio::fs::create_dir_all(parent).await?;
        }

        let uuid = Some(job.uuid.as_str());
        let video = db.get_video_by_uuid(&job.uuid).await?;
        let total_frames = video.as_ref().map(|v| v.total_frames as i32).unwrap_or(0);

        match processor_type {
            ProcessorType::Asr => {
                let result =
                    processor::process_asr(video_path, output_path.to_str().unwrap(), uuid).await?;
                let chunks_produced = result.segments.len() as i32;
                tracing::info!(
                    "ASR completed, storing {} segments for {}",
                    chunks_produced,
                    job.uuid
                );
                if let Err(e) = Self::store_asr_chunks(db, &job.uuid, &result).await {
                    tracing::error!("Failed to store ASR chunks for {}: {}", job.uuid, e);
                }
                Ok(ProcessorOutput {
                    data: serde_json::to_value(result)?,
                    chunks_produced,
                    frames_processed: total_frames,
                })
            }
            ProcessorType::Cut => {
                let result =
                    processor::process_cut(video_path, output_path.to_str().unwrap(), uuid).await?;
                let chunks_produced = result.scenes.len() as i32;
                tracing::info!(
                    "CUT completed, storing {} scenes for {}",
                    chunks_produced,
                    job.uuid
                );
                if let Err(e) = Self::store_cut_chunks(db, &job.uuid, &result).await {
                    tracing::error!("Failed to store CUT chunks for {}: {}", job.uuid, e);
                }
                Ok(ProcessorOutput {
                    data: serde_json::to_value(result)?,
                    chunks_produced,
                    frames_processed: total_frames,
                })
            }
            ProcessorType::Yolo => {
                let result =
                    processor::process_yolo(video_path, output_path.to_str().unwrap(), uuid)
                        .await?;
                let chunks_produced = result.frames.len() as i32;
                tracing::info!(
                    "YOLO completed, storing {} frames for {}",
                    chunks_produced,
                    job.uuid
                );
                if let Err(e) = Self::store_yolo_chunks(db, &job.uuid, &result).await {
                    tracing::error!("Failed to store YOLO chunks for {}: {}", job.uuid, e);
                }
                Ok(ProcessorOutput {
                    data: serde_json::to_value(result)?,
                    chunks_produced,
                    frames_processed: total_frames,
                })
            }
            ProcessorType::Ocr => {
                let result =
                    processor::process_ocr(video_path, output_path.to_str().unwrap(), uuid).await?;
                let chunks_produced = result.frames.len() as i32;
                tracing::info!(
                    "OCR completed, storing {} frames for {}",
                    chunks_produced,
                    job.uuid
                );
                if let Err(e) = Self::store_ocr_chunks(db, &job.uuid, &result).await {
                    tracing::error!("Failed to store OCR chunks for {}: {}", job.uuid, e);
                }
                Ok(ProcessorOutput {
                    data: serde_json::to_value(result)?,
                    chunks_produced,
                    frames_processed: total_frames,
                })
            }
            ProcessorType::Face => {
                let result =
                    processor::process_face(video_path, output_path.to_str().unwrap(), uuid)
                        .await?;
                let chunks_produced = result.frames.len() as i32;
                tracing::info!(
                    "FACE completed, storing {} frames for {}",
                    chunks_produced,
                    job.uuid
                );
                if let Err(e) = Self::store_face_chunks(db, &job.uuid, &result).await {
                    tracing::error!("Failed to store FACE chunks for {}: {}", job.uuid, e);
                }
                Ok(ProcessorOutput {
                    data: serde_json::to_value(result)?,
                    chunks_produced,
                    frames_processed: total_frames,
                })
            }
            ProcessorType::Pose => {
                let result =
                    processor::process_pose(video_path, output_path.to_str().unwrap(), uuid)
                        .await?;
                let chunks_produced = result.frames.len() as i32;
                tracing::info!(
                    "POSE completed, storing {} frames for {}",
                    chunks_produced,
                    job.uuid
                );
                if let Err(e) = Self::store_pose_chunks(db, &job.uuid, &result).await {
                    tracing::error!("Failed to store POSE chunks for {}: {}", job.uuid, e);
                }
                Ok(ProcessorOutput {
                    data: serde_json::to_value(result)?,
                    chunks_produced,
                    frames_processed: total_frames,
                })
            }
            ProcessorType::Asrx => {
                let result =
                    processor::process_asrx(video_path, output_path.to_str().unwrap(), uuid)
                        .await?;
                let chunks_produced = result.segments.len() as i32;
                tracing::info!(
                    "ASRX completed, storing {} segments for {}",
                    chunks_produced,
                    job.uuid
                );
                if let Err(e) = Self::store_asrx_chunks(db, &job.uuid, &result).await {
                    tracing::error!("Failed to store ASRX chunks for {}: {}", job.uuid, e);
                }
                Ok(ProcessorOutput {
                    data: serde_json::to_value(result)?,
                    chunks_produced,
                    frames_processed: total_frames,
                })
            }
            ProcessorType::VisualChunk => {
                let result = processor::process_visual_chunk_advanced(
                    video_path,
                    output_path.to_str().unwrap(),
                    uuid,
                )
                .await?;
                let chunks_produced = result.chunk_count as i32;
                tracing::info!(
                    "VisualChunk completed, storing {} chunks for {}",
                    chunks_produced,
                    job.uuid
                );
                if let Err(e) = Self::store_visual_chunk_chunks(db, &job.uuid, &result).await {
                    tracing::error!("Failed to store VisualChunk chunks for {}: {}", job.uuid, e);
                }
                Ok(ProcessorOutput {
                    data: serde_json::to_value(result)?,
                    chunks_produced,
                    frames_processed: total_frames,
                })
            }
            ProcessorType::Scene => {
                let result = processor::process_scene_classification(
                    video_path,
                    output_path.to_str().unwrap(),
                    uuid,
                )
                .await?;
                let chunks_produced = result.scenes.len() as i32;
                tracing::info!(
                    "Scene classification completed, storing {} scenes for {}",
                    chunks_produced,
                    job.uuid
                );
                if let Err(e) = Self::store_scene_chunks(db, &job.uuid, &result).await {
                    tracing::error!("Failed to store Scene chunks for {}: {}", job.uuid, e);
                }
                Ok(ProcessorOutput {
                    data: serde_json::to_value(result)?,
                    chunks_produced,
                    frames_processed: total_frames,
                })
            }
        }
    }

    #[allow(dead_code)]
    async fn run_asr(
        _db: &PostgresDb,
        _redis: &RedisClient,
        video_path: &str,
        _cancel_rx: &mut mpsc::Receiver<()>,
    ) -> Result<serde_json::Value> {
        let script_path = std::env::var("MOMENTRY_ASR_SCRIPT")
            .unwrap_or_else(|_| format!("{}/asr_processor.py", SCRIPTS_DIR.as_str()));

        let output = tokio::process::Command::new(PYTHON_PATH.as_str())
            .arg(&script_path)
            .arg(video_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("ASR script failed: {}", stderr);
        }

        let result: serde_json::Value = serde_json::from_slice(&output.stdout)?;
        Ok(result)
    }

    #[allow(dead_code)]
    async fn run_cut(
        _db: &PostgresDb,
        _redis: &RedisClient,
        video_path: &str,
        _cancel_rx: &mut mpsc::Receiver<()>,
    ) -> Result<serde_json::Value> {
        let script_path = std::env::var("MOMENTRY_CUT_SCRIPT")
            .unwrap_or_else(|_| format!("{}/cut_processor.py", SCRIPTS_DIR.as_str()));

        let output = tokio::process::Command::new(PYTHON_PATH.as_str())
            .arg(&script_path)
            .arg(video_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("CUT script failed: {}", stderr);
        }

        let result: serde_json::Value = serde_json::from_slice(&output.stdout)?;
        Ok(result)
    }

    #[allow(dead_code)]
    async fn run_yolo(
        _db: &PostgresDb,
        _redis: &RedisClient,
        video_path: &str,
        _cancel_rx: &mut mpsc::Receiver<()>,
    ) -> Result<serde_json::Value> {
        let script_path = std::env::var("MOMENTRY_YOLO_SCRIPT")
            .unwrap_or_else(|_| format!("{}/yolo_processor.py", SCRIPTS_DIR.as_str()));

        let output = tokio::process::Command::new(PYTHON_PATH.as_str())
            .arg(&script_path)
            .arg(video_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("YOLO script failed: {}", stderr);
        }

        let result: serde_json::Value = serde_json::from_slice(&output.stdout)?;
        Ok(result)
    }

    #[allow(dead_code)]
    async fn run_ocr(
        _db: &PostgresDb,
        _redis: &RedisClient,
        video_path: &str,
        _cancel_rx: &mut mpsc::Receiver<()>,
    ) -> Result<serde_json::Value> {
        let script_path = std::env::var("MOMENTRY_OCR_SCRIPT")
            .unwrap_or_else(|_| format!("{}/ocr_processor.py", SCRIPTS_DIR.as_str()));

        let output = tokio::process::Command::new(PYTHON_PATH.as_str())
            .arg(&script_path)
            .arg(video_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("OCR script failed: {}", stderr);
        }

        let result: serde_json::Value = serde_json::from_slice(&output.stdout)?;
        Ok(result)
    }

    #[allow(dead_code)]
    async fn run_face(
        _db: &PostgresDb,
        _redis: &RedisClient,
        video_path: &str,
        _cancel_rx: &mut mpsc::Receiver<()>,
    ) -> Result<serde_json::Value> {
        let script_path = std::env::var("MOMENTRY_FACE_SCRIPT")
            .unwrap_or_else(|_| format!("{}/face_processor.py", SCRIPTS_DIR.as_str()));

        let output = tokio::process::Command::new(PYTHON_PATH.as_str())
            .arg(&script_path)
            .arg(video_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("Face script failed: {}", stderr);
        }

        let result: serde_json::Value = serde_json::from_slice(&output.stdout)?;
        Ok(result)
    }

    #[allow(dead_code)]
    async fn run_pose(
        _db: &PostgresDb,
        _redis: &RedisClient,
        video_path: &str,
        _cancel_rx: &mut mpsc::Receiver<()>,
    ) -> Result<serde_json::Value> {
        let script_path = std::env::var("MOMENTRY_POSE_SCRIPT")
            .unwrap_or_else(|_| format!("{}/pose_processor.py", SCRIPTS_DIR.as_str()));

        let output = tokio::process::Command::new(PYTHON_PATH.as_str())
            .arg(&script_path)
            .arg(video_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("Pose script failed: {}", stderr);
        }

        let result: serde_json::Value = serde_json::from_slice(&output.stdout)?;
        Ok(result)
    }

    #[allow(dead_code)]
    async fn run_asrx(
        _db: &PostgresDb,
        _redis: &RedisClient,
        video_path: &str,
        _cancel_rx: &mut mpsc::Receiver<()>,
    ) -> Result<serde_json::Value> {
        let script_path = std::env::var("MOMENTRY_ASRX_SCRIPT")
            .unwrap_or_else(|_| format!("{}/asrx_processor_custom.py", SCRIPTS_DIR.as_str()));

        let output = tokio::process::Command::new(PYTHON_PATH.as_str())
            .arg(&script_path)
            .arg(video_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("ASRX script failed: {}", stderr);
        }

        let result: serde_json::Value = serde_json::from_slice(&output.stdout)?;
        Ok(result)
    }

    pub async fn store_asr_chunks(
        db: &PostgresDb,
        uuid: &str,
        asr_result: &AsrResult,
    ) -> Result<()> {
        let video = db
            .get_video_by_uuid(uuid)
            .await?
            .ok_or_else(|| anyhow::anyhow!("Video not found for uuid: {}", uuid))?;
        let fps = if video.fps > 0.0 { video.fps } else { 30.0 };

        let segments: Vec<(i64, i64, i64, f64, f64, serde_json::Value)> = asr_result
            .segments
            .iter()
            .enumerate()
            .map(|(i, segment)| {
                let start_frame = (segment.start * fps).round() as i64;
                let end_frame = (segment.end * fps).round() as i64;
                let data = serde_json::json!({
                    "text": segment.text,
                    "text_normalized": segment.text.to_lowercase(),
                    "language": asr_result.language,
                    "language_probability": asr_result.language_probability,
                });
                (
                    i as i64,
                    start_frame,
                    end_frame,
                    segment.start,
                    segment.end,
                    data,
                )
            })
            .collect();

        db.store_asr_pre_chunks_batch(uuid, &segments).await?;

        tracing::info!(
            "Stored {} ASR pre-chunks for video {}",
            segments.len(),
            uuid
        );

        Ok(())
    }

    pub async fn store_cut_chunks(
        db: &PostgresDb,
        uuid: &str,
        cut_result: &CutResult,
    ) -> Result<()> {
        let video = db
            .get_video_by_uuid(uuid)
            .await?
            .ok_or_else(|| anyhow::anyhow!("Video not found for uuid: {}", uuid))?;
        let fps = if video.fps > 0.0 { video.fps } else { 30.0 };

        let scenes: Vec<(i64, i64, i64, f64, f64, serde_json::Value)> = cut_result
            .scenes
            .iter()
            .enumerate()
            .map(|(i, scene)| {
                let data = serde_json::json!({
                    "scene_number": scene.scene_number,
                });
                (
                    i as i64,
                    scene.start_frame as i64,
                    scene.end_frame as i64,
                    scene.start_time,
                    scene.end_time,
                    data,
                )
            })
            .collect();

        db.store_cut_pre_chunks_batch(uuid, &scenes).await?;

        tracing::info!("Stored {} CUT pre-chunks for video {}", scenes.len(), uuid);

        Ok(())
    }

    pub async fn store_yolo_chunks(
        db: &PostgresDb,
        uuid: &str,
        yolo_result: &YoloResult,
    ) -> Result<()> {
        let frames_count = yolo_result.frames.len();
        tracing::info!(
            "Storing {} YOLO pre-chunks for video {}",
            frames_count,
            uuid
        );

        let mut pre_chunks_to_store = Vec::new();

        for frame in yolo_result.frames.iter() {
            let data = serde_json::json!({
                "objects": frame.objects,
                "timestamp": frame.timestamp,
            });

            pre_chunks_to_store.push((
                frame.frame as i64,    // coordinate_index
                Some(frame.timestamp), // timestamp
                data,
                None, // identity_id
                None, // confidence
            ));
        }

        db.store_raw_pre_chunks_batch(uuid, "yolo", &pre_chunks_to_store)
            .await?;
        Ok(())
    }

    pub async fn store_ocr_chunks(
        db: &PostgresDb,
        uuid: &str,
        ocr_result: &OcrResult,
    ) -> Result<()> {
        let frames_count = ocr_result.frames.len();
        tracing::info!("Storing {} OCR pre-chunks for video {}", frames_count, uuid);

        let mut pre_chunks_to_store = Vec::new();

        for frame in ocr_result.frames.iter() {
            let data = serde_json::json!({
                "texts": frame.texts,
                "timestamp": frame.timestamp,
            });

            pre_chunks_to_store.push((frame.frame as i64, Some(frame.timestamp), data, None, None));
        }

        db.store_raw_pre_chunks_batch(uuid, "ocr", &pre_chunks_to_store)
            .await?;
        Ok(())
    }

    pub async fn store_face_chunks(
        db: &PostgresDb,
        uuid: &str,
        face_result: &FaceResult,
    ) -> Result<()> {
        let frames_count = face_result.frames.len();
        tracing::info!(
            "Storing {} Face pre-chunks for video {}",
            frames_count,
            uuid
        );

        let mut pre_chunks_to_store = Vec::new();

        for frame in face_result.frames.iter() {
            let data = serde_json::json!({
                "faces": frame.faces,
                "timestamp": frame.timestamp,
            });

            // We could potentially parse identity_id if it's already matched, but for raw ingestion it's None.
            pre_chunks_to_store.push((
                frame.frame as i64,
                Some(frame.timestamp),
                data,
                None, // identity_id
                None, // confidence
            ));
        }

        db.store_raw_pre_chunks_batch(uuid, "face", &pre_chunks_to_store)
            .await?;
        Ok(())
    }

    pub async fn store_pose_chunks(
        db: &PostgresDb,
        uuid: &str,
        pose_result: &PoseResult,
    ) -> Result<()> {
        let frames_count = pose_result.frames.len();
        tracing::info!(
            "Storing {} Pose pre-chunks for video {}",
            frames_count,
            uuid
        );

        let mut pre_chunks_to_store = Vec::new();

        for frame in pose_result.frames.iter() {
            let data = serde_json::json!({
                "persons": frame.persons,
                "timestamp": frame.timestamp,
            });

            pre_chunks_to_store.push((frame.frame as i64, Some(frame.timestamp), data, None, None));
        }

        db.store_raw_pre_chunks_batch(uuid, "pose", &pre_chunks_to_store)
            .await?;
        Ok(())
    }

    pub async fn store_asrx_chunks(
        db: &PostgresDb,
        uuid: &str,
        asrx_result: &AsrxResult,
    ) -> Result<()> {
        let segments_count = asrx_result.segments.len();
        tracing::info!(
            "Storing {} ASRX pre-chunks for video {}",
            segments_count,
            uuid
        );

        let mut pre_chunks_to_store = Vec::new();

        for (i, segment) in asrx_result.segments.iter().enumerate() {
            let data = serde_json::json!({
                "text": segment.text,
                "speaker_id": segment.speaker_id,
                "timestamp": segment.start,
            });

            // ASRX is time-based, so we use segment index or start time as coordinate.
            // Let's use index for simplicity in pre_chunks, or start time.
            pre_chunks_to_store.push((i as i64, Some(segment.start), data, None, None));
        }

        db.store_raw_pre_chunks_batch(uuid, "asrx", &pre_chunks_to_store)
            .await?;
        Ok(())
    }

    pub async fn store_visual_chunk_chunks(
        db: &PostgresDb,
        uuid: &str,
        visual_chunk_result: &VisualChunkResult,
    ) -> Result<()> {
        for (i, chunk) in visual_chunk_result.chunks.iter().enumerate() {
            match db.store_chunk(chunk).await {
                Ok(_) => {
                    tracing::info!("Stored VisualChunk chunk {} for video {}", i, uuid);
                }
                Err(e) => {
                    tracing::error!("Failed to store VisualChunk chunk {}: {}", i, e);
                }
            }
        }
        Ok(())
    }

    pub async fn store_scene_chunks(
        db: &PostgresDb,
        uuid: &str,
        scene_result: &SceneClassificationResult,
    ) -> Result<()> {
        let video = db
            .get_video_by_uuid(uuid)
            .await?
            .ok_or_else(|| anyhow::anyhow!("Video not found for uuid: {}", uuid))?;
        let fps = if video.fps > 0.0 { video.fps } else { 30.0 };

        let scenes: Vec<(i64, i64, i64, f64, f64, serde_json::Value)> = scene_result
            .scenes
            .iter()
            .enumerate()
            .map(|(i, scene)| {
                let start_frame = (scene.start_time * fps).round() as i64;
                let end_frame = (scene.end_time * fps).round() as i64;
                let data = serde_json::json!({
                    "scene_type": scene.scene_type,
                    "scene_type_zh": scene.scene_type_zh,
                    "confidence": scene.confidence,
                    "top_5": scene.top_5,
                });
                (
                    i as i64,
                    start_frame,
                    end_frame,
                    scene.start_time,
                    scene.end_time,
                    data,
                )
            })
            .collect();

        db.store_scene_pre_chunks_batch(uuid, &scenes).await?;

        tracing::info!(
            "Stored {} Scene pre-chunks for video {}",
            scenes.len(),
            uuid
        );

        Ok(())
    }

    pub async fn get_running_count(&self) -> usize {
        *self.running_count.read().await
    }

    pub async fn cancel_all(&self) {
        let mut running = self.running.write().await;
        for (_, handle) in running.drain() {
            let _ = handle.cancel_tx.send(()).await;
        }
        let mut count = self.running_count.write().await;
        *count = 0;
    }
}
