//! Media types (Image, Audio, Pdf, Video)
//!
//! These wrap FFI pointers to media objects managed by the BAML runtime.

use std::ffi::c_void;

use super::{RawObject, RawObjectTrait};
use crate::{baml_unreachable, proto::baml_cffi_v1::BamlObjectType};
use serde::{Deserialize, Serialize};

// =============================================================================
// Media type macro - generates Image, Audio, Pdf, Video
// Uses define_raw_object_wrapper for base struct + from_raw + RawObjectTrait
// =============================================================================

macro_rules! define_media_type {
    (
        $(#[$meta:meta])*
        $name:ident => $object_type:ident
    ) => {
        // Use the base wrapper for struct, from_raw, and RawObjectTrait
        define_raw_object_wrapper! {
            $(#[$meta])*
            $name => $object_type
        }

        // Add media-specific constructors and methods
        impl $name {
            /// Create from a URL
            pub fn from_url(runtime: *const c_void, url: &str, mime_type: Option<&str>) -> Self {
                let raw = RawObject::new(
                    runtime,
                    BamlObjectType::$object_type,
                    (("url", url), ("mime_type", mime_type)),
                )
                .unwrap_or_else(|e| baml_unreachable!("Failed to create {} from URL: {}", stringify!($name), e));
                Self { raw }
            }

            /// Create from base64-encoded data
            pub fn from_base64(runtime: *const c_void, base64: &str, mime_type: Option<&str>) -> Self {
                let raw = RawObject::new(
                    runtime,
                    BamlObjectType::$object_type,
                    (("base64", base64), ("mime_type", mime_type)),
                )
                .unwrap_or_else(|e| baml_unreachable!("Failed to create {} from base64: {}", stringify!($name), e));
                Self { raw }
            }

            /// Get the MIME type (if known)
            pub fn mime_type(&self) -> Option<String> {
                self.raw.call_method("mime_type", ())
            }

            /// Check if this is a URL reference
            pub fn is_url(&self) -> bool {
                self.raw.call_method("is_url", ())
            }

            /// Check if this is base64 encoded
            pub fn is_base64(&self) -> bool {
                self.raw.call_method("is_base64", ())
            }

            /// Get as URL (if applicable)
            pub fn as_url(&self) -> Option<String> {
                self.raw.call_method("as_url", ())
            }

            /// Get as base64 (if applicable)
            pub fn as_base64(&self) -> Option<String> {
                self.raw.call_method("as_base64", ())
            }
        }

        impl std::fmt::Debug for $name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                f.debug_struct(stringify!($name)).finish_non_exhaustive()
            }
        }

        impl Default for $name {
            fn default() -> Self {
                unreachable!("Media types cannot be default-constructed; they require a runtime")
            }
        }

        impl $crate::BamlDecode for $name {
            fn baml_decode(
                _holder: &$crate::__internal::CffiValueHolder
            ) -> ::core::result::Result<Self, $crate::BamlError> {
                // Media types come as ObjectValue which requires runtime context to decode.
                // This path shouldn't be hit in normal usage - media decoding goes through
                // the runtime's object handle mechanism instead.
                Err($crate::BamlError::internal(
                    concat!("Cannot decode ", stringify!($name), " without runtime context")
                ))
            }
        }

        impl serde::Serialize for $name {
            /// See https://github.com/BoundaryML/baml/blob/canary/engine/language_client_python/src/types/media_repr.rs
            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
                let content = if let Some(base64) = self.as_base64() {
                    BamlMediaReprContent::Base64 { base64 }
                } else if let Some(url) = self.as_url() {
                    BamlMediaReprContent::Url { url }
                } else {
                    return Err(serde::ser::Error::custom(
                        "media cannot be serialized: was not URL or base64",
                    ));
                };
                BamlMediaRepr {
                    mime_type: self.mime_type(),
                    content,
                }
                .serialize(serializer)
            }
        }
        impl<'de> serde::Deserialize<'de> for $name {
            fn deserialize<D: serde::Deserializer<'de>>(_deserializer: D) -> Result<Self, D::Error> {
                // we don't have access to the runtime here
                Err(serde::de::Error::custom(format!("Cannot deserialize {} directly", stringify!($name))))
            }
        }
    };
}

// =============================================================================
// Media type definitions
// =============================================================================

define_media_type! {
    /// Image media type
    Image => ObjectMediaImage
}

define_media_type! {
    /// Audio media type
    Audio => ObjectMediaAudio
}

define_media_type! {
    /// PDF media type
    Pdf => ObjectMediaPdf
}

define_media_type! {
    /// Video media type
    Video => ObjectMediaVideo
}

/// Used for serializing BAML media types in serde
///
/// See https://github.com/BoundaryML/baml/blob/canary/engine/language_client_python/src/types/media_repr.rs
#[derive(Debug, Serialize, Deserialize)]
pub struct BamlMediaRepr {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "media_type")]
    pub mime_type: Option<String>,
    #[serde(flatten)]
    pub content: BamlMediaReprContent,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BamlMediaReprContent {
    Url { url: String },
    Base64 { base64: String },
}
