//! Video processing service for session media
//!
//! Handles video validation, metadata extraction, and thumbnail generation
//! using ffmpeg/ffprobe.

use std::path::Path;
use std::process::Command;
use thiserror::Error;

/// Video processing errors
#[derive(Error, Debug)]
pub enum VideoError {
    #[error("Invalid video format: {0}")]
    InvalidFormat(String),

    #[error("Video too large: {size} bytes (max {max} bytes)")]
    TooLarge { size: usize, max: usize },

    #[error("Failed to extract metadata: {0}")]
    MetadataFailed(String),

    #[error("Failed to generate thumbnail: {0}")]
    ThumbnailFailed(String),

    #[error("ffprobe not available: {0}")]
    FfprobeNotAvailable(String),

    #[error("ffmpeg not available: {0}")]
    FfmpegNotAvailable(String),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

/// Video metadata
#[derive(Debug, Clone)]
pub struct VideoMetadata {
    /// Video width in pixels
    pub width: u32,
    /// Video height in pixels
    pub height: u32,
    /// Duration in seconds
    pub duration_seconds: i32,
    /// Content type (MIME)
    pub content_type: String,
}

/// Maximum video size (250MB)
pub const MAX_VIDEO_SIZE: usize = 250 * 1024 * 1024;

/// Supported video MIME types
const SUPPORTED_VIDEO_TYPES: &[&str] = &[
    "video/mp4",
    "video/quicktime", // .mov
    "video/webm",
    "video/x-msvideo", // .avi
    "video/x-matroska", // .mkv
    "video/3gpp",
    "video/3gpp2",
];

/// Detect video format from magic bytes
fn detect_format(data: &[u8]) -> Option<&'static str> {
    if data.len() < 12 {
        return None;
    }

    // MP4/MOV: ftyp box
    if data.len() >= 12 && &data[4..8] == b"ftyp" {
        let brand = &data[8..12];
        // Common MP4 brands
        if brand == b"isom"
            || brand == b"iso2"
            || brand == b"mp41"
            || brand == b"mp42"
            || brand == b"avc1"
            || brand == b"M4V "
            || brand == b"M4A "
        {
            return Some("video/mp4");
        }
        // QuickTime brands
        if brand == b"qt  " {
            return Some("video/quicktime");
        }
        // 3GPP
        if brand == b"3gp4" || brand == b"3gp5" || brand == b"3gp6" {
            return Some("video/3gpp");
        }
    }

    // WebM: EBML header
    if data.starts_with(&[0x1A, 0x45, 0xDF, 0xA3]) {
        return Some("video/webm");
    }

    // AVI: RIFF....AVI
    if data.len() >= 12 && &data[0..4] == b"RIFF" && &data[8..12] == b"AVI " {
        return Some("video/x-msvideo");
    }

    // MKV: EBML header (same as WebM, distinguished by doctype)
    // For simplicity, we detect WebM/MKV together as webm

    None
}

/// Verify video data and return content type
pub fn verify_video(data: &[u8], filename: &str) -> Result<String, VideoError> {
    // Check size
    if data.len() > MAX_VIDEO_SIZE {
        return Err(VideoError::TooLarge {
            size: data.len(),
            max: MAX_VIDEO_SIZE,
        });
    }

    // Try to detect from magic bytes
    if let Some(content_type) = detect_format(data) {
        return Ok(content_type.to_string());
    }

    // Fall back to extension
    let extension = Path::new(filename)
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("")
        .to_lowercase();

    let content_type = match extension.as_str() {
        "mp4" | "m4v" => "video/mp4",
        "mov" => "video/quicktime",
        "webm" => "video/webm",
        "avi" => "video/x-msvideo",
        "mkv" => "video/x-matroska",
        "3gp" => "video/3gpp",
        "3g2" => "video/3gpp2",
        _ => {
            return Err(VideoError::InvalidFormat(format!(
                "Unknown video format: {}",
                extension
            )))
        }
    };

    if !SUPPORTED_VIDEO_TYPES.contains(&content_type) {
        return Err(VideoError::InvalidFormat(format!(
            "Unsupported video type: {}",
            content_type
        )));
    }

    Ok(content_type.to_string())
}

/// Extract video metadata using ffprobe
pub fn extract_metadata(video_path: &Path) -> Result<VideoMetadata, VideoError> {
    // Run ffprobe to get video info as JSON
    let output = Command::new("ffprobe")
        .args([
            "-v",
            "quiet",
            "-print_format",
            "json",
            "-show_format",
            "-show_streams",
            "-select_streams",
            "v:0", // First video stream only
        ])
        .arg(video_path)
        .output()
        .map_err(|e| VideoError::FfprobeNotAvailable(e.to_string()))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(VideoError::MetadataFailed(format!(
            "ffprobe failed: {}",
            stderr
        )));
    }

    let json_str = String::from_utf8_lossy(&output.stdout);

    // Parse the JSON
    let json: serde_json::Value = serde_json::from_str(&json_str)
        .map_err(|e| VideoError::MetadataFailed(format!("Failed to parse ffprobe output: {}", e)))?;

    // Extract dimensions from first video stream
    let streams = json["streams"]
        .as_array()
        .ok_or_else(|| VideoError::MetadataFailed("No streams found".to_string()))?;

    let video_stream = streams
        .first()
        .ok_or_else(|| VideoError::MetadataFailed("No video stream found".to_string()))?;

    let width = video_stream["width"]
        .as_u64()
        .ok_or_else(|| VideoError::MetadataFailed("Could not get width".to_string()))?
        as u32;

    let height = video_stream["height"]
        .as_u64()
        .ok_or_else(|| VideoError::MetadataFailed("Could not get height".to_string()))?
        as u32;

    // Get duration from format or stream
    let duration_seconds = json["format"]["duration"]
        .as_str()
        .and_then(|s| s.parse::<f64>().ok())
        .or_else(|| {
            video_stream["duration"]
                .as_str()
                .and_then(|s| s.parse::<f64>().ok())
        })
        .unwrap_or(0.0) as i32;

    // Determine content type from format
    let format_name = json["format"]["format_name"]
        .as_str()
        .unwrap_or("mp4");

    let content_type = match format_name {
        name if name.contains("mp4") || name.contains("m4v") => "video/mp4",
        name if name.contains("mov") || name.contains("quicktime") => "video/quicktime",
        name if name.contains("webm") => "video/webm",
        name if name.contains("avi") => "video/x-msvideo",
        name if name.contains("matroska") || name.contains("mkv") => "video/x-matroska",
        name if name.contains("3gp") => "video/3gpp",
        _ => "video/mp4", // Default
    };

    Ok(VideoMetadata {
        width,
        height,
        duration_seconds,
        content_type: content_type.to_string(),
    })
}

/// Generate a thumbnail from a video at a specific timestamp
pub fn generate_thumbnail(
    video_path: &Path,
    output_path: &Path,
    timestamp_seconds: f64,
) -> Result<(), VideoError> {
    // Run ffmpeg to extract a frame
    let output = Command::new("ffmpeg")
        .args([
            "-y",                    // Overwrite output
            "-ss",                   // Seek to timestamp
            &format!("{:.2}", timestamp_seconds),
            "-i",                    // Input file
        ])
        .arg(video_path)
        .args([
            "-vframes",
            "1",                     // Extract 1 frame
            "-vf",
            "scale=320:-1",         // Scale to 320px width, maintain aspect ratio
            "-f",
            "mjpeg",                 // Output as JPEG
            "-q:v",
            "3",                     // Quality (2-31, lower is better)
        ])
        .arg(output_path)
        .output()
        .map_err(|e| VideoError::FfmpegNotAvailable(e.to_string()))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(VideoError::ThumbnailFailed(format!(
            "ffmpeg failed: {}",
            stderr
        )));
    }

    // Verify output file exists and has content
    let metadata = std::fs::metadata(output_path)
        .map_err(|e| VideoError::ThumbnailFailed(format!("Output file not created: {}", e)))?;

    if metadata.len() == 0 {
        return Err(VideoError::ThumbnailFailed(
            "Thumbnail file is empty".to_string(),
        ));
    }

    Ok(())
}

/// Get the file extension for a video content type
pub fn extension_for_content_type(content_type: &str) -> &'static str {
    match content_type {
        "video/mp4" => "mp4",
        "video/quicktime" => "mov",
        "video/webm" => "webm",
        "video/x-msvideo" => "avi",
        "video/x-matroska" => "mkv",
        "video/3gpp" => "3gp",
        "video/3gpp2" => "3g2",
        _ => "mp4",
    }
}

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

    #[test]
    fn test_detect_mp4() {
        // Minimal MP4 ftyp box
        let data = [
            0x00, 0x00, 0x00, 0x18,
            b'f', b't', b'y', b'p',
            b'i', b's', b'o', b'm',
        ];
        assert_eq!(detect_format(&data), Some("video/mp4"));
    }

    #[test]
    fn test_detect_webm() {
        let data = [0x1A, 0x45, 0xDF, 0xA3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
        assert_eq!(detect_format(&data), Some("video/webm"));
    }

    #[test]
    fn test_detect_avi() {
        let data = [
            b'R', b'I', b'F', b'F',
            0x00, 0x00, 0x00, 0x00,
            b'A', b'V', b'I', b' ',
        ];
        assert_eq!(detect_format(&data), Some("video/x-msvideo"));
    }
}
