//! analyze_image: load a local image, validate it, and package it as a
//! `data:` URL for one multimodal request to the `visual` role model.
//! All validation is local — the network call lives in the agent dispatch.

use base64::Engine;
use std::path::Path;

/// Maximum accepted image size (~8 MB) — keeps a single request well under
/// provider payload limits once base64 inflation (~4/3) is applied.
pub const MAX_IMAGE_BYTES: u64 = 8 * 1024 * 1024;

/// Question used when the model doesn't supply one.
pub const DEFAULT_QUESTION: &str =
    "Describe this image in detail: layout, any visible text, UI elements, diagrams, charts or error messages.";

/// Read and validate an image file, returning it as a `data:<mime>;base64,…`
/// URL suitable for [`ChatMessage::user_with_images`](crate::llm::ChatMessage).
/// Errors (missing file, unsupported type, too big) come back as
/// model-readable `Error: …` strings.
pub fn image_data_url(path: &Path) -> Result<String, String> {
    let ext = path
        .extension()
        .and_then(|e| e.to_str())
        .map(|e| e.to_ascii_lowercase())
        .unwrap_or_default();
    let mime = match ext.as_str() {
        "png" => "image/png",
        "jpg" | "jpeg" => "image/jpeg",
        "gif" => "image/gif",
        "webp" => "image/webp",
        _ => {
            return Err(format!(
                "Error: unsupported image type '.{ext}' for {} — supported: png, jpg, jpeg, gif, webp.",
                path.display()
            ))
        }
    };
    let meta = std::fs::metadata(path)
        .map_err(|e| format!("Error: cannot read image {}: {e}", path.display()))?;
    if !meta.is_file() {
        return Err(format!("Error: {} is not a regular file.", path.display()));
    }
    if meta.len() > MAX_IMAGE_BYTES {
        return Err(format!(
            "Error: image {} is {:.1} MB — exceeds the {} MB limit for analyze_image.",
            path.display(),
            meta.len() as f64 / (1024.0 * 1024.0),
            MAX_IMAGE_BYTES / (1024 * 1024)
        ));
    }
    let bytes = std::fs::read(path)
        .map_err(|e| format!("Error: cannot read image {}: {e}", path.display()))?;
    let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
    Ok(format!("data:{mime};base64,{b64}"))
}

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

    #[test]
    fn rejects_missing_file() {
        let err = image_data_url(Path::new("/nonexistent/definitely-missing.png")).unwrap_err();
        assert!(err.starts_with("Error:"), "got: {err}");
        assert!(err.contains("cannot read image"), "got: {err}");
    }

    #[test]
    fn rejects_unsupported_extension() {
        let dir = tempfile::tempdir().unwrap();
        let p = dir.path().join("notes.txt");
        std::fs::write(&p, "hello").unwrap();
        let err = image_data_url(&p).unwrap_err();
        assert!(err.contains("unsupported image type"), "got: {err}");
    }

    #[test]
    fn rejects_oversize_file() {
        let dir = tempfile::tempdir().unwrap();
        let p = dir.path().join("huge.png");
        // One byte over the cap.
        let f = std::fs::File::create(&p).unwrap();
        f.set_len(MAX_IMAGE_BYTES + 1).unwrap();
        drop(f);
        let err = image_data_url(&p).unwrap_err();
        assert!(err.contains("exceeds"), "got: {err}");
    }

    #[test]
    fn encodes_small_png_as_data_url() {
        let dir = tempfile::tempdir().unwrap();
        let p = dir.path().join("tiny.png");
        std::fs::write(&p, [0x89, b'P', b'N', b'G']).unwrap();
        let url = image_data_url(&p).unwrap();
        assert!(url.starts_with("data:image/png;base64,"), "got: {url}");
    }
}
