use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;

use super::executor::PythonExecutor;

const YOLO_TIMEOUT: Duration = Duration::from_secs(7200);

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct YoloResult {
    pub frame_count: u64,
    pub fps: f64,
    pub frames: Vec<YoloFrame>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct YoloFrame {
    pub frame: u64,
    pub timestamp: f64,
    pub objects: Vec<YoloObject>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct YoloObject {
    pub class_name: String,
    pub class_id: u32,
    pub x: i32,
    pub y: i32,
    pub width: i32,
    pub height: i32,
    pub confidence: f32,
}

// New structs for parsing Python YOLO output
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct YoloPythonMetadata {
    video_path: String,
    fps: f64,
    width: i32,
    height: i32,
    total_frames: i64,
    total_duration: f64,
    processed_at: String,
    auto_save_interval: i32,
    auto_save_frames: i32,
    status: String,
    last_saved_at: String,
    last_saved_frame: i64,
    completed_at: Option<String>,
    processing_time: Option<f64>,
    total_detections: Option<i64>,
    auto_save_count: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct YoloPythonDetection {
    class_name: String,
    confidence: f32,
    x1: f32,
    y1: f32,
    x2: f32,
    y2: f32,
    width: i32,
    height: i32,
    class_id: Option<u32>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct YoloPythonFrame {
    frame_number: u64,
    time_seconds: f64,
    time_formatted: String,
    detections: Vec<YoloPythonDetection>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct YoloPythonResult {
    metadata: YoloPythonMetadata,
    frames: HashMap<String, YoloPythonFrame>,
}

impl YoloPythonResult {
    pub fn to_yolo_result(&self) -> YoloResult {
        let mut frames = Vec::new();

        // Sort frames by frame number (key is string, but we parse as u64)
        let mut frame_entries: Vec<_> = self.frames.iter().collect();
        frame_entries.sort_by_key(|(key, _)| key.parse::<u64>().unwrap_or(0));

        for (_, frame) in frame_entries {
            let mut objects = Vec::new();
            for detection in &frame.detections {
                objects.push(YoloObject {
                    class_name: detection.class_name.clone(),
                    class_id: detection.class_id.unwrap_or(0),
                    x: detection.x1 as i32,
                    y: detection.y1 as i32,
                    width: detection.width,
                    height: detection.height,
                    confidence: detection.confidence,
                });
            }
            frames.push(YoloFrame {
                frame: frame.frame_number,
                timestamp: frame.time_seconds,
                objects,
            });
        }

        YoloResult {
            frame_count: frames.len() as u64,
            fps: self.metadata.fps,
            frames,
        }
    }
}

pub async fn process_yolo(
    video_path: &str,
    output_path: &str,
    uuid: Option<&str>,
) -> Result<YoloResult> {
    let executor = PythonExecutor::new()?;
    let script_path = executor.script_path("yolo_processor.py");

    tracing::info!("[YOLO] Starting object detection: {}", video_path);

    if !script_path.exists() {
        tracing::warn!("[YOLO] Script not found, returning empty result");
        return Ok(YoloResult {
            frame_count: 0,
            fps: 0.0,
            frames: vec![],
        });
    }

    executor
        .run(
            "yolo_processor.py",
            &[video_path, output_path],
            uuid,
            "YOLO",
            Some(YOLO_TIMEOUT),
        )
        .await
        .with_context(|| format!("Failed to run {:?}", script_path))?;

    let json_str = std::fs::read_to_string(output_path).context("Failed to read YOLO output")?;

    let python_result: YoloPythonResult =
        serde_json::from_str(&json_str).context("Failed to parse YOLO output")?;

    let result = python_result.to_yolo_result();

    tracing::info!(
        "[YOLO] Result: {} frames, {:.2} fps",
        result.frame_count,
        result.fps
    );

    Ok(result)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_yolo_result_serialization() {
        let result = YoloResult {
            frame_count: 100,
            fps: 30.0,
            frames: vec![YoloFrame {
                frame: 0,
                timestamp: 0.0,
                objects: vec![YoloObject {
                    class_name: "person".to_string(),
                    class_id: 0,
                    x: 100,
                    y: 200,
                    width: 50,
                    height: 100,
                    confidence: 0.95,
                }],
            }],
        };

        let json = serde_json::to_string(&result).unwrap();
        assert!(json.contains("person"));
        assert!(json.contains("100"));
    }

    #[test]
    fn test_yolo_result_deserialization() {
        let json = r#"{
            "frame_count": 50,
            "fps": 25.0,
            "frames": [
                {
                    "frame": 10,
                    "timestamp": 0.4,
                    "objects": [
                        {"class_name": "car", "class_id": 2, "x": 0, "y": 0, "width": 100, "height": 80, "confidence": 0.87}
                    ]
                }
            ]
        }"#;

        let result: YoloResult = serde_json::from_str(json).unwrap();
        assert_eq!(result.frame_count, 50);
        assert_eq!(result.fps, 25.0);
        assert_eq!(result.frames.len(), 1);
        assert_eq!(result.frames[0].objects[0].class_name, "car");
    }

    #[test]
    fn test_yolo_object_confidence_range() {
        let obj = YoloObject {
            class_name: "test".to_string(),
            class_id: 0,
            x: 0,
            y: 0,
            width: 10,
            height: 10,
            confidence: 0.5,
        };
        assert!(obj.confidence >= 0.0 && obj.confidence <= 1.0);
    }

    #[test]
    fn test_yolo_result_empty_frames() {
        let result = YoloResult {
            frame_count: 0,
            fps: 0.0,
            frames: vec![],
        };
        assert!(result.frames.is_empty());
    }

    #[test]
    fn test_yolo_python_result_parsing() {
        // Sample JSON matching Python script output
        let json = r#"{
            "metadata": {
                "video_path": "/test/video.mp4",
                "fps": 22.0,
                "width": 640,
                "height": 360,
                "total_frames": 3512,
                "total_duration": 159.63636363636363,
                "processed_at": "2026-03-26T05:20:48.230143",
                "auto_save_interval": 30,
                "auto_save_frames": 300,
                "status": "completed",
                "last_saved_at": "2026-03-26T05:23:22.791673",
                "last_saved_frame": 0,
                "completed_at": "2026-03-26T05:23:22.791666",
                "processing_time": 154.5577518939972,
                "total_detections": 12786,
                "auto_save_count": 11
            },
            "frames": {
                "13": {
                    "frame_number": 13,
                    "time_seconds": 0.545,
                    "time_formatted": "00:00:00",
                    "detections": [
                        {
                            "class_id": 0,
                            "class_name": "person",
                            "confidence": 0.8424218893051147,
                            "x1": 473.4156494140625,
                            "y1": 79.5609359741211,
                            "x2": 639.77783203125,
                            "y2": 303.8714294433594,
                            "width": 166,
                            "height": 224
                        }
                    ]
                }
            }
        }"#;

        let python_result: YoloPythonResult = serde_json::from_str(json).unwrap();
        assert_eq!(python_result.metadata.fps, 22.0);
        assert_eq!(python_result.frames.len(), 1);
        let frame = python_result.frames.get("13").unwrap();
        assert_eq!(frame.frame_number, 13);
        assert_eq!(frame.detections.len(), 1);
        let detection = &frame.detections[0];
        assert_eq!(detection.class_id, Some(0));
        assert_eq!(detection.class_name, "person");
        assert!((detection.confidence - 0.8424218893051147).abs() < 0.0001);
        assert!((detection.x1 - 473.4156494140625).abs() < 0.0001);

        // Convert to YoloResult
        let yolo_result = python_result.to_yolo_result();
        assert_eq!(yolo_result.frames.len(), 1);
        assert_eq!(yolo_result.frames[0].frame, 13);
        assert_eq!(yolo_result.frames[0].objects.len(), 1);
        let obj = &yolo_result.frames[0].objects[0];
        assert_eq!(obj.class_name, "person");
        assert_eq!(obj.class_id, 0);
        assert_eq!(obj.x, 473);
        assert_eq!(obj.y, 79);
        assert_eq!(obj.width, 166);
        assert_eq!(obj.height, 224);
        assert!((obj.confidence - 0.842421889).abs() < 0.0001);
    }
}
