use anyhow::{Context, Result, anyhow};
use eventsource_stream::Eventsource;
use futures::{StreamExt, future::Future};
use reqwest::multipart::{Form, Part};
use reqwest::{Client, RequestBuilder, Url};
use serde::{Deserialize, Serialize};
use serde_json::Value;

const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
const DEFAULT_OPENAI_IMAGE_TIMEOUT_SECS: u64 = 300;
const OPENAI_IMAGE_TIMEOUT_ENV: &str = "OPENAI_IMAGE_TIMEOUT_SECS";

#[derive(Debug, Clone)]
pub struct OpenAiImageClient {
    client: Client,
    api_key: String,
    base_url: Option<String>,
}

impl OpenAiImageClient {
    pub fn new(api_key: impl Into<String>, base_url: Option<String>) -> Result<Self> {
        let timeout_secs = std::env::var(OPENAI_IMAGE_TIMEOUT_ENV)
            .ok()
            .and_then(|value| value.parse::<u64>().ok())
            .filter(|value| *value > 0)
            .unwrap_or(DEFAULT_OPENAI_IMAGE_TIMEOUT_SECS);
        Ok(Self {
            client: Client::builder()
                .timeout(std::time::Duration::from_secs(timeout_secs))
                .build()
                .context("failed to build OpenAI image client")?,
            api_key: api_key.into(),
            base_url,
        })
    }

    pub async fn generate(&self, request: GenerateImageRequest) -> Result<ImageApiResponse> {
        let url = image_endpoint_url(self.base_url.as_deref(), "images/generations")?;
        let response = self
            .apply_auth(self.client.post(url.clone()), url.as_str())
            .json(&request)
            .send()
            .await
            .context("failed to call OpenAI image generation API")?;

        parse_image_response(response).await
    }

    pub async fn generate_with_events<F, Fut>(
        &self,
        request: GenerateImageRequest,
        on_event: F,
    ) -> Result<ImageApiResponse>
    where
        F: FnMut(ImageApiStreamEvent) -> Fut + Send,
        Fut: Future<Output = ()> + Send,
    {
        let url = image_endpoint_url(self.base_url.as_deref(), "images/generations")?;
        let response = self
            .apply_auth(self.client.post(url.clone()), url.as_str())
            .json(&request)
            .send()
            .await
            .context("failed to call OpenAI image generation API")?;

        parse_image_stream_response(response, on_event).await
    }

    pub async fn edit(&self, request: EditImageRequest) -> Result<ImageApiResponse> {
        let url = image_endpoint_url(self.base_url.as_deref(), "images/edits")?;
        let form = build_edit_image_form(request)?;

        let response = self
            .apply_auth(self.client.post(url.clone()), url.as_str())
            .multipart(form)
            .send()
            .await
            .context("failed to call OpenAI image edit API")?;

        parse_image_response(response).await
    }

    pub async fn edit_with_events<F, Fut>(
        &self,
        request: EditImageRequest,
        on_event: F,
    ) -> Result<ImageApiResponse>
    where
        F: FnMut(ImageApiStreamEvent) -> Fut + Send,
        Fut: Future<Output = ()> + Send,
    {
        let url = image_endpoint_url(self.base_url.as_deref(), "images/edits")?;
        let form = build_edit_image_form(request)?;

        let response = self
            .apply_auth(self.client.post(url.clone()), url.as_str())
            .multipart(form)
            .send()
            .await
            .context("failed to call OpenAI image edit API")?;

        parse_image_stream_response(response, on_event).await
    }

    fn apply_auth(&self, request: RequestBuilder, api_url: &str) -> RequestBuilder {
        if everruns_core::openai_protocol::is_azure_openai_api_url(api_url) {
            request.header("api-key", &self.api_key)
        } else {
            request.bearer_auth(&self.api_key)
        }
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct GenerateImageRequest {
    pub model: String,
    pub prompt: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub size: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub quality: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub background: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "output_format")]
    pub output_format: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stream: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub partial_images: Option<u8>,
    #[serde(rename = "n")]
    pub count: usize,
}

#[derive(Debug, Clone)]
pub struct EditImageRequest {
    pub model: String,
    pub prompt: String,
    pub images: Vec<EditImageInput>,
    pub size: Option<String>,
    pub quality: Option<String>,
    pub background: Option<String>,
    pub output_format: Option<String>,
    pub stream: Option<bool>,
    pub partial_images: Option<u8>,
    pub count: usize,
}

#[derive(Debug, Clone)]
pub struct EditImageInput {
    pub filename: String,
    pub content_type: String,
    pub data: Vec<u8>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ImageApiResponse {
    pub data: Vec<ImageApiImage>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ImageApiImage {
    pub b64_json: String,
    #[serde(default)]
    pub revised_prompt: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImageApiStreamEvent {
    PartialImage {
        partial_image_index: usize,
        b64_json: Option<String>,
        revised_prompt: Option<String>,
    },
    Completed {
        completed_image_count: usize,
    },
}

fn build_edit_image_form(request: EditImageRequest) -> Result<Form> {
    let image_field_name = if request.images.len() > 1 {
        "image[]"
    } else {
        "image"
    };

    let mut form = Form::new()
        .text("model", request.model)
        .text("prompt", request.prompt)
        .text("n", request.count.to_string());

    if let Some(size) = request.size {
        form = form.text("size", size);
    }
    if let Some(quality) = request.quality {
        form = form.text("quality", quality);
    }
    if let Some(background) = request.background {
        form = form.text("background", background);
    }
    if let Some(output_format) = request.output_format {
        form = form.text("output_format", output_format);
    }
    if let Some(stream) = request.stream {
        form = form.text("stream", stream.to_string());
    }
    if let Some(partial_images) = request.partial_images {
        form = form.text("partial_images", partial_images.to_string());
    }

    for image in request.images {
        let part = Part::bytes(image.data)
            .file_name(image.filename)
            .mime_str(&image.content_type)
            .context("invalid source image content type")?;
        form = form.part(image_field_name.to_string(), part);
    }

    Ok(form)
}

fn image_endpoint_url(base_url: Option<&str>, endpoint: &str) -> Result<Url> {
    let mut normalized = base_url
        .unwrap_or(DEFAULT_OPENAI_BASE_URL)
        .trim_end_matches('/');

    for suffix in [
        "/responses",
        "/chat/completions",
        "/images/generations",
        "/images/edits",
    ] {
        if let Some(prefix) = normalized.strip_suffix(suffix) {
            normalized = prefix;
            break;
        }
    }

    Url::parse(&format!(
        "{}/{}",
        normalized,
        endpoint.trim_start_matches('/')
    ))
    .map_err(|error| anyhow!("invalid OpenAI image API URL: {error}"))
}

async fn parse_image_response(response: reqwest::Response) -> Result<ImageApiResponse> {
    let status = response.status();
    if !status.is_success() {
        let body = response
            .text()
            .await
            .unwrap_or_else(|_| "<unreadable response body>".to_string());
        return Err(anyhow!("OpenAI image API returned {status}: {body}"));
    }

    response
        .json::<ImageApiResponse>()
        .await
        .context("failed to decode OpenAI image API response")
}

async fn parse_image_stream_response<F, Fut>(
    response: reqwest::Response,
    mut on_event: F,
) -> Result<ImageApiResponse>
where
    F: FnMut(ImageApiStreamEvent) -> Fut + Send,
    Fut: Future<Output = ()> + Send,
{
    let status = response.status();
    if !status.is_success() {
        let body = response
            .text()
            .await
            .unwrap_or_else(|_| "<unreadable response body>".to_string());
        return Err(anyhow!("OpenAI image API returned {status}: {body}"));
    }

    let mut images = Vec::new();
    let mut event_stream = response.bytes_stream().eventsource();

    while let Some(event) = event_stream.next().await {
        let event = event.context("failed to read OpenAI image stream event")?;
        if event.data == "[DONE]" {
            break;
        }

        let payload: Value = serde_json::from_str(&event.data)
            .context("failed to decode OpenAI image stream event")?;
        let Some(event_type) = payload.get("type").and_then(Value::as_str) else {
            tracing::debug!("Ignoring OpenAI image stream payload without type");
            continue;
        };

        match event_type {
            "image_generation.partial_image" => {
                let partial_image_index = payload
                    .get("partial_image_index")
                    .and_then(Value::as_u64)
                    .unwrap_or(0) as usize;
                let b64_json = payload
                    .get("b64_json")
                    .and_then(Value::as_str)
                    .map(ToOwned::to_owned);
                let revised_prompt = payload
                    .get("revised_prompt")
                    .and_then(Value::as_str)
                    .map(ToOwned::to_owned);
                on_event(ImageApiStreamEvent::PartialImage {
                    partial_image_index,
                    b64_json,
                    revised_prompt,
                })
                .await;
            }
            "image_generation.completed" => {
                let image = serde_json::from_value::<ImageApiImage>(payload)
                    .context("failed to decode OpenAI completed image event")?;
                images.push(image);
                on_event(ImageApiStreamEvent::Completed {
                    completed_image_count: images.len(),
                })
                .await;
            }
            other => {
                tracing::debug!(
                    event_type = other,
                    "Ignoring unknown OpenAI image stream event"
                );
            }
        }
    }

    if images.is_empty() {
        return Err(anyhow!(
            "OpenAI image stream ended before any completed image events were received"
        ));
    }

    Ok(ImageApiResponse { data: images })
}

#[cfg(test)]
mod tests {
    use super::*;
    use wiremock::matchers::{body_json, header, method, path};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    #[tokio::test]
    async fn generate_uses_bearer_auth_and_json_payload() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(path("/v1/images/generations"))
            .and(header("authorization", "Bearer sk-test"))
            .and(body_json(serde_json::json!({
                "model": "gpt-image-1",
                "prompt": "otter",
                "size": "1024x1024",
                "quality": "high",
                "background": "transparent",
                "output_format": "png",
                "n": 1
            })))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "data": [{"b64_json": "aGVsbG8=", "revised_prompt": "otter"}]
            })))
            .mount(&server)
            .await;

        let client =
            OpenAiImageClient::new("sk-test", Some(format!("{}/v1", server.uri()))).unwrap();
        let response = client
            .generate(GenerateImageRequest {
                model: "gpt-image-1".to_string(),
                prompt: "otter".to_string(),
                size: Some("1024x1024".to_string()),
                quality: Some("high".to_string()),
                background: Some("transparent".to_string()),
                output_format: Some("png".to_string()),
                stream: None,
                partial_images: None,
                count: 1,
            })
            .await
            .unwrap();

        assert_eq!(response.data.len(), 1);
        assert_eq!(response.data[0].b64_json, "aGVsbG8=");
    }

    #[tokio::test]
    async fn edit_uses_multipart_endpoint_with_custom_base_url() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(path("/openai/v1/images/edits"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "data": [{"b64_json": "aGVsbG8="}]
            })))
            .mount(&server)
            .await;

        let client =
            OpenAiImageClient::new("azure-key", Some(format!("{}/openai/v1", server.uri())))
                .unwrap();

        let response = client
            .edit(EditImageRequest {
                model: "gpt-image-1".to_string(),
                prompt: "edit it".to_string(),
                images: vec![EditImageInput {
                    filename: "source.png".to_string(),
                    content_type: "image/png".to_string(),
                    data: vec![1, 2, 3],
                }],
                size: Some("1024x1024".to_string()),
                quality: Some("medium".to_string()),
                background: Some("opaque".to_string()),
                output_format: Some("png".to_string()),
                stream: None,
                partial_images: None,
                count: 1,
            })
            .await
            .unwrap();

        assert_eq!(response.data.len(), 1);
    }

    #[tokio::test]
    async fn generate_with_events_streams_partial_and_completed_images() {
        let server = MockServer::start().await;
        let stream_body = concat!(
            "data: {\"type\":\"image_generation.partial_image\",\"partial_image_index\":0,\"b64_json\":\"cHJldmlldw==\",\"revised_prompt\":\"otter sketch\"}\n\n",
            "data: {\"type\":\"image_generation.completed\",\"b64_json\":\"aGVsbG8=\",\"revised_prompt\":\"otter\"}\n\n",
            "data: [DONE]\n\n"
        );
        Mock::given(method("POST"))
            .and(path("/v1/images/generations"))
            .respond_with(
                ResponseTemplate::new(200)
                    .insert_header("content-type", "text/event-stream")
                    .set_body_string(stream_body),
            )
            .mount(&server)
            .await;

        let client =
            OpenAiImageClient::new("sk-test", Some(format!("{}/v1", server.uri()))).unwrap();
        let mut events = Vec::new();
        let response = client
            .generate_with_events(
                GenerateImageRequest {
                    model: "gpt-image-2".to_string(),
                    prompt: "otter".to_string(),
                    size: Some("1024x1024".to_string()),
                    quality: Some("medium".to_string()),
                    background: Some("opaque".to_string()),
                    output_format: Some("png".to_string()),
                    stream: Some(true),
                    partial_images: Some(1),
                    count: 1,
                },
                |event| {
                    events.push(event);
                    async {}
                },
            )
            .await
            .unwrap();

        assert_eq!(
            events,
            vec![
                ImageApiStreamEvent::PartialImage {
                    partial_image_index: 0,
                    b64_json: Some("cHJldmlldw==".to_string()),
                    revised_prompt: Some("otter sketch".to_string()),
                },
                ImageApiStreamEvent::Completed {
                    completed_image_count: 1
                },
            ]
        );
        assert_eq!(response.data.len(), 1);
        assert_eq!(response.data[0].revised_prompt.as_deref(), Some("otter"));
    }

    #[tokio::test]
    async fn edit_with_events_streams_partial_and_completed_images() {
        let server = MockServer::start().await;
        let stream_body = concat!(
            "data: {\"type\":\"image_generation.partial_image\",\"partial_image_index\":0,\"b64_json\":\"cHJldmlldw==\"}\n\n",
            "data: {\"type\":\"image_generation.completed\",\"b64_json\":\"ZmluYWw=\",\"revised_prompt\":\"edited otter\"}\n\n",
            "data: [DONE]\n\n"
        );
        Mock::given(method("POST"))
            .and(path("/v1/images/edits"))
            .respond_with(
                ResponseTemplate::new(200)
                    .insert_header("content-type", "text/event-stream")
                    .set_body_string(stream_body),
            )
            .mount(&server)
            .await;

        let client =
            OpenAiImageClient::new("sk-test", Some(format!("{}/v1", server.uri()))).unwrap();
        let mut events = Vec::new();
        let response = client
            .edit_with_events(
                EditImageRequest {
                    model: "gpt-image-2".to_string(),
                    prompt: "edit otter".to_string(),
                    images: vec![EditImageInput {
                        filename: "source.png".to_string(),
                        content_type: "image/png".to_string(),
                        data: vec![1, 2, 3],
                    }],
                    size: Some("1024x1024".to_string()),
                    quality: Some("medium".to_string()),
                    background: Some("opaque".to_string()),
                    output_format: Some("png".to_string()),
                    stream: Some(true),
                    partial_images: Some(1),
                    count: 1,
                },
                |event| {
                    events.push(event);
                    async {}
                },
            )
            .await
            .unwrap();

        assert_eq!(
            events,
            vec![
                ImageApiStreamEvent::PartialImage {
                    partial_image_index: 0,
                    b64_json: Some("cHJldmlldw==".to_string()),
                    revised_prompt: None,
                },
                ImageApiStreamEvent::Completed {
                    completed_image_count: 1
                },
            ]
        );
        assert_eq!(response.data.len(), 1);
        assert_eq!(response.data[0].b64_json, "ZmluYWw=");
        assert_eq!(
            response.data[0].revised_prompt.as_deref(),
            Some("edited otter")
        );
    }
}
