use super::types::ChatCompletionRequest;
use crate::error::{ApiError, bail_invalid_request};
use crate::routes::openai::utils::types::{ChatMessage, Tool, ToolChoice, ToolChoiceValue};

/// Enforce the minimal compatibility contract for the Rust OpenAI server.
pub(super) fn validate_request_compat(
    request: &ChatCompletionRequest,
    served_model_names: &[String],
) -> Result<(), ApiError> {
    if !served_model_names.iter().any(|n| n == &request.model) {
        return Err(ApiError::model_not_found(request.model.clone()));
    }

    if request.stream_options.is_some() && !request.stream {
        bail_invalid_request!(
            param = "stream_options",
            "stream_options are only supported when stream=true."
        );
    }

    if request.n.unwrap_or(1) > 1 {
        bail_invalid_request!(param = "n", "Only n=1 is supported.");
    }

    if request.top_logprobs.is_some() && !request.logprobs {
        bail_invalid_request!(
            param = "top_logprobs",
            "top_logprobs can only be used when logprobs=true."
        );
    }

    if let Some(prompt_logprobs) = request.prompt_logprobs {
        if prompt_logprobs < 0 && prompt_logprobs != -1 {
            bail_invalid_request!(
                param = "prompt_logprobs",
                "prompt_logprobs must be a non-negative value or -1."
            );
        }

        if request.stream && (prompt_logprobs > 0 || prompt_logprobs == -1) {
            bail_invalid_request!(
                param = "prompt_logprobs",
                "prompt_logprobs are not available when stream=true."
            );
        }
    }

    if let Some(tools) = request.tools.as_ref() {
        validate_function_tools(tools, "tools")?;
    }

    for message in &request.messages {
        if let ChatMessage::Developer {
            tools: Some(tools), ..
        } = message
        {
            validate_function_tools(tools, "messages[].tools")?;
        }
    }

    if let Some(tool_choice) = &request.tool_choice {
        match tool_choice {
            ToolChoice::Value(ToolChoiceValue::Auto | ToolChoiceValue::None) => {}
            ToolChoice::Value(ToolChoiceValue::Required) => {
                bail_invalid_request!(
                    param = "tool_choice",
                    "tool_choice=required is not supported yet."
                );
            }
            ToolChoice::Function { .. } => {
                bail_invalid_request!(
                    param = "tool_choice",
                    "Named function tool_choice is not supported yet."
                );
            }
            ToolChoice::AllowedTools { .. } => {
                bail_invalid_request!(
                    param = "tool_choice",
                    "allowed_tools tool_choice is not supported yet."
                );
            }
        }
    }

    if request.use_beam_search {
        bail_invalid_request!(
            param = "use_beam_search",
            "use_beam_search is not supported."
        );
    }

    // ---- Reject parameters that are accepted for deserialization but not yet
    // implemented ----

    if request.parallel_tool_calls.is_some() {
        bail_invalid_request!(
            param = "parallel_tool_calls",
            "parallel_tool_calls is not supported."
        );
    }

    reject_non_default(
        request.length_penalty.as_ref(),
        "length_penalty",
        "length_penalty is not supported.",
    )?;
    if !request.spaces_between_special_tokens {
        bail_invalid_request!(
            param = "spaces_between_special_tokens",
            "spaces_between_special_tokens is not supported."
        );
    }
    reject_non_default(
        request.truncate_prompt_tokens.as_ref(),
        "truncate_prompt_tokens",
        "truncate_prompt_tokens is not supported.",
    )?;
    reject_non_default(
        request.thinking_token_budget.as_ref(),
        "thinking_token_budget",
        "thinking_token_budget is not supported.",
    )?;
    if !request.include_reasoning {
        bail_invalid_request!(
            param = "include_reasoning",
            "include_reasoning is not supported."
        );
    }
    reject_non_default(
        request.media_io_kwargs.as_ref(),
        "media_io_kwargs",
        "media_io_kwargs is not supported.",
    )?;
    reject_non_default(
        request.mm_processor_kwargs.as_ref(),
        "mm_processor_kwargs",
        "mm_processor_kwargs is not supported.",
    )?;
    reject_non_default(
        request.repetition_detection.as_ref(),
        "repetition_detection",
        "repetition_detection is not supported.",
    )?;

    if let Some(options) = &request.stream_options
        && options.continuous_usage_stats.is_some()
    {
        bail_invalid_request!(
            param = "stream_options",
            "continuous_usage_stats is not supported."
        );
    }

    Ok(())
}

/// Reject one option unless it is entirely absent.
fn reject_non_default<T>(
    value: Option<&T>,
    param: &'static str,
    message: &str,
) -> Result<(), ApiError> {
    if value.is_some() {
        bail_invalid_request!(param = param, "{}", message);
    }
    Ok(())
}

fn validate_function_tools(tools: &[Tool], param: &'static str) -> Result<(), ApiError> {
    for tool in tools {
        if tool.tool_type != "function" {
            bail_invalid_request!(param = param, "Only function tools are supported.");
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use serde_json::json;
    use vllm_chat::ReasoningEffort;

    use super::validate_request_compat;
    use crate::routes::openai::chat_completions::types::ChatCompletionRequest;
    use crate::routes::openai::utils::structured_outputs::ResponseFormat;
    use crate::routes::openai::utils::types::{
        ChatMessage, Function, FunctionChoice, MessageContent, StringOrArray, Tool, ToolChoice,
        ToolChoiceValue, ToolReference,
    };

    fn served(names: &[&str]) -> Vec<String> {
        names.iter().map(|s| s.to_string()).collect()
    }

    fn base_request() -> ChatCompletionRequest {
        ChatCompletionRequest {
            model: "Qwen/Qwen1.5-0.5B-Chat".to_string(),
            messages: vec![ChatMessage::User {
                content: MessageContent::Text("hello".to_string()),
                name: None,
            }],
            stream: true,
            ..Default::default()
        }
    }

    #[test]
    fn validate_request_compat_accepts_stop() {
        let request = ChatCompletionRequest {
            stop: Some(StringOrArray::String("stop".to_string())),
            ..base_request()
        };

        validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"]))
            .expect("stop strings should be accepted");
    }

    #[test]
    fn validate_request_compat_accepts_non_zero_penalties_and_function_tools() {
        let request = ChatCompletionRequest {
            frequency_penalty: Some(0.5),
            presence_penalty: Some(0.25),
            min_p: Some(0.2),
            repetition_penalty: Some(1.1),
            seed: Some(7),
            ..base_request()
        };
        validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"]))
            .expect("sampling fields should be accepted");

        let request = ChatCompletionRequest {
            tools: Some(vec![Tool {
                tool_type: "function".to_string(),
                function: Function {
                    name: "tool".to_string(),
                    description: None,
                    parameters: json!({}),
                    strict: None,
                },
            }]),
            ..base_request()
        };
        validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"]))
            .expect("function tools should be accepted");

        let request = ChatCompletionRequest {
            messages: vec![ChatMessage::Developer {
                content: MessageContent::Text("policy".to_string()),
                tools: Some(vec![Tool {
                    tool_type: "function".to_string(),
                    function: Function {
                        name: "tool".to_string(),
                        description: None,
                        parameters: json!({}),
                        strict: None,
                    },
                }]),
                name: None,
            }],
            ..base_request()
        };
        validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"]))
            .expect("developer function tools should be accepted");
    }

    #[test]
    fn validate_request_compat_rejects_non_function_developer_tools() {
        let request = ChatCompletionRequest {
            messages: vec![ChatMessage::Developer {
                content: MessageContent::Text("policy".to_string()),
                tools: Some(vec![Tool {
                    tool_type: "mcp".to_string(),
                    function: Function {
                        name: "tool".to_string(),
                        description: None,
                        parameters: json!({}),
                        strict: None,
                    },
                }]),
                name: None,
            }],
            ..base_request()
        };

        assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
    }

    #[test]
    fn validate_request_compat_accepts_output_logprobs() {
        let request = ChatCompletionRequest {
            logprobs: true,
            ..base_request()
        };
        validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"]))
            .expect("logprobs should be accepted");
    }

    #[test]
    fn validate_request_compat_accepts_reasoning_effort() {
        let request = ChatCompletionRequest {
            reasoning_effort: Some(ReasoningEffort::Max),
            chat_template_kwargs: Some(HashMap::from([(
                "reasoning_effort".to_string(),
                json!("low"),
            )])),
            ..base_request()
        };

        validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"]))
            .expect("reasoning_effort should be accepted");
    }

    #[test]
    fn validate_request_compat_rejects_top_logprobs_without_logprobs() {
        let request = ChatCompletionRequest {
            top_logprobs: Some(0),
            ..base_request()
        };
        assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
    }

    #[test]
    fn validate_request_compat_rejects_streaming_prompt_logprobs_requests() {
        let request = ChatCompletionRequest {
            prompt_logprobs: Some(1),
            ..base_request()
        };
        assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());

        let request = ChatCompletionRequest {
            prompt_logprobs: Some(-1),
            ..base_request()
        };
        assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
    }

    #[test]
    fn validate_request_compat_rejects_invalid_prompt_logprobs_value() {
        let request = ChatCompletionRequest {
            stream: false,
            prompt_logprobs: Some(-2),
            ..base_request()
        };
        assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
    }

    #[test]
    fn validate_request_compat_accepts_response_format() {
        let request = ChatCompletionRequest {
            response_format: Some(ResponseFormat::Text),
            ..base_request()
        };
        validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"]))
            .expect("response_format=text should be accepted");

        let request = ChatCompletionRequest {
            response_format: Some(ResponseFormat::JsonObject),
            ..base_request()
        };
        validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"]))
            .expect("response_format=json_object should be accepted");
    }

    #[test]
    fn validate_request_compat_accepts_noop_tool_choice_none() {
        let request = ChatCompletionRequest {
            tool_choice: Some(ToolChoice::Value(ToolChoiceValue::None)),
            ..base_request()
        };

        validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"]))
            .expect("tool_choice=none is ok");
    }

    #[test]
    fn validate_request_compat_rejects_required_and_named_tool_choices() {
        let required = ChatCompletionRequest {
            tool_choice: Some(ToolChoice::Value(ToolChoiceValue::Required)),
            ..base_request()
        };
        assert!(validate_request_compat(&required, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());

        let named = ChatCompletionRequest {
            tool_choice: Some(ToolChoice::Function {
                tool_type: "function".to_string(),
                function: FunctionChoice {
                    name: "tool".to_string(),
                },
            }),
            ..base_request()
        };
        assert!(validate_request_compat(&named, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());

        let allowed_tools = ChatCompletionRequest {
            tool_choice: Some(ToolChoice::AllowedTools {
                tool_type: "allowed_tools".to_string(),
                mode: "auto".to_string(),
                tools: vec![ToolReference::Function {
                    name: "tool".to_string(),
                }],
            }),
            ..base_request()
        };
        assert!(
            validate_request_compat(&allowed_tools, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err()
        );
    }
}
