use crate::openai::types::{
    ChatCompletionRequest, ChatContent, ChatContentPart, ChatMessage, ChatTool, ResponsesRequest,
};
use serde_json::{Value, json};

/// Normalizes model IDs by removing the `openai-codex/` prefix when present.
#[must_use]
pub fn normalize_model(model: &str) -> String {
    model
        .strip_prefix("openai-codex/")
        .unwrap_or(model)
        .to_owned()
}

/// Converts a chat completions request into the JSON body expected by Codex.
pub fn to_codex_request(request: &ChatCompletionRequest) -> Value {
    let (instructions, input) = split_messages(&request.messages);
    let mut body = json!({
        "model": normalize_model(&request.model),
        "store": false,
        "stream": true,
        "instructions": instructions,
        "input": input,
        "text": { "verbosity": text_verbosity(request) },
        "include": ["reasoning.encrypted_content"],
        "tool_choice": convert_tool_choice(request.tool_choice.as_ref()).unwrap_or_else(|| json!("auto")),
        "parallel_tool_calls": request.parallel_tool_calls.unwrap_or(true)
    });

    insert_optional(
        &mut body,
        "service_tier",
        request.service_tier.clone().map(Value::from),
    );
    insert_optional(
        &mut body,
        "stop",
        request
            .stop
            .as_ref()
            .filter(|stop| !stop.is_empty())
            .cloned()
            .map(|stop| json!(stop)),
    );
    if let Some(tools) = request.tools.as_ref().filter(|tools| !tools.is_empty()) {
        body["tools"] = Value::Array(tools.iter().map(convert_tool).collect());
    }

    if let Some(effort) = request.reasoning_effort.as_deref() {
        body["reasoning"] = json!({
            "effort": clamp_reasoning_effort(&request.model, effort),
            "summary": "auto"
        });
    }

    strip_unsupported_keys(&mut body);

    body
}

fn split_messages(messages: &[ChatMessage]) -> (String, Vec<Value>) {
    messages
        .iter()
        .enumerate()
        .fold((Vec::new(), Vec::new()), |mut acc, (index, message)| {
            match message.role.as_str() {
                "system" | "developer" => {
                    if let Some(text) = message_text(message) {
                        acc.0.push(text);
                    }
                }
                "user" => acc.1.push(json!({
                    "role": "user",
                    "content": content_to_input_parts(message.content.as_ref())
                })),
                "assistant" => append_assistant_message(&mut acc.1, message, index),
                "tool" => {
                    if let Some(call_id) = message.tool_call_id.as_deref() {
                        acc.1.push(json!({
                            "type": "function_call_output",
                            "call_id": call_id,
                            "output": message_text(message).unwrap_or_default()
                        }));
                    }
                }
                _ => {}
            }
            acc
        })
        .map_first(|parts| parts.join("\n\n"))
}

fn append_assistant_message(input: &mut Vec<Value>, message: &ChatMessage, index: usize) {
    if let Some(text) = message_text(message).filter(|text| !text.is_empty()) {
        input.push(json!({
            "type": "message",
            "role": "assistant",
            "content": [{"type": "output_text", "text": text, "annotations": []}],
            "status": "completed",
            "id": format!("msg_{index}")
        }));
    }

    for tool_call in message.tool_calls.iter().flatten() {
        input.push(json!({
            "type": "function_call",
            "call_id": &tool_call.id,
            "name": &tool_call.function.name,
            "arguments": &tool_call.function.arguments
        }));
    }
}

fn content_to_input_parts(content: Option<&ChatContent>) -> Vec<Value> {
    match content {
        Some(ChatContent::Text(text)) => vec![json!({ "type": "input_text", "text": text })],
        Some(ChatContent::Parts(parts)) => parts.iter().filter_map(convert_content_part).collect(),
        None => Vec::new(),
    }
}

fn convert_content_part(part: &ChatContentPart) -> Option<Value> {
    match part.kind.as_str() {
        "text" => part
            .text
            .as_ref()
            .map(|text| json!({ "type": "input_text", "text": text })),
        "image_url" => part.image_url.as_ref().map(|image| {
            json!({
                "type": "input_image",
                "detail": image.detail.as_deref().unwrap_or("auto"),
                "image_url": image.url
            })
        }),
        _ => None,
    }
}

/// Converts one compatibility-layer tool definition into the upstream Codex shape.
///
/// # Panics
///
/// Panics when a tool declares `"function"` as its type but omits the
/// corresponding function metadata payload.
#[must_use]
pub fn convert_tool(tool: &ChatTool) -> Value {
    if tool.kind == "function" {
        let function = tool
            .function
            .as_ref()
            .expect("function tool metadata missing");
        let mut value = json!({
            "type": "function",
            "name": &function.name,
            "description": function.description.clone().unwrap_or_default(),
            "parameters": function.parameters.clone().unwrap_or_else(|| json!({ "type": "object" })),
            "strict": function.strict
        });
        if let Some(object) = value.as_object_mut() {
            object.extend(tool.extra.clone());
        }
        value
    } else {
        let mut value = Value::Object(tool.extra.clone());
        value["type"] = Value::String(tool.kind.clone());
        value
    }
}

/// Normalizes compatibility-layer tool choice values into the upstream Codex shape.
pub fn convert_tool_choice(tool_choice: Option<&Value>) -> Option<Value> {
    let choice = tool_choice?;
    if choice.is_string() {
        return Some(choice.clone());
    }

    let kind = choice.get("type").and_then(Value::as_str)?;
    if kind != "function" {
        return Some(choice.clone());
    }

    let name = choice
        .get("name")
        .or_else(|| choice.pointer("/function/name"))?
        .as_str()?;

    Some(json!({
        "type": "function",
        "name": name
    }))
}

/// Converts a Responses request plus normalized input items into the Codex body.
#[must_use]
pub fn responses_to_codex_request(request: &ResponsesRequest, input: &[Value]) -> Value {
    let text = request.extra.get("text").cloned().unwrap_or_else(|| {
        json!({ "verbosity": request.extra.get("text_verbosity").and_then(Value::as_str).unwrap_or("medium") })
    });
    let include = request
        .extra
        .get("include")
        .cloned()
        .unwrap_or_else(|| json!(["reasoning.encrypted_content"]));
    let mut body = json!({
        "model": normalize_model(&request.model),
        "store": request.should_store(),
        // Codex currently expects SSE upstream even when the downstream API
        // requested a one-shot JSON response.
        "stream": true,
        "input": input,
        "instructions": request.instructions.as_deref().unwrap_or(""),
        "text": text,
        "include": include,
        "tool_choice": convert_tool_choice(request.tool_choice.as_ref()).unwrap_or_else(|| json!("auto")),
        "parallel_tool_calls": request.parallel_tool_calls()
    });

    insert_optional(
        &mut body,
        "temperature",
        request.temperature.map(Value::from),
    );
    insert_optional(&mut body, "top_p", request.top_p.map(Value::from));
    insert_optional(
        &mut body,
        "max_output_tokens",
        request.max_output_tokens.map(Value::from),
    );
    insert_optional(&mut body, "stop", request.extra.get("stop").cloned());
    insert_optional(
        &mut body,
        "service_tier",
        request.service_tier.clone().map(Value::from),
    );
    if let Some(tools) = request.tools.as_ref().filter(|tools| !tools.is_empty()) {
        body["tools"] = Value::Array(tools.iter().map(convert_tool).collect());
    }
    if let Some(reasoning) = request.reasoning.clone() {
        body["reasoning"] = reasoning;
    }

    strip_unsupported_keys(&mut body);

    body
}

fn message_text(message: &ChatMessage) -> Option<String> {
    match message.content.as_ref()? {
        ChatContent::Text(text) => Some(text.clone()),
        ChatContent::Parts(parts) => {
            let text = parts
                .iter()
                .filter_map(|part| match part.kind.as_str() {
                    "text" => part.text.as_deref(),
                    _ => None,
                })
                .collect::<Vec<_>>()
                .join("\n");
            Some(text)
        }
    }
}

fn text_verbosity(request: &ChatCompletionRequest) -> String {
    request
        .extra
        .get("text_verbosity")
        .and_then(Value::as_str)
        .unwrap_or("medium")
        .to_owned()
}

fn clamp_reasoning_effort(model: &str, effort: &str) -> String {
    let id = normalize_model(model);
    if (id.starts_with("gpt-5.2")
        || id.starts_with("gpt-5.3")
        || id.starts_with("gpt-5.4")
        || id.starts_with("gpt-5.5"))
        && effort == "minimal"
    {
        "low".to_owned()
    } else if id == "gpt-5.1" && effort == "xhigh" {
        "high".to_owned()
    } else {
        effort.to_owned()
    }
}

fn insert_optional(body: &mut Value, key: &str, value: Option<Value>) {
    if let Some(value) = value {
        body[key] = value;
    }
}

fn strip_unsupported_keys(value: &mut Value) {
    match value {
        Value::Object(object) => {
            object.remove("cache_control");
            for nested in object.values_mut() {
                strip_unsupported_keys(nested);
            }
        }
        Value::Array(array) => {
            for nested in array {
                strip_unsupported_keys(nested);
            }
        }
        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
    }
}

trait TupleMapFirst<A, B> {
    fn map_first<C>(self, map: impl FnOnce(A) -> C) -> (C, B);
}

impl<A, B> TupleMapFirst<A, B> for (A, B) {
    fn map_first<C>(self, map: impl FnOnce(A) -> C) -> (C, B) {
        (map(self.0), self.1)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::openai::types::ChatCompletionRequest;

    fn request(value: Value) -> ChatCompletionRequest {
        serde_json::from_value(value).unwrap()
    }

    fn assert_key_absent_recursive(value: &Value, key: &str) {
        match value {
            Value::Object(object) => {
                assert!(
                    !object.contains_key(key),
                    "unexpected key `{key}` in object: {value}"
                );
                for nested in object.values() {
                    assert_key_absent_recursive(nested, key);
                }
            }
            Value::Array(array) => {
                for nested in array {
                    assert_key_absent_recursive(nested, key);
                }
            }
            Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
        }
    }

    #[test]
    fn strips_openai_codex_prefix() {
        assert_eq!(normalize_model("openai-codex/gpt-5.4"), "gpt-5.4");
        assert_eq!(normalize_model("gpt-5.4"), "gpt-5.4");
    }

    #[test]
    fn converts_chat_messages_to_responses_body() {
        let body = to_codex_request(&request(json!({
            "model": "openai-codex/gpt-5.4",
            "messages": [
                {"role": "system", "content": "be terse"},
                {"role": "user", "content": "hello"}
            ],
            "temperature": 0.2
        })));

        assert_eq!(body["model"], "gpt-5.4");
        assert_eq!(body["instructions"], "be terse");
        assert!(body.get("temperature").is_none());
        assert_eq!(body["input"][0]["content"][0]["type"], "input_text");
    }

    #[test]
    fn does_not_forward_unsupported_sampling_controls() {
        let body = to_codex_request(&request(json!({
            "model": "gpt-5.5",
            "messages": [{"role": "user", "content": "hello"}],
            "temperature": 0.2,
            "top_p": 0.7,
            "parallel_tool_calls": false,
            "stop": ["DONE"]
        })));

        assert!(body.get("temperature").is_none());
        assert!(body.get("top_p").is_none());
        assert_eq!(body["parallel_tool_calls"], false);
        assert_eq!(body["stop"], json!(["DONE"]));
    }

    #[test]
    fn converts_assistant_tool_calls_and_tool_results() {
        let body = to_codex_request(&request(json!({
            "model": "gpt-5.4",
            "messages": [
                {"role": "assistant", "tool_calls": [{
                    "id": "call_1",
                    "type": "function",
                    "function": {"name": "lookup", "arguments": "{\"q\":\"x\"}"}
                }]},
                {"role": "tool", "tool_call_id": "call_1", "content": "done"}
            ]
        })));

        assert_eq!(body["input"][0]["type"], "function_call");
        assert_eq!(body["input"][1]["type"], "function_call_output");
    }

    #[test]
    fn clamps_minimal_reasoning_for_new_codex_models() {
        let body = to_codex_request(&request(json!({
            "model": "gpt-5.5",
            "messages": [],
            "reasoning_effort": "minimal"
        })));

        assert_eq!(body["reasoning"]["effort"], "low");
    }

    #[test]
    fn does_not_forward_unsupported_chat_completion_token_limit() {
        let body = to_codex_request(&request(json!({
            "model": "gpt-5.4",
            "messages": [],
            "max_completion_tokens": 42
        })));

        assert_key_absent_recursive(&body, "max_output_tokens");
    }

    #[test]
    fn maps_chat_tool_choice_to_responses_tool_choice() {
        let body = to_codex_request(&request(json!({
            "model": "gpt-5.4",
            "messages": [],
            "tool_choice": {"type": "function", "function": {"name": "lookup"}}
        })));

        assert_eq!(
            body["tool_choice"],
            json!({"type": "function", "name": "lookup"})
        );
    }

    #[test]
    fn preserves_string_tool_choice() {
        let body = to_codex_request(&request(json!({
            "model": "gpt-5.4",
            "messages": [],
            "tool_choice": "required"
        })));

        assert_eq!(body["tool_choice"], "required");
    }

    #[test]
    fn responses_request_always_includes_instructions() {
        let request: ResponsesRequest = serde_json::from_value(json!({
            "model": "gpt-5.5",
            "input": "hello"
        }))
        .unwrap();
        let input = vec![json!({
            "role": "user",
            "content": [{"type": "input_text", "text": "hello"}]
        })];

        let body = responses_to_codex_request(&request, &input);

        assert_eq!(body["instructions"], "");
    }

    #[test]
    fn responses_requests_force_upstream_streaming() {
        let request: ResponsesRequest = serde_json::from_value(json!({
            "model": "gpt-5.5",
            "stream": false,
            "input": "hello"
        }))
        .unwrap();
        let input = vec![json!({
            "role": "user",
            "content": [{"type": "input_text", "text": "hello"}]
        })];

        let body = responses_to_codex_request(&request, &input);

        assert_eq!(body["stream"], true);
    }

    #[test]
    fn forwards_responses_controls_for_provider_filtering() {
        let request: ResponsesRequest = serde_json::from_value(json!({
            "model": "gpt-5.5",
            "temperature": 0.2,
            "top_p": 0.8,
            "max_output_tokens": 128,
            "stop": ["DONE"],
            "text": {"verbosity": "low"},
            "include": ["reasoning.encrypted_content"],
            "input": "hello"
        }))
        .unwrap();
        let input = vec![json!({
            "role": "user",
            "content": [{"type": "input_text", "text": "hello"}]
        })];

        let body = responses_to_codex_request(&request, &input);

        assert_eq!(body["temperature"], 0.2);
        assert_eq!(body["top_p"], 0.8);
        assert_eq!(body["max_output_tokens"], 128);
        assert_eq!(body["stop"], json!(["DONE"]));
        assert_eq!(body["text"]["verbosity"], "low");
        assert_eq!(body["include"], json!(["reasoning.encrypted_content"]));
    }

    #[test]
    fn strips_cache_control_recursively_from_chat_requests() {
        let body = to_codex_request(&request(json!({
            "model": "gpt-5.4",
            "messages": [{
                "role": "user",
                "content": "hello"
            }],
            "tools": [{
                "type": "function",
                "function": {
                    "name": "lookup",
                    "parameters": {"type": "object"}
                },
                "cache_control": {"type": "ephemeral"}
            }]
        })));

        assert_key_absent_recursive(&body, "cache_control");
    }

    #[test]
    fn strips_cache_control_recursively_from_responses_requests() {
        let request: ResponsesRequest = serde_json::from_value(json!({
            "model": "gpt-5.5",
            "tools": [{
                "type": "function",
                "function": {
                    "name": "lookup",
                    "parameters": {"type": "object"}
                },
                "cache_control": {"type": "ephemeral"}
            }]
        }))
        .unwrap();
        let input = vec![json!({
            "role": "developer",
            "content": [{
                "type": "input_text",
                "text": "be terse",
                "cache_control": {"type": "ephemeral"}
            }]
        })];

        let body = responses_to_codex_request(&request, &input);

        assert_key_absent_recursive(&body, "cache_control");
    }
}
