//! Media utilities shared across SDK crates.
//!
//! Provides:
//! - [`MediaUri`] — typed representation of `media://{direction}/{uuid}` URIs
//! - [`detect_mime_type`] — magic-byte MIME detection
//! - [`inline_data_uri`] — construct `data:{mime};base64,{data}` URIs

use std::fmt;

/// Direction of media flow through the media service.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaDirection {
    /// User → agent (uploaded by channel, consumed by agent).
    Inbound,
    /// Agent → user (uploaded by agent/tool, consumed by channel).
    Outbound,
}

impl MediaDirection {
    /// String representation used in the URI scheme.
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Inbound => "inbound",
            Self::Outbound => "outbound",
        }
    }
}

impl fmt::Display for MediaDirection {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Parsed `media://{direction}/{uuid}` URI.
///
/// The media service uses a claim-check pattern: channels upload bytes and receive
/// a `media://inbound/<uuid>` URI. This type provides structured access to the
/// URI components without repeated string parsing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MediaUri {
    /// Direction of media flow.
    pub direction: MediaDirection,
    /// Unique identifier for the stored media.
    pub id: String,
}

impl MediaUri {
    /// Construct a new media URI.
    pub fn new(direction: MediaDirection, id: impl Into<String>) -> Self {
        Self {
            direction,
            id: id.into(),
        }
    }

    /// Parse a `media://{direction}/{uuid}` string into a structured URI.
    ///
    /// Returns `None` if the string doesn't match the expected format.
    pub fn parse(uri: &str) -> Option<Self> {
        let rest = uri.strip_prefix("media://")?;
        let (direction_str, id) = rest.split_once('/')?;

        if id.is_empty() {
            return None;
        }

        let direction = match direction_str {
            "inbound" => MediaDirection::Inbound,
            "outbound" => MediaDirection::Outbound,
            _ => return None,
        };

        Some(Self {
            direction,
            id: id.to_string(),
        })
    }

    /// Check if a string is a media:// URI (without full parsing).
    pub fn is_media_uri(uri: &str) -> bool {
        uri.starts_with("media://")
    }

    /// Build the HTTP URL to fetch this media from the media service.
    ///
    /// Given endpoint `http://host:port/media`, returns `http://host:port/media/{uuid}`.
    pub fn fetch_url(&self, endpoint: &str) -> String {
        format!("{}/{}", endpoint.trim_end_matches('/'), self.id)
    }
}

impl fmt::Display for MediaUri {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "media://{}/{}", self.direction, self.id)
    }
}

/// Detect MIME type from the first bytes of file data using magic byte signatures.
///
/// Supports: PNG, JPEG, GIF, WebP, PDF, ZIP-based (xlsx, docx, etc.).
/// Returns `"application/octet-stream"` for unrecognized formats.
pub fn detect_mime_type(bytes: &[u8]) -> &'static str {
    if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
        "image/png"
    } else if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
        "image/jpeg"
    } else if bytes.starts_with(b"GIF8") {
        "image/gif"
    } else if bytes.starts_with(b"RIFF") && bytes.len() >= 12 && &bytes[8..12] == b"WEBP" {
        "image/webp"
    } else if bytes.starts_with(b"%PDF") {
        "application/pdf"
    } else if bytes.starts_with(&[0x50, 0x4B, 0x03, 0x04]) {
        // ZIP-based formats (xlsx, docx, pptx, jar, etc.)
        "application/zip"
    } else {
        "application/octet-stream"
    }
}

/// Construct a `data:{mime};base64,{encoded}` URI from raw bytes.
///
/// Used to inline file content into ResourceLink URIs when the agent cannot
/// access the media service directly.
pub fn inline_data_uri(mime: &str, bytes: &[u8]) -> String {
    use base64::Engine;
    let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
    format!("data:{mime};base64,{b64}")
}

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

    #[rstest]
    #[case::inbound("media://inbound/abc-123", Some(MediaDirection::Inbound), "abc-123")]
    #[case::outbound("media://outbound/def-456", Some(MediaDirection::Outbound), "def-456")]
    #[case::uuid_with_hyphens(
        "media://inbound/4febf95f-fd1d-4343-b674-2abba850ee23",
        Some(MediaDirection::Inbound),
        "4febf95f-fd1d-4343-b674-2abba850ee23"
    )]
    fn when_valid_media_uri_then_parsed(
        #[case] input: &str,
        #[case] expected_dir: Option<MediaDirection>,
        #[case] expected_id: &str,
    ) {
        let parsed = MediaUri::parse(input);
        assert!(parsed.is_some(), "failed to parse: {input}");
        let uri = parsed.unwrap();
        assert_eq!(uri.direction, expected_dir.unwrap());
        assert_eq!(uri.id, expected_id);
    }

    #[rstest]
    #[case::not_media("https://example.com/file.pdf")]
    #[case::no_id("media://inbound/")]
    #[case::no_direction("media:///abc-123")]
    #[case::unknown_direction("media://unknown/abc-123")]
    #[case::empty("")]
    fn when_invalid_media_uri_then_none(#[case] input: &str) {
        assert!(
            MediaUri::parse(input).is_none(),
            "should not parse: {input}"
        );
    }

    #[rstest]
    fn when_display_then_roundtrips() {
        let uri = MediaUri::new(MediaDirection::Inbound, "test-uuid");
        assert_eq!(uri.to_string(), "media://inbound/test-uuid");
        assert_eq!(MediaUri::parse(&uri.to_string()), Some(uri));
    }

    #[rstest]
    fn when_fetch_url_then_correct() {
        let uri = MediaUri::new(MediaDirection::Inbound, "abc-123");
        assert_eq!(
            uri.fetch_url("http://localhost:9090/media"),
            "http://localhost:9090/media/abc-123"
        );
    }

    #[rstest]
    fn when_fetch_url_with_trailing_slash_then_no_double_slash() {
        let uri = MediaUri::new(MediaDirection::Outbound, "xyz");
        assert_eq!(
            uri.fetch_url("http://localhost:9090/media/"),
            "http://localhost:9090/media/xyz"
        );
    }

    #[rstest]
    #[case::png(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A], "image/png")]
    #[case::jpeg(&[0xFF, 0xD8, 0xFF, 0xE0], "image/jpeg")]
    #[case::gif(&[0x47, 0x49, 0x46, 0x38, 0x39, 0x61], "image/gif")]
    #[case::pdf(b"%PDF-1.4", "application/pdf")]
    #[case::zip(&[0x50, 0x4B, 0x03, 0x04, 0x14, 0x00], "application/zip")]
    #[case::unknown(&[0x00, 0x01, 0x02], "application/octet-stream")]
    fn when_magic_bytes_then_correct_mime(#[case] bytes: &[u8], #[case] expected: &str) {
        assert_eq!(detect_mime_type(bytes), expected);
    }

    #[rstest]
    fn when_inline_data_uri_then_correct_format() {
        let bytes = b"hello world";
        let uri = inline_data_uri("text/plain", bytes);
        assert!(uri.starts_with("data:text/plain;base64,"));
        // Verify the base64 decodes back
        let b64_part = uri.strip_prefix("data:text/plain;base64,").unwrap();
        use base64::Engine;
        let decoded = base64::engine::general_purpose::STANDARD
            .decode(b64_part)
            .unwrap();
        assert_eq!(decoded, b"hello world");
    }

    #[rstest]
    fn when_is_media_uri_then_correct() {
        assert!(MediaUri::is_media_uri("media://inbound/abc"));
        assert!(MediaUri::is_media_uri("media://outbound/xyz"));
        assert!(!MediaUri::is_media_uri("https://example.com"));
        assert!(!MediaUri::is_media_uri(""));
    }
}
