//! Image processing service for session media
//!
//! Handles image validation, HEIC conversion, and thumbnail generation.

use bytes::Bytes;
use image::codecs::jpeg::JpegEncoder;
use image::imageops::FilterType;
use image::{DynamicImage, GenericImageView, ImageReader};
use std::io::Cursor;
use std::process::Command;
use tempfile::NamedTempFile;
use thiserror::Error;

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

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

    #[error("Failed to decode image: {0}")]
    DecodeFailed(String),

    #[error("Failed to encode image: {0}")]
    EncodeFailed(String),

    #[error("HEIC conversion failed: {0}")]
    HeicConversionFailed(String),

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

/// Result of processing an image
#[derive(Debug)]
pub struct ProcessedImage {
    /// The processed image data (JPEG for HEIC, original for others)
    pub data: Bytes,
    /// Content type (always image/jpeg for processed images, or original)
    pub content_type: String,
    /// Image width in pixels
    pub width: u32,
    /// Image height in pixels
    pub height: u32,
}

/// Maximum image size (10MB)
pub const MAX_IMAGE_SIZE: usize = 10 * 1024 * 1024;

/// Thumbnail size (max dimension)
pub const THUMBNAIL_SIZE: u32 = 320;

/// JPEG quality for thumbnails and HEIC conversion
pub const JPEG_QUALITY: u8 = 85;

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

    // JPEG: FF D8 FF
    if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
        return Some("image/jpeg");
    }

    // PNG: 89 50 4E 47 0D 0A 1A 0A
    if data.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
        return Some("image/png");
    }

    // GIF: GIF87a or GIF89a
    if data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a") {
        return Some("image/gif");
    }

    // WebP: RIFF....WEBP
    if data.len() >= 12 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" {
        return Some("image/webp");
    }

    // HEIC/HEIF: ftyp followed by heic, heix, hevc, mif1, etc.
    if data.len() >= 12 && &data[4..8] == b"ftyp" {
        let brand = &data[8..12];
        if brand == b"heic"
            || brand == b"heix"
            || brand == b"hevc"
            || brand == b"hevx"
            || brand == b"mif1"
            || brand == b"msf1"
        {
            return Some("image/heic");
        }
    }

    // BMP: BM
    if data.starts_with(b"BM") {
        return Some("image/bmp");
    }

    // TIFF: II or MM
    if data.starts_with(&[0x49, 0x49, 0x2A, 0x00]) || data.starts_with(&[0x4D, 0x4D, 0x00, 0x2A]) {
        return Some("image/tiff");
    }

    None
}

/// Check if format is HEIC/HEIF
fn is_heic(content_type: &str) -> bool {
    matches!(content_type, "image/heic" | "image/heif")
}

/// Convert HEIC to JPEG using heif-convert CLI tool
fn convert_heic_to_jpeg(data: &[u8]) -> Result<Vec<u8>, ImageError> {
    // Write HEIC data to temp file
    let mut input_file = NamedTempFile::with_suffix(".heic")?;
    std::io::Write::write_all(&mut input_file, data)?;

    // Create output temp file
    let output_file = NamedTempFile::with_suffix(".jpg")?;
    let output_path = output_file.path().to_path_buf();

    // Run heif-convert
    let output = Command::new("heif-convert")
        .arg("-q")
        .arg(JPEG_QUALITY.to_string())
        .arg(input_file.path())
        .arg(&output_path)
        .output();

    match output {
        Ok(result) if result.status.success() => {
            // Read the converted JPEG
            std::fs::read(&output_path).map_err(|e| ImageError::HeicConversionFailed(e.to_string()))
        }
        Ok(result) => {
            let stderr = String::from_utf8_lossy(&result.stderr);
            Err(ImageError::HeicConversionFailed(format!(
                "heif-convert failed: {}",
                stderr
            )))
        }
        Err(e) => {
            // heif-convert not installed, try ImageMagick convert
            let output = Command::new("convert")
                .arg(input_file.path())
                .arg("-quality")
                .arg(JPEG_QUALITY.to_string())
                .arg(format!("jpeg:{}", output_path.display()))
                .output();

            match output {
                Ok(result) if result.status.success() => std::fs::read(&output_path)
                    .map_err(|e| ImageError::HeicConversionFailed(e.to_string())),
                Ok(result) => {
                    let stderr = String::from_utf8_lossy(&result.stderr);
                    Err(ImageError::HeicConversionFailed(format!(
                        "ImageMagick convert failed: {}",
                        stderr
                    )))
                }
                Err(_) => Err(ImageError::HeicConversionFailed(format!(
                    "Neither heif-convert nor ImageMagick available: {}",
                    e
                ))),
            }
        }
    }
}

/// Process an uploaded image
///
/// - Validates size and format
/// - Converts HEIC to JPEG
/// - Extracts dimensions
pub fn process_image(data: &[u8], _filename: &str) -> Result<ProcessedImage, ImageError> {
    // Check size
    if data.len() > MAX_IMAGE_SIZE {
        return Err(ImageError::TooLarge {
            size: data.len(),
            max: MAX_IMAGE_SIZE,
        });
    }

    // Detect format
    let content_type = detect_format(data)
        .ok_or_else(|| ImageError::InvalidFormat("Unknown image format".to_string()))?;

    // Handle HEIC conversion
    let (processed_data, final_content_type) = if is_heic(content_type) {
        let jpeg_data = convert_heic_to_jpeg(data)?;
        (jpeg_data, "image/jpeg".to_string())
    } else {
        (data.to_vec(), content_type.to_string())
    };

    // Decode to get dimensions
    let img = ImageReader::new(Cursor::new(&processed_data))
        .with_guessed_format()
        .map_err(|e| ImageError::DecodeFailed(e.to_string()))?
        .decode()
        .map_err(|e| ImageError::DecodeFailed(e.to_string()))?;

    let (width, height) = img.dimensions();

    Ok(ProcessedImage {
        data: Bytes::from(processed_data),
        content_type: final_content_type,
        width,
        height,
    })
}

/// Generate a thumbnail from image data
///
/// Returns JPEG thumbnail data fitting within max_size x max_size
pub fn generate_thumbnail(data: &[u8], max_size: u32) -> Result<Vec<u8>, ImageError> {
    // Decode the image
    let img = ImageReader::new(Cursor::new(data))
        .with_guessed_format()
        .map_err(|e| ImageError::DecodeFailed(e.to_string()))?
        .decode()
        .map_err(|e| ImageError::DecodeFailed(e.to_string()))?;

    // Calculate thumbnail dimensions (preserve aspect ratio)
    let (orig_width, orig_height) = img.dimensions();
    let (thumb_width, thumb_height) = if orig_width > orig_height {
        let ratio = max_size as f64 / orig_width as f64;
        (max_size, (orig_height as f64 * ratio) as u32)
    } else {
        let ratio = max_size as f64 / orig_height as f64;
        ((orig_width as f64 * ratio) as u32, max_size)
    };

    // Resize
    let thumbnail = img.resize(thumb_width, thumb_height, FilterType::Lanczos3);

    // Encode as JPEG
    encode_jpeg(&thumbnail)
}

/// Encode an image as JPEG
fn encode_jpeg(img: &DynamicImage) -> Result<Vec<u8>, ImageError> {
    let mut output = Vec::new();
    let mut encoder = JpegEncoder::new_with_quality(&mut output, JPEG_QUALITY);
    encoder
        .encode_image(img)
        .map_err(|e| ImageError::EncodeFailed(e.to_string()))?;
    Ok(output)
}

/// Get image dimensions without full decode (faster for validation)
pub fn get_dimensions(data: &[u8]) -> Result<(u32, u32), ImageError> {
    let reader = ImageReader::new(Cursor::new(data))
        .with_guessed_format()
        .map_err(|e| ImageError::DecodeFailed(e.to_string()))?;

    let dimensions = reader
        .into_dimensions()
        .map_err(|e| ImageError::DecodeFailed(e.to_string()))?;

    Ok(dimensions)
}

/// Get the file extension for a content type
pub fn extension_for_content_type(content_type: &str) -> &'static str {
    match content_type {
        "image/jpeg" => "jpg",
        "image/png" => "png",
        "image/gif" => "gif",
        "image/webp" => "webp",
        "image/heic" | "image/heif" => "heic",
        "image/bmp" => "bmp",
        "image/tiff" => "tiff",
        _ => "bin",
    }
}

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

    #[test]
    fn test_detect_jpeg() {
        let data = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01];
        assert_eq!(detect_format(&data), Some("image/jpeg"));
    }

    #[test]
    fn test_detect_png() {
        let data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D];
        assert_eq!(detect_format(&data), Some("image/png"));
    }

    #[test]
    fn test_detect_heic() {
        let data = [0x00, 0x00, 0x00, 0x18, b'f', b't', b'y', b'p', b'h', b'e', b'i', b'c'];
        assert_eq!(detect_format(&data), Some("image/heic"));
    }
}
