//! Content parts for Open Responses items.
//!
//! Content parts represent the atomic units of content within items,
//! such as text, images, or files. They are streamable and follow
//! the delta event pattern for incremental updates.

use serde::{Deserialize, Serialize};

/// Unique identifier for a content part within an output item.
pub type ContentPartId = String;

/// Content part types supported by Open Responses.
///
/// Content parts are the atomic units of content within message items.
/// Each content part has its own lifecycle and can be streamed independently.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
    /// Text output from the model.
    OutputText(OutputTextContent),

    /// Text input provided to the model.
    InputText(InputTextContent),

    /// Image input provided to the model.
    InputImage(InputImageContent),

    /// File input provided to the model.
    InputFile(InputFileContent),

    /// URL citation for web resources used in generation.
    UrlCitation(UrlCitationContent),

    /// Refusal content when the model declines to respond.
    Refusal(RefusalContent),
}

impl ContentPart {
    /// Create a new output text content part.
    pub fn output_text(text: impl Into<String>) -> Self {
        Self::OutputText(OutputTextContent { text: text.into() })
    }

    /// Create a new input text content part.
    pub fn input_text(text: impl Into<String>) -> Self {
        Self::InputText(InputTextContent { text: text.into() })
    }

    /// Create a new refusal content part.
    pub fn refusal(refusal: impl Into<String>) -> Self {
        Self::Refusal(RefusalContent {
            refusal: refusal.into(),
        })
    }

    /// Returns the text content if this is a text-based content part.
    pub fn as_text(&self) -> Option<&str> {
        match self {
            Self::OutputText(c) => Some(&c.text),
            Self::InputText(c) => Some(&c.text),
            Self::Refusal(c) => Some(&c.refusal),
            _ => None,
        }
    }
}

/// Text output from the model.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutputTextContent {
    /// The text content generated by the model.
    pub text: String,
}

/// Text input provided to the model.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InputTextContent {
    /// The text content provided as input.
    pub text: String,
}

/// Image input provided to the model.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InputImageContent {
    /// URL of the image (data URI or HTTP URL).
    pub image_url: String,

    /// Level of detail for image analysis.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub detail: Option<ImageDetail>,
}

/// Level of detail for image analysis.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ImageDetail {
    /// Low detail mode.
    Low,
    /// High detail mode.
    High,
    /// Let the model decide.
    #[default]
    Auto,
    /// Preserve the original image detail.
    Original,
}

/// File input provided to the model.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InputFileContent {
    /// Filename of the input file.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub filename: Option<String>,

    /// File ID returned by the Files API.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub file_id: Option<String>,

    /// Base64-encoded file data.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub file_data: Option<String>,

    /// URL of the file.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub file_url: Option<String>,
}

/// URL citation for web resources.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UrlCitationContent {
    /// The URL of the web resource.
    pub url: String,

    /// Title of the web resource.
    pub title: String,

    /// Start index in the message text.
    pub start_index: usize,

    /// End index in the message text.
    pub end_index: usize,
}

/// Refusal content when the model declines to respond.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RefusalContent {
    /// The refusal message.
    pub refusal: String,
}

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

    #[test]
    fn test_content_part_output_text() {
        let part = ContentPart::output_text("Hello, world!");
        assert_eq!(part.as_text(), Some("Hello, world!"));
    }

    #[test]
    fn test_content_part_serialization() {
        let part = ContentPart::output_text("Test");
        let json = serde_json::to_string(&part).unwrap();
        assert!(json.contains("\"type\":\"output_text\""));
        assert!(json.contains("\"text\":\"Test\""));
    }
}
