#![allow(clippy::doc_lazy_continuation)]

use std::fmt::Display;
use std::str::FromStr;

use clap::Args;
use clap::builder::{TypedValueParser, ValueParserFactory};
use itertools::Itertools;
use serde::{Deserialize, Deserializer, Serialize};

/// Marker type for frontend-owned `serve` arguments that `vllm-rs` recognizes
/// but does not support yet.
///
/// When passed as JSON args, it can be deserialized from any value, and
/// serializes back to the original value.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Unsupported(pub serde_json::Value);

impl FromStr for Unsupported {
    type Err = String;

    fn from_str(_s: &str) -> Result<Self, Self::Err> {
        Err("argument is not implemented in Rust frontend yet

Remove this unsupported argument to continue.

Alternatively, if you intend to pass it only to the Python engine, put it after `--` (e.g., `-- <arg>`).
This may lead to unexpected behavior as the Rust frontend will completely ignore that argument."
            .to_string())
    }
}

/// Marker type for no-op arguments that are accepted by the Rust frontend but
/// have no effect.
///
/// When passed as JSON args, it can be deserialized from any value, but always
/// serializes back to `null`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Noop;

impl<'de> Deserialize<'de> for Noop {
    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        Ok(Noop)
    }
}

impl ValueParserFactory for Noop {
    type Parser = NoopValueParser;

    fn value_parser() -> Self::Parser {
        NoopValueParser
    }
}

#[derive(Copy, Clone, Debug)]
pub struct NoopValueParser;

#[track_caller]
fn noop_warn(arg: impl Display) {
    tracing::warn!("argument '{arg}' currently has no effect in Rust frontend, ignoring");
}

impl TypedValueParser for NoopValueParser {
    type Value = Noop;

    fn parse_ref(
        &self,
        _cmd: &clap::Command,
        arg: Option<&clap::Arg>,
        _value: &std::ffi::OsStr,
    ) -> Result<Self::Value, clap::Error> {
        if let Some(arg) = arg {
            noop_warn(arg);
        }
        Ok(Noop)
    }
}

/// Frontend-owned Python `serve` arguments that `vllm-rs` recognizes but does
/// not support yet.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, PartialEq, Eq, Default, Args, Serialize, Deserialize)]
#[command(next_help_heading = "Options not implemented in Rust frontend yet")]
pub struct UnsupportedArgs {
    #[command(flatten)]
    #[serde(default, flatten)]
    top_level: TopLevelUnsupportedArgs,
    #[command(flatten)]
    #[serde(default, flatten)]
    engine: EngineUnsupportedArgs,
    #[command(flatten)]
    #[serde(default, flatten)]
    server: ServerUnsupportedArgs,
}

impl UnsupportedArgs {
    /// Check whether any unsupported arguments are set, and if so, return an
    /// error listing them. Also warn about any no-op arguments that are set
    /// but will be ignored.
    pub(crate) fn check(&self) -> Result<(), String> {
        let value = serde_json::to_value(self).unwrap();
        let map = value.as_object().unwrap();
        let mut unsupported = Vec::new();

        for (key, value) in map {
            if value.is_null() {
                noop_warn(key);
            } else {
                unsupported.push(key.as_str());
            }
        }

        if !unsupported.is_empty() {
            unsupported.sort_unstable();
            let bullets = unsupported.into_iter().map(|key| format!("- {key}")).join("\n");
            return Err(format!(
                "
The following arguments are not implemented in Rust frontend yet:
{bullets}

Remove these arguments to continue."
            ));
        }

        Ok(())
    }
}

/// Frontend-owned Python `vllm serve` top-level arguments that `vllm-rs`
/// recognizes but does not support yet.
///
/// Source of truth in Python vLLM:
/// - `vllm.entrypoints.openai.cli_args.make_arg_parser(...)`
/// - `vllm.entrypoints.cli.serve.ServeSubcommand.subparser_init(...)`
///
/// These are not part of `EngineArgs`, `AsyncEngineArgs`, `BaseFrontendArgs`,
/// or `FrontendArgs`. They live on the `serve` command itself and control
/// managed-engine / multi-process orchestration rather than the shared frontend
/// runtime config.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, PartialEq, Eq, Default, Args, Serialize, Deserialize)]
pub struct TopLevelUnsupportedArgs {
    /// How many API server processes to run. Defaults to data_parallel_size if
    /// not specified.
    #[arg(long, hide = true)]
    pub api_server_count: Option<Noop>,

    /// Read CLI options from a config file. Must be a YAML with the following
    /// options: https://docs.vllm.ai/en/latest/configuration/serve_args.html
    #[arg(long)]
    pub config: Option<Unsupported>,

    /// Launch a gRPC server instead of the HTTP OpenAI-compatible server.
    /// Requires: pip install vllm[grpc].
    #[arg(long, default_missing_value = "true", num_args = 0..=1)]
    pub grpc: Option<Unsupported>,
}

/// Frontend-owned Python engine arguments that `vllm-rs` recognizes but does
/// not support yet.
///
/// Source of truth in Python vLLM:
/// - `vllm.engine.arg_utils.EngineArgs.add_cli_args(...)`
/// - `vllm.engine.arg_utils.AsyncEngineArgs.add_cli_args(...)`
///
/// These arguments are declared through the Python engine-args surface, but
/// they are still frontend-owned: the API server / AsyncLLM layer reads them
/// for tokenizer setup, request validation, routing, logging, and other
/// frontend behavior, so Rust must recognize them rather than treating them as
/// pure engine passthrough.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, PartialEq, Eq, Default, Args, Serialize, Deserialize)]
pub struct EngineUnsupportedArgs {
    /// Name or path of the Hugging Face tokenizer to use. If unspecified, model
    /// name or path will be used.
    #[arg(long)]
    pub tokenizer: Option<Unsupported>,

    /// Name or path of the Hugging Face config to use. If unspecified, model
    /// name or path will be used.
    #[arg(long)]
    pub hf_config_path: Option<Unsupported>,

    /// Allowing API requests to read local images or videos from directories
    /// specified by the server file system. This is a security risk. Should
    /// only be enabled in trusted environments.
    #[arg(long)]
    pub allowed_local_media_path: Option<Unsupported>,

    /// If set, only media URLs that belong to this domain can be used for
    /// multi-modal inputs.
    #[arg(long)]
    pub allowed_media_domains: Option<Unsupported>,

    /// The specific revision to use for the tokenizer on the Hugging Face Hub.
    /// It can be a branch name, a tag name, or a commit id. If unspecified,
    /// will use the default version.
    #[arg(long)]
    pub tokenizer_revision: Option<Unsupported>,

    /// Skip initialization of tokenizer and detokenizer. Expects valid
    /// `prompt_token_ids` and `None` for prompt from the input. The generated
    /// output will contain token ids.
    #[arg(
        long,
        visible_alias = "no-skip-tokenizer-init",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub skip_tokenizer_init: Option<Unsupported>,

    /// If `True`, enables passing text embeddings as inputs via the
    /// `prompt_embeds` key.
    ///
    /// WARNING: The vLLM engine may crash if incorrect shape of embeddings is
    /// passed. Only enable this flag for trusted users!
    #[arg(
        long,
        visible_alias = "no-enable-prompt-embeds",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub enable_prompt_embeds: Option<Unsupported>,

    /// The token to use as HTTP bearer authorization for remote files. If
    /// `True`, will use the token generated when running `hf auth login`
    /// (stored in `~/.cache/huggingface/token`).
    #[arg(long, default_missing_value = "true", num_args = 0..=1)]
    pub hf_token: Option<Unsupported>,

    /// If a dictionary, contains arguments to be forwarded to the Hugging Face
    /// config. If a callable, it is called to update the HuggingFace config.
    #[arg(long)]
    pub hf_overrides: Option<Unsupported>,

    /// The folder path to the generation config. Defaults to `"auto"`, the
    /// generation config will be loaded from model path. If set to `"vllm"`, no
    /// generation config is loaded, vLLM defaults will be used. If set to a
    /// folder path, the generation config will be loaded from the specified
    /// folder path. If `max_new_tokens` is specified in generation config,
    /// then it sets a server-wide limit on the number of output tokens for
    /// all requests.
    #[arg(long)]
    pub generation_config: Option<Unsupported>,

    /// IOProcessor plugin name to load at model startup
    #[arg(long)]
    pub io_processor_plugin: Option<Unsupported>,

    /// Path to a dynamically reasoning parser plugin that can be dynamically
    /// loaded and registered.
    #[arg(long)]
    pub reasoning_parser_plugin: Option<Unsupported>,

    /// Rank of the data parallel group.
    #[arg(long, env = "VLLM_DP_RANK")]
    pub data_parallel_rank: Option<Unsupported>,

    /// Whether to use "hybrid" DP LB mode. Applies only to online serving
    /// and when data_parallel_size > 0. Enables running an AsyncLLM
    /// and API server on a "per-node" basis where vLLM load balances
    /// between local data parallel ranks, but an external LB balances
    /// between vLLM nodes/replicas. Set explicitly in conjunction with
    /// --data-parallel-start-rank.
    #[arg(
        long,
        visible_alias = "no-data-parallel-hybrid-lb",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub data_parallel_hybrid_lb: Option<Unsupported>,

    /// Whether to use "external" DP LB mode. Applies only to online serving
    /// and when data_parallel_size > 0. This is useful for a "one-pod-per-rank"
    /// wide-EP setup in Kubernetes. Set implicitly when --data-parallel-rank
    /// is provided explicitly to vllm serve.
    #[arg(
        long,
        visible_alias = "no-data-parallel-external-lb",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub data_parallel_external_lb: Option<Unsupported>,

    /// This feature is work in progress and no prefill optimization takes place
    /// with this flag enabled currently.
    #[arg(
        long,
        visible_alias = "no-kv-sharing-fast-prefill",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub kv_sharing_fast_prefill: Option<Unsupported>,

    /// The maximum number of input items and options allowed per
    /// prompt for each modality.
    #[arg(long)]
    pub limit_mm_per_prompt: Option<Unsupported>,

    /// Additional args passed to process media inputs, keyed by modalities.
    #[arg(long)]
    pub media_io_kwargs: Option<Unsupported>,

    /// Arguments to be forwarded to the model's processor for multi-modal data,
    /// e.g., image processor.
    #[arg(long)]
    pub mm_processor_kwargs: Option<Unsupported>,

    /// The size (in GiB) of the multi-modal processor cache.
    #[arg(long)]
    pub mm_processor_cache_gb: Option<Unsupported>,

    /// Type of cache to use for the multi-modal preprocessor/mapper.
    #[arg(long)]
    pub mm_processor_cache_type: Option<Unsupported>,

    /// Dictionary mapping specific modalities to LoRA model paths.
    #[arg(long)]
    pub default_mm_loras: Option<Unsupported>,

    /// Target URL to which OpenTelemetry traces will be sent.
    #[arg(long)]
    pub otlp_traces_endpoint: Option<Unsupported>,

    /// It makes sense to set this only if `--otlp-traces-endpoint` is set.
    #[arg(long)]
    pub collect_detailed_traces: Option<Unsupported>,

    /// The interval (or buffer size) for streaming in terms of token length.
    #[arg(long)]
    pub stream_interval: Option<Unsupported>,

    /// Structured outputs configuration.
    #[arg(long)]
    pub structured_outputs_config: Option<Noop>,

    /// Log aggregate rather than per-engine statistics when using data
    /// parallelism.
    #[arg(long, default_missing_value = "true", num_args = 0..=1)]
    pub aggregate_engine_logging: Option<Unsupported>,
}

/// Frontend-owned Python OpenAI server arguments that `vllm-rs` recognizes but
/// does not support yet.
///
/// Source of truth in Python vLLM:
/// - `vllm.entrypoints.openai.cli_args.BaseFrontendArgs`
/// - `vllm.entrypoints.openai.cli_args.FrontendArgs`
///
/// These are not engine args. They belong to the Python OpenAI-compatible
/// frontend / API-server layer itself, for example chat-template configuration,
/// tool/frontend behavior, TLS / CORS / HTTP server settings, and other
/// northbound server knobs.
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, PartialEq, Eq, Default, Args, Serialize, Deserialize)]
pub struct ServerUnsupportedArgs {
    /// LoRA modules configurations in either 'name=path' format or JSON format
    /// or JSON list format. Example (old format): `'name=path'` Example (new
    /// format): `{"name": "name", "path": "lora_path",
    /// "base_model_name": "id"}`
    #[arg(long)]
    pub lora_modules: Option<Unsupported>,

    /// Whether to trust the chat template provided in the request. If False,
    /// the server will always use the chat template specified by
    /// `--chat-template` or the ones from tokenizer.
    #[arg(
        long,
        visible_alias = "no-trust-request-chat-template",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub trust_request_chat_template: Option<Unsupported>,

    /// The role name to return if `request.add_generation_prompt=true`.
    #[arg(long)]
    pub response_role: Option<Unsupported>,

    /// When `--max-logprobs` is specified, represents single tokens as
    /// strings of the form 'token_id:{token_id}' so that tokens that are not
    /// JSON-encodable can be identified.
    #[arg(
        long,
        visible_alias = "no-return-tokens-as-token-ids",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub return_tokens_as_token_ids: Option<Unsupported>,

    /// Enable auto tool choice for supported models. Use `--tool-call-parser`
    /// to specify which parser to use.
    #[arg(
        long,
        visible_alias = "no-enable-auto-tool-choice",
        default_missing_value = "true",
        num_args = 0..=1,
        hide = true
    )]
    pub enable_auto_tool_choice: Option<Noop>,

    /// If specified, exclude tool definitions in prompts when
    /// tool_choice='none'.
    #[arg(
        long,
        visible_alias = "no-exclude-tools-when-tool-choice-none",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub exclude_tools_when_tool_choice_none: Option<Unsupported>,

    /// Special the tool parser plugin write to parse the model-generated tool
    /// into OpenAI API format, the name register in this plugin can be used in
    /// `--tool-call-parser`.
    #[arg(long)]
    pub tool_parser_plugin: Option<Unsupported>,

    /// Comma-separated list of host:port pairs (IPv4, IPv6, or hostname).
    /// Examples: 127.0.0.1:8000, [::1]:8000, localhost:1234. Or `demo` for
    /// built-in demo tools (browser and Python code interpreter). WARNING:
    /// The `demo` Python tool executes model-generated code in Docker without
    /// network isolation by default. See the security guide for more
    /// information.
    #[arg(long)]
    pub tool_server: Option<Unsupported>,

    /// Path to logging config JSON file for both vllm and uvicorn
    #[arg(long, /* env = "VLLM_LOGGING_CONFIG_PATH" */)]
    pub log_config_file: Option<Unsupported>,

    /// Max number of prompt characters or prompt ID numbers being printed in
    /// log. The default of None means unlimited.
    #[arg(long)]
    pub max_log_len: Option<Unsupported>,

    /// If set to True, enable tracking server_load_metrics in the app state.
    #[arg(
        long,
        visible_alias = "no-enable-server-load-tracking",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub enable_server_load_tracking: Option<Noop>,

    /// If set to True, including usage on every request.
    #[arg(
        long,
        visible_alias = "no-enable-force-include-usage",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub enable_force_include_usage: Option<Unsupported>,

    /// Enable the `/tokenizer_info` endpoint. May expose chat
    /// templates and other tokenizer configuration.
    #[arg(
        long,
        visible_alias = "no-enable-tokenizer-info-endpoint",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub enable_tokenizer_info_endpoint: Option<Unsupported>,

    /// If set to True, log model outputs (generations).
    /// Requires `--enable-log-requests`. As with `--enable-log-requests`,
    /// information is only logged at INFO level at maximum.
    #[arg(
        long,
        visible_alias = "no-enable-log-outputs",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub enable_log_outputs: Option<Unsupported>,

    /// If set to False, output deltas will not be logged. Relevant only if
    /// --enable-log-outputs is set.
    #[arg(
        long,
        visible_alias = "no-enable-log-deltas",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub enable_log_deltas: Option<Unsupported>,

    /// If set to True, log the stack trace of error responses
    #[arg(
        long,
        // env = "VLLM_SERVER_DEV_MODE",
        visible_alias = "no-log-error-stack",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub log_error_stack: Option<Unsupported>,

    /// If set to True, only enable the Tokens In<>Out endpoint.
    /// This is intended for use in a Disaggregated Everything setup.
    #[arg(
        long,
        visible_alias = "no-tokens-only",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub tokens_only: Option<Unsupported>,

    /// Log level for uvicorn.
    #[arg(long)]
    pub uvicorn_log_level: Option<Unsupported>,

    /// Disable uvicorn access log.
    #[arg(
        long,
        visible_alias = "no-disable-uvicorn-access-log",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub disable_uvicorn_access_log: Option<Noop>,

    /// Comma-separated list of endpoint paths to exclude from uvicorn access
    /// logs. This is useful to reduce log noise from high-frequency endpoints
    /// like health checks. Example: "/health,/metrics,/ping".
    /// When set, access logs for requests to these paths will be suppressed
    /// while keeping logs for other endpoints.
    #[arg(long)]
    pub disable_access_log_for_endpoints: Option<Noop>,

    /// The file path to the SSL key file.
    #[arg(long)]
    pub ssl_keyfile: Option<Unsupported>,

    /// The file path to the SSL cert file.
    #[arg(long)]
    pub ssl_certfile: Option<Unsupported>,

    /// The CA certificates file.
    #[arg(long)]
    pub ssl_ca_certs: Option<Unsupported>,

    /// Refresh SSL Context when SSL certificate files change
    #[arg(
        long,
        visible_alias = "no-enable-ssl-refresh",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub enable_ssl_refresh: Option<Unsupported>,

    /// Whether client certificate is required (see stdlib ssl module's).
    #[arg(long)]
    pub ssl_cert_reqs: Option<Unsupported>,

    /// SSL cipher suites for HTTPS (TLS 1.2 and below only).
    /// Example: 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305'
    #[arg(long)]
    pub ssl_ciphers: Option<Unsupported>,

    /// FastAPI root_path when app is behind a path based routing proxy.
    #[arg(long)]
    pub root_path: Option<Unsupported>,

    /// Additional ASGI middleware to apply to the app. We accept multiple
    /// --middleware arguments. The value should be an import path. If a
    /// function is provided, vLLM will add it to the server using
    /// `@app.middleware('http')`. If a class is provided, vLLM will
    /// add it to the server using `app.add_middleware()`.
    #[arg(long)]
    pub middleware: Option<Unsupported>,

    /// Disable FastAPI's OpenAPI schema, Swagger UI, and ReDoc endpoint.
    #[arg(
        long,
        visible_alias = "no-disable-fastapi-docs",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub disable_fastapi_docs: Option<Unsupported>,

    /// Maximum size (bytes) of an incomplete HTTP event (header or body) for
    /// h11 parser. Helps mitigate header abuse. Default: 4194304 (4 MB).
    #[arg(long)]
    pub h11_max_incomplete_event_size: Option<Unsupported>,

    /// Maximum number of HTTP headers allowed in a request for h11 parser.
    /// Helps mitigate header abuse. Default: 256.
    #[arg(long)]
    pub h11_max_header_count: Option<Unsupported>,

    /// Enable offline FastAPI documentation for air-gapped environments.
    /// Uses vendored static assets bundled with vLLM.
    #[arg(
        long,
        visible_alias = "no-enable-offline-docs",
        default_missing_value = "true",
        num_args = 0..=1
    )]
    pub enable_offline_docs: Option<Unsupported>,
}
