use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Debug)]
pub struct ApiRequest {
    pub method: String,
    pub path_and_query: String,
    pub body: Option<SearchBody>,
}

#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct UrlQueryParams {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub allow_no_indices: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub allow_partial_search_results: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub batched_reduce_size: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ccs_minimize_roundtrips: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub docvalue_fields: Option<String>, // array of strings, comma-separated
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expand_wildcards: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub explain: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ignore_throttled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ignore_unavailable: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_concurrent_shard_requests: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pre_filter_shard_size: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub preference: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub q: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub request_cache: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rest_total_hits_as_int: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub routing: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scroll: Option<String>, // string is "time value"
    #[serde(skip_serializing_if = "Option::is_none")]
    pub search_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub seq_no_primary_term: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub size: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sort: Option<String>, // array of strings, comma-separated
    #[serde(skip_serializing_if = "Option::is_none")]
    pub _source: Option<bool>, // TODO: bool or string
    #[serde(skip_serializing_if = "Option::is_none")]
    pub _source_excludes: Option<String>, // array of strings, comma-separated
    #[serde(skip_serializing_if = "Option::is_none")]
    pub _source_includes: Option<String>, // array of strings, comma-separated
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stats: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stored_fields: Option<String>, // array of strings, comma-separated
    #[serde(skip_serializing_if = "Option::is_none")]
    pub suggest_field: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub suggest_text: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub terminate_after: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub timeout: Option<String>, // string is "time units"
    #[serde(skip_serializing_if = "Option::is_none")]
    pub track_scores: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub track_total_hits: Option<bool>, // XXX: bool or integer
    #[serde(skip_serializing_if = "Option::is_none")]
    pub typed_keys: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version: Option<bool>,

    // additional generic params
    #[serde(skip_serializing_if = "Option::is_none")]
    pub human: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pretty: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub filter_path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_trace: Option<bool>,
}

// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct SearchBody {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub query: Option<ApiQuery>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub highlight: Option<ApiHighlight>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub collapse: Option<ApiCollapse>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub post_filter: Option<ApiQuery>, // TODO: leaf query only?
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rescore: Option<ApiRescore>, // TODO: single or an array of rescore objects
    // script_fields disabled
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aggs: Option<HashMap<String, ApiAggregation>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aggregations: Option<HashMap<String, ApiAggregation>>,

    // https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sort: Option<Vec<SortElement>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub slice: Option<ApiSlice>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stored_fields: Option<String>, // array of strings, or "_none_"

    // overlap with URL query parameters
    #[serde(skip_serializing_if = "Option::is_none")]
    pub docvalue_fields: Option<Vec<DocValOrString>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub explain: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub min_score: Option<Num>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub seq_no_primary_term: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub size: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub _source: Option<bool>, // XXX: bool, string, or object
    #[serde(skip_serializing_if = "Option::is_none")]
    pub terminate_after: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub timeout: Option<String>, // string is "time units"
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct ScrollBody {
    pub scroll_id: StringOrArray,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scroll: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct ApiSlice {
    id: u32,
    max: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    field: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct ApiRescore {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub query: Option<ApiQuery>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub window_size: Option<u32>,
}

// TODO: could revert to having query types as an enum, with flattening
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct ApiQuery {
    // compound queries
    #[serde(rename = "bool")]
    #[serde(skip_serializing_if = "Option::is_none")]
    bool_query: Option<BoolQuery>,
    #[serde(skip_serializing_if = "Option::is_none")]
    boosting: Option<BoostingQuery>,
    #[serde(skip_serializing_if = "Option::is_none")]
    constant_score: Option<ConstantScoreQuery>,

    // fulltext (leaf) queries
    #[serde(rename = "match")]
    #[serde(skip_serializing_if = "Option::is_none")]
    match_query: Option<HashMap<String, MatchQueryOrString>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    match_phrase: Option<HashMap<String, QueryFieldOrString>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    multi_match: Option<MultiMatchQuery>,
    #[serde(skip_serializing_if = "Option::is_none")]
    query_string: Option<QueryStringQuery>,
    #[serde(skip_serializing_if = "Option::is_none")]
    simple_query_string: Option<QueryStringQuery>,

    // term-level (leaf) queries
    #[serde(skip_serializing_if = "Option::is_none")]
    exists: Option<SimpleFieldOrString>,
    #[serde(skip_serializing_if = "Option::is_none")]
    match_all: Option<SimpleBoost>,
    #[serde(skip_serializing_if = "Option::is_none")]
    match_none: Option<SimpleBoost>,
    #[serde(skip_serializing_if = "Option::is_none")]
    ids: Option<IdsQuery>,
    #[serde(skip_serializing_if = "Option::is_none")]
    wildcard: Option<HashMap<String, TermQueryOrValue>>, // also works for wildcard
    #[serde(skip_serializing_if = "Option::is_none")]
    prefix: Option<HashMap<String, TermQueryOrValue>>, // also works for prefix query
    #[serde(skip_serializing_if = "Option::is_none")]
    range: Option<HashMap<String, RangeQuery>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    term: Option<HashMap<String, TermQueryOrValue>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    // TODO: boost in terms query
    terms: Option<HashMap<String, Vec<String>>>,

    // other
    #[serde(skip_serializing_if = "Option::is_none")]
    nested: Option<NestedQuery>,
    #[serde(skip_serializing_if = "Option::is_none")]
    rescore_query: Option<Box<ApiQuery>>,

    // fields as part of a rescore query
    #[serde(skip_serializing_if = "Option::is_none")]
    score_mode: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    query_weight: Option<Num>,
    #[serde(skip_serializing_if = "Option::is_none")]
    rescore_query_weight: Option<Num>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum ApiQueryOrArray {
    Single(ApiQuery),
    Array(Vec<ApiQuery>),
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum ApiQueryNamedOrArray {
    Named(HashMap<String, ApiQuery>),
    Array(Vec<ApiQuery>),
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct ApiHighlight {
    // TODO: fields could also be an array of strings?
    fields: HashMap<String, HighlightField>,

    #[serde(flatten)]
    settings: HighlightField,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum SortMapValue {
    String(String),
    Object {
        order: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        mode: Option<String>,
    },
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum SortElement {
    String(String),
    Object(HashMap<String, SortMapValue>),
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum DocValOrString {
    String(String),
    Object {
        field: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        format: Option<String>,
    },
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum MatchQueryOrString {
    Object(MatchQuery),
    String(String),
}

// https://www.elastic.co/guide/en/elasticsearch/reference/7.9/query-dsl-match-query.html
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct MatchQuery {
    query: String,

    #[serde(flatten)]
    options: MatchOptions,
}

// https://www.elastic.co/guide/en/elasticsearch/reference/7.9/query-dsl-multi-match-query.html
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct MultiMatchQuery {
    query: String,
    fields: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "type")]
    query_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    tie_breaker: Option<Num>,
    #[serde(flatten)]
    options: MatchOptions,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct MatchOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    analyzer: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    auto_generate_synonyms_phrase_query: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fuzziness: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    max_expansions: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    prefix_length: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fuzzy_transpositions: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fuzzy_rewrite: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    lenient: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    operator: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    minimum_should_match: Option<StringOrNum>,
    #[serde(skip_serializing_if = "Option::is_none")]
    zero_terms_query: Option<String>,
}

// https://www.elastic.co/guide/en/elasticsearch/reference/7.9/query-dsl-query-string-query.html
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct QueryStringQuery {
    query: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "type")]
    query_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    default_field: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    allow_leading_wildcard: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    analyze_wildcard: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    analyzer: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    auto_generate_synonyms_phrase_query: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    boost: Option<Num>,
    #[serde(skip_serializing_if = "Option::is_none")]
    default_operator: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    enable_position_increments: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fields: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fuzziness: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fuzzy_max_expansions: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fuzzy_prefix_length: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fuzzy_transpositions: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    lenient: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    max_determinized_states: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    minimum_should_match: Option<StringOrNum>,
    #[serde(skip_serializing_if = "Option::is_none")]
    quote_analyzer: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    phrase_slop: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    quote_field_suffix: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    rewrite: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    time_zone: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct SimpleBoost {
    #[serde(skip_serializing_if = "Option::is_none")]
    boost: Option<Num>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct IdsQuery {
    values: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum TermQueryOrValue {
    String(String),
    Int(u64),
    Float(f64),
    Boolean(bool),
    Object(TermQuery),
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct TermQuery {
    value: StringOrNumOrBool,
    #[serde(skip_serializing_if = "Option::is_none")]
    rewrite: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    boost: Option<Num>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum StringOrNum {
    String(String),
    Int(u64),
    Float(f64),
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum StringOrNumOrBool {
    String(String),
    Int(u64),
    Float(f64),
    Boolean(bool),
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum Num {
    Int(u64),
    Float(f64),
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum StringOrArray {
    String(String),
    Array(Vec<String>),
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct RangeQuery {
    #[serde(skip_serializing_if = "Option::is_none")]
    gt: Option<StringOrNum>,
    #[serde(skip_serializing_if = "Option::is_none")]
    gte: Option<StringOrNum>,
    #[serde(skip_serializing_if = "Option::is_none")]
    lt: Option<StringOrNum>,
    #[serde(skip_serializing_if = "Option::is_none")]
    lte: Option<StringOrNum>,
    #[serde(skip_serializing_if = "Option::is_none")]
    format: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    relation: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    timezone: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    boost: Option<Num>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum QueryFieldOrString {
    Object(QueryField),
    String(String),
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct QueryField {
    query: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    fuzziness: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    slop: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    boost: Option<Num>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct BoolQuery {
    #[serde(skip_serializing_if = "Option::is_none")]
    must: Option<Box<ApiQueryOrArray>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    filter: Option<Box<ApiQueryOrArray>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    should: Option<Box<ApiQueryOrArray>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    must_not: Option<Box<ApiQueryOrArray>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    minimum_should_match: Option<StringOrNum>,
    #[serde(skip_serializing_if = "Option::is_none")]
    boost: Option<Num>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct NestedQuery {
    path: String,
    query: Box<ApiQuery>,
    #[serde(skip_serializing_if = "Option::is_none")]
    score_mode: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    ignore_unmapped: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    inner_hits: Option<InnerHitsOneOrMore>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct BoostingQuery {
    positive: Box<ApiQuery>,
    negative: Box<ApiQuery>,
    negative_boost: Num,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct ConstantScoreQuery {
    filter: Box<ApiQueryOrArray>,
    #[serde(skip_serializing_if = "Option::is_none")]
    boost: Option<Num>,
}

// https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct HighlightField {
    #[serde(skip_serializing_if = "Option::is_none")]
    boundary_chars: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    boundary_max_scan: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    boundary_scanner: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    boundary_scanner_locale: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    encoder: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    force_source: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fragmenter: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fragment_offset: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fragment_size: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    highlight_query: Option<ApiQuery>,
    #[serde(skip_serializing_if = "Option::is_none")]
    matched_fields: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    no_match_size: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    number_of_fragments: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    order: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    phrase_limit: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pre_tags: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    post_tags: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    require_field_match: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    tags_schema: Option<String>,
    #[serde(rename = "type")]
    #[serde(skip_serializing_if = "Option::is_none")]
    highlight_type: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum SimpleFieldOrString {
    String(String),
    Object { field: String },
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct ApiCollapse {
    field: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    inner_hits: Option<InnerHitsOneOrMore>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum InnerHitsOneOrMore {
    Single(InnerHits),
    Multiple(Vec<InnerHits>),
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct InnerHits {
    #[serde(skip_serializing_if = "Option::is_none")]
    from: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    size: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    sort: Option<Vec<SortElement>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    collapse: Option<Box<ApiCollapse>>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct ApiAggregation {
    // bucket type aggregations
    #[serde(skip_serializing_if = "Option::is_none")]
    nested: Option<NestedAggregation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    filter: Option<ApiQuery>,
    #[serde(skip_serializing_if = "Option::is_none")]
    filters: Option<FiltersAggregation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    histogram: Option<SimpleAggregation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    terms: Option<TermsAggregation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    significant_terms: Option<SimpleAggregation>,

    // metrics type aggregations
    #[serde(skip_serializing_if = "Option::is_none")]
    avg: Option<SimpleAggregation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    min: Option<SimpleAggregation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    max: Option<SimpleAggregation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    sum: Option<SimpleAggregation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    value_count: Option<SimpleAggregation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    stats: Option<SimpleAggregation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    percentiles: Option<SimpleAggregation>,

    // nested aggregations
    #[serde(skip_serializing_if = "Option::is_none")]
    aggs: Option<HashMap<String, Box<ApiAggregation>>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    aggregations: Option<HashMap<String, Box<ApiAggregation>>>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct NestedAggregation {
    path: String,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct SimpleAggregation {
    field: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    interval: Option<StringOrNum>,
    #[serde(skip_serializing_if = "Option::is_none")]
    missing: Option<StringOrNum>,
    #[serde(skip_serializing_if = "Option::is_none")]
    keyed: Option<bool>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct DateHistogramAggregation {
    field: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    fixed_interval: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    calendar_interval: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    format: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    time_zone: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    offset: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    order: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    keyed: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    missing: Option<StringOrNum>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct FiltersAggregation {
    #[serde(skip_serializing_if = "Option::is_none")]
    other_bucket_key: Option<String>,
    filters: ApiQueryNamedOrArray,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct TermsAggregation {
    field: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    size: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    shard_size: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    min_doc_count: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    show_term_doc_count_error: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    order: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    include: Option<StringOrArray>,
    #[serde(skip_serializing_if = "Option::is_none")]
    exclude: Option<StringOrArray>,
    #[serde(skip_serializing_if = "Option::is_none")]
    execution_hint: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    missing: Option<StringOrNum>,
}
