//! Camera capture — cross-platform frame grabber.
//!
//! macOS: uses `screencapture` or `ffmpeg -f avfoundation` for camera frames
//! Linux: uses `v4l2-ctl` or `ffmpeg -f v4l2` for camera frames
//! Both: capture to JPEG, decode to RGB, return raw pixel data

use anyhow::{bail, Result};
use std::path::PathBuf;
use std::process::Command;

/// Captured frame with raw RGB data.
pub struct Frame {
    pub width: u32,
    pub height: u32,
    pub rgb: Vec<u8>, // row-major [height * width * 3]
}

/// Camera source configuration.
pub struct CameraConfig {
    pub device_index: u32,
    pub width: u32,
    pub height: u32,
    pub fps: u32,
}

impl Default for CameraConfig {
    fn default() -> Self {
        Self {
            device_index: 0,
            width: 640,
            height: 480,
            fps: 15,
        }
    }
}

/// Capture a single frame from the camera.
///
/// Tries multiple backends in order: ffmpeg, v4l2, imagesnap (macOS).
pub fn capture_frame(config: &CameraConfig) -> Result<Frame> {
    let tmp = tmp_path();

    // Try ffmpeg first (cross-platform)
    if let Ok(frame) = capture_ffmpeg(config, &tmp) {
        return Ok(frame);
    }

    // Linux: try v4l2
    #[cfg(target_os = "linux")]
    if let Ok(frame) = capture_v4l2(config, &tmp) {
        return Ok(frame);
    }

    // macOS: try screencapture (camera mode)
    #[cfg(target_os = "macos")]
    if let Ok(frame) = capture_macos(config, &tmp) {
        return Ok(frame);
    }

    bail!("No camera backend available. Install ffmpeg or run on a machine with a camera.")
}

/// Capture via ffmpeg (works on Linux + macOS).
fn capture_ffmpeg(config: &CameraConfig, tmp: &PathBuf) -> Result<Frame> {
    let input = if cfg!(target_os = "macos") {
        format!("{}:none", config.device_index) // avfoundation: video:audio
    } else {
        format!("/dev/video{}", config.device_index) // v4l2
    };

    let format = if cfg!(target_os = "macos") {
        "avfoundation"
    } else {
        "v4l2"
    };

    let status = Command::new("ffmpeg")
        .args([
            "-y",
            "-f",
            format,
            "-video_size",
            &format!("{}x{}", config.width, config.height),
            "-framerate",
            &config.fps.to_string(),
            "-i",
            &input,
            "-frames:v",
            "1",
            "-f",
            "rawvideo",
            "-pix_fmt",
            "rgb24",
            tmp.to_str().unwrap_or("/tmp/ruview-frame.raw"),
        ])
        .output()?;

    if !status.status.success() {
        bail!(
            "ffmpeg capture failed: {}",
            String::from_utf8_lossy(&status.stderr)
        );
    }

    let rgb = std::fs::read(tmp)?;
    let expected = (config.width * config.height * 3) as usize;
    if rgb.len() < expected {
        bail!(
            "frame too small: {} bytes, expected {}",
            rgb.len(),
            expected
        );
    }

    let _ = std::fs::remove_file(tmp);

    Ok(Frame {
        width: config.width,
        height: config.height,
        rgb: rgb[..expected].to_vec(),
    })
}

/// Linux: capture via v4l2-ctl.
#[cfg(target_os = "linux")]
fn capture_v4l2(config: &CameraConfig, tmp: &PathBuf) -> Result<Frame> {
    let device = format!("/dev/video{}", config.device_index);
    if !std::path::Path::new(&device).exists() {
        bail!("no camera at {device}");
    }

    // Use v4l2-ctl to grab a frame
    let status = Command::new("v4l2-ctl")
        .args([
            "--device",
            &device,
            "--set-fmt-video",
            &format!(
                "width={},height={},pixelformat=MJPG",
                config.width, config.height
            ),
            "--stream-mmap",
            "--stream-count=1",
            "--stream-to",
            tmp.to_str().unwrap_or("/tmp/frame.mjpg"),
        ])
        .output()?;

    if !status.status.success() {
        bail!("v4l2-ctl failed");
    }

    // Decode MJPEG to RGB
    decode_jpeg_to_rgb(tmp, config.width, config.height)
}

/// macOS: capture via screencapture or swift.
#[cfg(target_os = "macos")]
fn capture_macos(config: &CameraConfig, tmp: &PathBuf) -> Result<Frame> {
    let jpg_path = tmp.with_extension("jpg");

    // Try swift-based capture (requires camera permission)
    let swift = format!(
        r#"import AVFoundation; import AppKit
let sem = DispatchSemaphore(value: 0)
let s = AVCaptureSession(); s.sessionPreset = .medium
guard let d = AVCaptureDevice.default(for: .video) else {{ exit(1) }}
let i = try! AVCaptureDeviceInput(device: d); s.addInput(i)
let o = AVCapturePhotoOutput(); s.addOutput(o)
class D: NSObject, AVCapturePhotoCaptureDelegate {{
    func photoOutput(_ o: AVCapturePhotoOutput, didFinishProcessingPhoto p: AVCapturePhoto, error: Error?) {{
        if let d = p.fileDataRepresentation() {{ try! d.write(to: URL(fileURLWithPath: "{path}")) }}
        exit(0)
    }}
}}
let dl = D(); s.startRunning(); Thread.sleep(forTimeInterval: 1)
o.capturePhoto(with: AVCapturePhotoSettings(), delegate: dl)
Thread.sleep(forTimeInterval: 3)"#,
        path = jpg_path.display()
    );

    let _ = Command::new("swift").args(["-e", &swift]).output();

    if jpg_path.exists() {
        return decode_jpeg_to_rgb(&jpg_path, config.width, config.height);
    }

    bail!("macOS camera capture requires GUI session with camera permission")
}

fn decode_jpeg_to_rgb(path: &PathBuf, _width: u32, _height: u32) -> Result<Frame> {
    let data = std::fs::read(path)?;
    let _ = std::fs::remove_file(path);

    // Simple JPEG decode — use the image crate if available, otherwise raw
    // For now, return the raw data and let the caller handle format
    Ok(Frame {
        width: _width,
        height: _height,
        rgb: data,
    })
}

fn tmp_path() -> PathBuf {
    std::env::temp_dir().join(format!("ruview-frame-{}.raw", std::process::id()))
}

/// Check if a camera is available on this system.
pub fn camera_available() -> bool {
    if cfg!(target_os = "macos") {
        Command::new("system_profiler")
            .args(["SPCameraDataType"])
            .output()
            .map(|o| String::from_utf8_lossy(&o.stdout).contains("Camera"))
            .unwrap_or(false)
    } else {
        std::path::Path::new("/dev/video0").exists()
    }
}

/// List available cameras.
pub fn list_cameras() -> Vec<String> {
    let mut cameras = Vec::new();

    if cfg!(target_os = "macos") {
        if let Ok(output) = Command::new("system_profiler")
            .args(["SPCameraDataType"])
            .output()
        {
            let text = String::from_utf8_lossy(&output.stdout);
            for line in text.lines() {
                let trimmed = line.trim();
                if trimmed.ends_with(':') && !trimmed.starts_with("Camera") && trimmed.len() > 2 {
                    cameras.push(trimmed.trim_end_matches(':').to_string());
                }
            }
        }
    } else {
        for i in 0..10 {
            if std::path::Path::new(&format!("/dev/video{i}")).exists() {
                cameras.push(format!("/dev/video{i}"));
            }
        }
    }
    cameras
}
