use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse};
use deserr::actix_web::AwebJson;
use index_scheduler::IndexScheduler;
use meilisearch_types::deserr::DeserrJsonError;
use meilisearch_types::error::ResponseError;
use meilisearch_types::index_uid::IndexUid;
use meilisearch_types::milli::update::Setting;
use meilisearch_types::network::route;
use meilisearch_types::settings::{
    settings, ChatSettings, SecretPolicy, SettingEmbeddingSettings, Settings, Unchecked,
};
use meilisearch_types::tasks::KindWithContent;
use tracing::debug;

use super::settings_analytics::*;
use crate::analytics::Analytics;
use crate::extractors::authentication::policies::*;
use crate::extractors::authentication::GuardedData;
use crate::proxy::{proxy, task_network_and_check_leader_and_version, Body, OverrideEndpoint};
use crate::routes::{get_task_id, is_dry_run, SummarizedTaskView};
use crate::Opt;

/// This macro generates the routes for the settings.
///
/// It takes a list of settings and generates a module for each setting.
/// Each module contains the `get`, `update` and `delete` routes for the setting.
///
/// It also generates a `configure` function that configures the routes for the settings.
macro_rules! make_setting_routes {
    ($({route: $route:literal, update_verb: $update_verb:ident, value_type: $type:ty, err_type: $err_ty:ty, attr: $attr:ident, camelcase_attr: $camelcase_attr:literal, analytics: $analytics:ident},)*) => {
        const _: fn(&meilisearch_types::settings::Settings<meilisearch_types::settings::Unchecked>) = |s| {
            // This pattern match will fail at compile time if any field in Settings is not listed in the macro
            match *s {
                meilisearch_types::settings::Settings { $($attr: _,)* _kind: _ } => {}
            }
        };
        $(
            make_setting_route!($route, $update_verb, $type, $err_ty, $attr, $camelcase_attr, $analytics);
        )*

        #[routes::routes(
            routes(
                "" => [patch(update_all), get(get_all), delete(delete_all)],
                $( $route => [get($attr::get), $update_verb($attr::update), delete($attr::delete)]),*
            ),
            tag = "Settings",
            tags(
                (
                    name = "Settings",
                    description = "Configure search and index behavior. Update all settings at once via PATCH /indexes/{indexUid}/settings, or use a sub-route to get, update, or reset a single setting.",
                ),
            ),
        )]
        pub struct SettingsApi;

        pub const ALL_SETTINGS_NAMES: &[&str] = &[$(stringify!($attr)),*];
    };
}

#[macro_export]
macro_rules! make_setting_route {
    ($route:literal, $update_verb:ident, $type:ty, $err_type:ty, $attr:ident, $camelcase_attr:literal, $analytics:ident) => {
        pub mod $attr {
            use actix_web::web::Data;
            use actix_web::{web, HttpRequest, HttpResponse, Resource};
            use index_scheduler::IndexScheduler;
            use meilisearch_types::error::ResponseError;
            use meilisearch_types::index_uid::IndexUid;
            use meilisearch_types::milli::update::Setting;
            use meilisearch_types::settings::{settings, Settings};
            use tracing::debug;
            use $crate::analytics::Analytics;
            use $crate::extractors::authentication::policies::*;
            use $crate::extractors::authentication::GuardedData;
            use $crate::extractors::sequential_extractor::SeqHandler;
            use $crate::Opt;
            use $crate::routes::SummarizedTaskView;
            #[allow(unused_imports)]
            use super::*;

            #[routes::path(
                security(("Bearer" = ["settings.update", "settings.*", "*"])),
                operation_id = concat!("delete", $camelcase_attr),
                summary = concat!("Reset ", $camelcase_attr),
                description = concat!("Resets the `", $camelcase_attr, "` setting to its default value."),
                params(("index_uid" = String, example = "movies", description = "Unique identifier of the index.", nullable = false)),
                responses(
                    (status = 202, description = "Task successfully enqueued.", body = SummarizedTaskView, content_type = "application/json", example = json!(
                        {
                            "taskUid": 147,
                            "indexUid": "movies",
                            "status": "enqueued",
                            "type": "settingsUpdate",
                            "enqueuedAt": "2024-08-08T17:05:55.791772Z"
                        }
                    )),
                    (status = 401, description = "The authorization header is missing.", body = ResponseError, content_type = "application/json", example = json!(
                        {
                            "message": "The Authorization header is missing. It must use the bearer authorization method.",
                            "code": "missing_authorization_header",
                            "type": "auth",
                            "link": "https://docs.meilisearch.com/errors#missing_authorization_header"
                        }
                    )),
                    (status = 404, description = "Index not found.", body = ResponseError, content_type = "application/json", example = json!(
                        {
                            "message": "Index `movies` not found.",
                            "code": "index_not_found",
                            "type": "invalid_request",
                            "link": "https://docs.meilisearch.com/errors#index_not_found"
                        }
                    )),
                )
            )]
            pub async fn delete(
                index_scheduler: GuardedData<
                    ActionPolicy<{ actions::SETTINGS_UPDATE }>,
                    Data<IndexScheduler>,
                >,
                index_uid: web::Path<String>,
                req: HttpRequest,
                opt: web::Data<Opt>,
            ) -> Result<HttpResponse, ResponseError> {
                let index_uid = IndexUid::try_from(index_uid.into_inner())?;

                let new_settings = Settings { $attr: Setting::Reset.into(), ..Default::default() };

                let task = register_new_settings(new_settings, true, index_scheduler, &req, index_uid, opt).await?;

                debug!(returns = ?task, "Delete settings");
                Ok(HttpResponse::Accepted().json(task))
            }


            #[routes::path(
                security(("Bearer" = ["settings.update", "settings.*", "*"])),
                operation_id = concat!(stringify!($update_verb), $camelcase_attr),
                summary = concat!("Update ", $camelcase_attr),
                description = concat!("Updates the `", $camelcase_attr, "` setting for the index. Send the new value in the request body; send null to reset to default."),
                params(("index_uid" = String, example = "movies", description = "Unique identifier of the index.", nullable = false)),
                request_body(content = $type),
                responses(
                    (status = 202, description = "Task successfully enqueued.", body = SummarizedTaskView, content_type = "application/json", example = json!(
                        {
                            "taskUid": 147,
                            "indexUid": "movies",
                            "status": "enqueued",
                            "type": "settingsUpdate",
                            "enqueuedAt": "2024-08-08T17:05:55.791772Z"
                        }
                    )),
                    (status = 401, description = "The authorization header is missing.", body = ResponseError, content_type = "application/json", example = json!(
                        {
                            "message": "The Authorization header is missing. It must use the bearer authorization method.",
                            "code": "missing_authorization_header",
                            "type": "auth",
                            "link": "https://docs.meilisearch.com/errors#missing_authorization_header"
                        }
                    )),
                    (status = 404, description = "Index not found.", body = ResponseError, content_type = "application/json", example = json!(
                        {
                            "message": "Index `movies` not found.",
                            "code": "index_not_found",
                            "type": "invalid_request",
                            "link": "https://docs.meilisearch.com/errors#index_not_found"
                        }
                    )),
                )
            )]
            pub async fn update(
                index_scheduler: GuardedData<
                    ActionPolicy<{ actions::SETTINGS_UPDATE }>,
                    Data<IndexScheduler>,
                >,
                index_uid: actix_web::web::Path<String>,
                body: deserr::actix_web::AwebJson<Option<$type>, $err_type>,
                req: HttpRequest,
                opt: web::Data<Opt>,
                analytics: web::Data<Analytics>,
            ) -> std::result::Result<HttpResponse, ResponseError> {
                let index_uid = IndexUid::try_from(index_uid.into_inner())?;

                let body = body.into_inner();
                debug!(parameters = ?body, "Update settings");

                #[allow(clippy::redundant_closure_call)]
                analytics.publish(
                    $crate::routes::indexes::settings_analytics::$analytics::new(body.as_ref()).into_settings(),
                    &req,
                );

                let new_settings = Settings {
                    $attr: match body {
                        Some(inner_body) => Setting::Set(inner_body).into(),
                        None => Setting::Reset.into(),
                    },
                    ..Default::default()
                };

                let task = register_new_settings(new_settings, false, index_scheduler, &req, index_uid, opt).await?;

                debug!(returns = ?task, "Update settings");
                Ok(HttpResponse::Accepted().json(task))
            }


            #[routes::path(
                summary = concat!("Get ", $camelcase_attr),
                description = concat!("Returns the current value of the `", $camelcase_attr, "` setting for the index."),
                security(("Bearer" = ["settings.get", "settings.*", "*"])),
                operation_id = concat!("get", $camelcase_attr),
                params(("index_uid" = String, example = "movies", description = "Unique identifier of the index.", nullable = false)),
                responses(
                    (status = 200, description = concat!("Returns the current value of the `", $camelcase_attr, "` setting."), body = $type, content_type = "application/json", example = json!(
                        <$type>::default()
                    )),
                    (status = 401, description = "The authorization header is missing.", body = ResponseError, content_type = "application/json", example = json!(
                        {
                            "message": "The Authorization header is missing. It must use the bearer authorization method.",
                            "code": "missing_authorization_header",
                            "type": "auth",
                            "link": "https://docs.meilisearch.com/errors#missing_authorization_header"
                        }
                    )),
                    (status = 404, description = "Index not found.", body = ResponseError, content_type = "application/json", example = json!(
                        {
                            "message": "Index `movies` not found.",
                            "code": "index_not_found",
                            "type": "invalid_request",
                            "link": "https://docs.meilisearch.com/errors#index_not_found"
                        }
                    )),
                )
            )]
            pub async fn get(
                index_scheduler: GuardedData<
                    ActionPolicy<{ actions::SETTINGS_GET }>,
                    Data<IndexScheduler>,
                >,
                index_uid: actix_web::web::Path<String>,
            ) -> std::result::Result<HttpResponse, ResponseError> {
                let index_uid = IndexUid::try_from(index_uid.into_inner())?;

                let index = index_scheduler.index(&index_uid)?;
                let rtxn = index.read_txn()?;
                let settings = settings(&index, &rtxn, meilisearch_types::settings::SecretPolicy::HideSecrets)?;

                debug!(returns = ?settings, "Update settings");

                Ok(HttpResponse::Ok().json(settings.$attr))
            }

            pub fn resources() -> Resource {
                Resource::new($route)
                    .route(web::get().to(SeqHandler(get)))
                    .route(web::$update_verb().to(SeqHandler(update)))
                    .route(web::delete().to(SeqHandler(delete)))
            }
        }
    };
}

make_setting_routes!(
    {
        route: "/filterable-attributes",
        update_verb: put,
        value_type: Vec<meilisearch_types::milli::FilterableAttributesRule>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsFilterableAttributes,
        >,
        attr: filterable_attributes,
        camelcase_attr: "filterableAttributes",
        analytics: FilterableAttributesAnalytics
    },
    {
        route: "/sortable-attributes",
        update_verb: put,
        value_type: std::collections::BTreeSet<String>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsSortableAttributes,
        >,
        attr: sortable_attributes,
        camelcase_attr: "sortableAttributes",
        analytics: SortableAttributesAnalytics
    },
    {
        route: "/displayed-attributes",
        update_verb: put,
        value_type: Vec<String>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsDisplayedAttributes,
        >,
        attr: displayed_attributes,
        camelcase_attr: "displayedAttributes",
        analytics: DisplayedAttributesAnalytics
    },
    {
        route: "/typo-tolerance",
        update_verb: patch,
        value_type: meilisearch_types::settings::TypoSettings,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsTypoTolerance,
        >,
        attr: typo_tolerance,
        camelcase_attr: "typoTolerance",
        analytics: TypoToleranceAnalytics
    },
    {
        route: "/searchable-attributes",
        update_verb: put,
        value_type: Vec<String>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsSearchableAttributes,
        >,
        attr: searchable_attributes,
        camelcase_attr: "searchableAttributes",
        analytics: SearchableAttributesAnalytics
    },
    {
        route: "/stop-words",
        update_verb: put,
        value_type: std::collections::BTreeSet<String>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsStopWords,
        >,
        attr: stop_words,
        camelcase_attr: "stopWords",
        analytics: StopWordsAnalytics
    },
    {
        route: "/non-separator-tokens",
        update_verb: put,
        value_type: std::collections::BTreeSet<String>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsNonSeparatorTokens,
        >,
        attr: non_separator_tokens,
        camelcase_attr: "nonSeparatorTokens",
        analytics: NonSeparatorTokensAnalytics
    },
    {
        route: "/separator-tokens",
        update_verb: put,
        value_type: std::collections::BTreeSet<String>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsSeparatorTokens,
        >,
        attr: separator_tokens,
        camelcase_attr: "separatorTokens",
        analytics: SeparatorTokensAnalytics
    },
    {
        route: "/dictionary",
        update_verb: put,
        value_type: std::collections::BTreeSet<String>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsDictionary,
        >,
        attr: dictionary,
        camelcase_attr: "dictionary",
        analytics: DictionaryAnalytics
    },
    {
        route: "/synonyms",
        update_verb: put,
        value_type: std::collections::BTreeMap<String, Vec<String>>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsSynonyms,
        >,
        attr: synonyms,
        camelcase_attr: "synonyms",
        analytics: SynonymsAnalytics
    },
    {
        route: "/distinct-attribute",
        update_verb: put,
        value_type: String,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsDistinctAttribute,
        >,
        attr: distinct_attribute,
        camelcase_attr: "distinctAttribute",
        analytics: DistinctAttributeAnalytics
    },
    {
        route: "/proximity-precision",
        update_verb: put,
        value_type: meilisearch_types::settings::ProximityPrecisionView,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsProximityPrecision,
        >,
        attr: proximity_precision,
        camelcase_attr: "proximityPrecision",
        analytics: ProximityPrecisionAnalytics
    },
    {
        route: "/localized-attributes",
        update_verb: put,
        value_type: Vec<meilisearch_types::locales::LocalizedAttributesRuleView>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsLocalizedAttributes,
        >,
        attr: localized_attributes,
        camelcase_attr: "localizedAttributes",
        analytics: LocalesAnalytics
    },
    {
        route: "/ranking-rules",
        update_verb: put,
        value_type: Vec<meilisearch_types::settings::RankingRuleView>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsRankingRules,
        >,
        attr: ranking_rules,
        camelcase_attr: "rankingRules",
        analytics: RankingRulesAnalytics
    },
    {
        route: "/faceting",
        update_verb: patch,
        value_type: meilisearch_types::settings::FacetingSettings,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsFaceting,
        >,
        attr: faceting,
        camelcase_attr: "faceting",
        analytics: FacetingAnalytics
    },
    {
        route: "/pagination",
        update_verb: patch,
        value_type: meilisearch_types::settings::PaginationSettings,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsPagination,
        >,
        attr: pagination,
        camelcase_attr: "pagination",
        analytics: PaginationAnalytics
    },
    {
        route: "/embedders",
        update_verb: patch,
        value_type: std::collections::BTreeMap<String, SettingEmbeddingSettings>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsEmbedders,
        >,
        attr: embedders,
        camelcase_attr: "embedders",
        analytics: EmbeddersAnalytics
    },
    {
        route: "/search-cutoff-ms",
        update_verb: put,
        value_type: u64,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsSearchCutoffMs,
        >,
        attr: search_cutoff_ms,
        camelcase_attr: "searchCutoffMs",
        analytics: SearchCutoffMsAnalytics
    },
    {
        route: "/facet-search",
        update_verb: put,
        value_type: bool,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsFacetSearch,
        >,
        attr: facet_search,
        camelcase_attr: "facetSearch",
        analytics: FacetSearchAnalytics
    },
    {
        route: "/prefix-search",
        update_verb: put,
        value_type: meilisearch_types::settings::PrefixSearchSettings,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsPrefixSearch,
        >,
        attr: prefix_search,
        camelcase_attr: "prefixSearch",
        analytics: PrefixSearchAnalytics
    },
    {
        route: "/chat",
        update_verb: patch,
        value_type: ChatSettings,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsIndexChat,
        >,
        attr: chat,
        camelcase_attr: "chat",
        analytics: ChatAnalytics
    },
    {
        route: "/foreign-keys",
        update_verb: put,
        value_type: Vec<meilisearch_types::milli::ForeignKey>,
        err_type: meilisearch_types::deserr::DeserrJsonError<
            meilisearch_types::error::deserr_codes::InvalidSettingsForeignKeys,
        >,
        attr: foreign_keys,
        camelcase_attr: "foreignKeys",
        analytics: ForeignKeysAnalytics
    },
);

#[routes::path(
    summary = "Update all settings",
    description = "Updates one or more settings for the index. Only the fields sent in the body are changed. Pass null for a setting to reset it to its default. If the index does not exist, it is created.\n\nSee also: [Configuring index settings on the Cloud](https://www.meilisearch.com/docs/learn/configuration/configuring_index_settings).",
    security(("Bearer" = ["settings.update", "settings.*", "*"])),
    params(("index_uid" = String, example = "movies", description = "Unique identifier of the index.", nullable = false)),
    request_body(content = Settings<Unchecked>),
    responses(
        (status = 202, description = "Task successfully enqueued.", body = SummarizedTaskView, content_type = "application/json", example = json!(
            {
                "taskUid": 147,
                "indexUid": "movies",
                "status": "enqueued",
                "type": "settingsUpdate",
                "enqueuedAt": "2024-08-08T17:05:55.791772Z"
            }
        )),
        (status = 401, description = "The authorization header is missing.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "The Authorization header is missing. It must use the bearer authorization method.",
                "code": "missing_authorization_header",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_authorization_header"
            }
        )),
        (status = 404, description = "Index not found.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "Index `movies` not found.",
                "code": "index_not_found",
                "type": "invalid_request",
                "link": "https://docs.meilisearch.com/errors#index_not_found"
            }
        )),
    )
)]
/// Update settings
///
/// Update the settings of an index. Updates are partial: only the fields you send are changed.
///
/// Passing `null` for a setting will reset it to its default.
///
/// If the index does not exist, it will be created.
///
/// See also: [Configuring index settings on the Cloud](https://www.meilisearch.com/docs/learn/configuration/configuring_index_settings).
pub async fn update_all(
    index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_UPDATE }>, Data<IndexScheduler>>,
    index_uid: web::Path<String>,
    body: AwebJson<Settings<Unchecked>, DeserrJsonError>,
    req: HttpRequest,
    opt: Data<Opt>,
    analytics: Data<Analytics>,
) -> Result<HttpResponse, ResponseError> {
    let index_uid = IndexUid::try_from(index_uid.into_inner())?;

    let new_settings: Settings<Unchecked> = body.into_inner();
    debug!(parameters = ?new_settings, "Update all settings");

    analytics.publish(
        SettingsAnalytics {
            ranking_rules: RankingRulesAnalytics::new(new_settings.ranking_rules.as_ref().set()),
            searchable_attributes: SearchableAttributesAnalytics::new(
                new_settings.searchable_attributes.as_ref().set(),
            ),
            displayed_attributes: DisplayedAttributesAnalytics::new(
                new_settings.displayed_attributes.as_ref().set(),
            ),
            sortable_attributes: SortableAttributesAnalytics::new(
                new_settings.sortable_attributes.as_ref().set(),
            ),
            filterable_attributes: FilterableAttributesAnalytics::new(
                new_settings.filterable_attributes.as_ref().set(),
            ),
            foreign_keys: ForeignKeysAnalytics::new(new_settings.foreign_keys.as_ref().set()),
            distinct_attribute: DistinctAttributeAnalytics::new(
                new_settings.distinct_attribute.as_ref().set(),
            ),
            proximity_precision: ProximityPrecisionAnalytics::new(
                new_settings.proximity_precision.as_ref().set(),
            ),
            typo_tolerance: TypoToleranceAnalytics::new(new_settings.typo_tolerance.as_ref().set()),
            faceting: FacetingAnalytics::new(new_settings.faceting.as_ref().set()),
            pagination: PaginationAnalytics::new(new_settings.pagination.as_ref().set()),
            stop_words: StopWordsAnalytics::new(new_settings.stop_words.as_ref().set()),
            synonyms: SynonymsAnalytics::new(new_settings.synonyms.as_ref().set()),
            embedders: EmbeddersAnalytics::new(new_settings.embedders.as_ref().set()),
            search_cutoff_ms: SearchCutoffMsAnalytics::new(
                new_settings.search_cutoff_ms.as_ref().set(),
            ),
            locales: LocalesAnalytics::new(new_settings.localized_attributes.as_ref().set()),
            dictionary: DictionaryAnalytics::new(new_settings.dictionary.as_ref().set()),
            separator_tokens: SeparatorTokensAnalytics::new(
                new_settings.separator_tokens.as_ref().set(),
            ),
            non_separator_tokens: NonSeparatorTokensAnalytics::new(
                new_settings.non_separator_tokens.as_ref().set(),
            ),
            facet_search: FacetSearchAnalytics::new(new_settings.facet_search.as_ref().set()),
            prefix_search: PrefixSearchAnalytics::new(new_settings.prefix_search.as_ref().set()),
            chat: ChatAnalytics::new(new_settings.chat.as_ref().set()),
        },
        &req,
    );

    let task =
        register_new_settings(new_settings, false, index_scheduler, &req, index_uid, opt).await?;

    debug!(returns = ?task, "Update all settings");
    Ok(HttpResponse::Accepted().json(task))
}

async fn register_new_settings(
    new_settings: Settings<Unchecked>,
    is_deletion: bool,
    index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_UPDATE }>, Data<IndexScheduler>>,
    req: &HttpRequest,
    index_uid: IndexUid,
    opt: Data<Opt>,
) -> Result<SummarizedTaskView, ResponseError> {
    let network = index_scheduler.network();
    let task_network = task_network_and_check_leader_and_version(req, &network)?;

    // validate settings unless this is a duplicated task
    let new_settings = if task_network.is_none() {
        validate_settings(new_settings, &index_scheduler)?
    } else {
        new_settings
    };

    let allow_index_creation = index_scheduler.filters().allow_index_creation(&index_uid);
    let settings_path = route::settings_path(&index_uid).unwrap();

    let index_uid = IndexUid::try_from(index_uid.into_inner())?.into_inner();
    let task = KindWithContent::SettingsUpdate {
        index_uid: index_uid.clone(),
        new_settings: Box::new(new_settings.clone()),
        is_deletion,
        allow_index_creation,
    };
    let uid = get_task_id(req, &opt)?;
    let dry_run = is_dry_run(req, &opt)?;

    let scheduler = index_scheduler.clone();
    let mut task = tokio::task::spawn_blocking(move || {
        scheduler.register_with_custom_metadata(task, uid, None, dry_run, task_network)
    })
    .await??;

    if let Some(task_network) = task.network.take() {
        proxy(
            &index_scheduler,
            Some(&index_uid),
            // since we are sending a full settings object, we need to override the path and method to target the main settings route
            &OverrideEndpoint::new(
                req,
                Some(settings_path),
                Some(http_client::reqwest::Method::PATCH),
                None,
            ),
            task_network,
            network,
            Body::inline(new_settings),
            &task,
        )
        .await?;
    }

    Ok(task.into())
}

#[routes::path(
    summary = "List all settings",
    description = "Returns all settings of the index. Each setting is returned with its current value or the default if not set.",
    security(("Bearer" = ["settings.get", "settings.*", "*"])),
    params(("index_uid" = String, example = "movies", description = "Unique identifier of the index.", nullable = false)),
    responses(
        (status = 200, description = "Returns all settings with their current or default values. Same structure as the PATCH request body.", body = Settings<Unchecked>, content_type = "application/json", example = json!({
            "displayedAttributes": ["id", "title", "description", "url"],
            "searchableAttributes": ["title", "description"],
            "filterableAttributes": ["release_date", "genre"],
            "sortableAttributes": ["release_date"],
            "rankingRules": ["words", "typo", "proximity", "attributeRank", "sort", "wordPosition", "exactness"],
            "stopWords": ["the", "a"],
            "nonSeparatorTokens": ["@", "#"],
            "separatorTokens": ["|"],
            "dictionary": ["J. R. R."],
            "synonyms": { "phone": ["iPhone"] },
            "distinctAttribute": null,
            "typoTolerance": {
                "enabled": true,
                "minWordSizeForTypos": { "oneTypo": 5, "twoTypos": 9 },
                "disableOnWords": [],
                "disableOnAttributes": ["title"],
                "disableOnNumbers": false
            },
            "faceting": { "maxValuesPerFacet": 100, "sortFacetValuesBy": { "genre": "count" } },
            "pagination": { "maxTotalHits": 1000 },
            "proximityPrecision": "byWord",
            "embedders": { "default": { "source": "openAi", "model": "text-embedding-3-small", "documentTemplate": "{{doc.title}}: {{doc.overview}}" } },
            "searchCutoffMs": null,
            "localizedAttributes": [{ "locales": ["jpn"], "attributePatterns": ["*_ja"] }],
            "facetSearch": true,
            "prefixSearch": "indexingTime",
            "chat": { "description": "A comprehensive movie database", "documentTemplateMaxBytes": 400, "searchParameters": { "limit": 20 } }
        })),
        (status = 401, description = "The authorization header is missing.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "The Authorization header is missing. It must use the bearer authorization method.",
                "code": "missing_authorization_header",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_authorization_header"
            }
        )),
        (status = 404, description = "Index not found.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "Index `movies` not found.",
                "code": "index_not_found",
                "type": "invalid_request",
                "link": "https://docs.meilisearch.com/errors#index_not_found"
            }
        )),
    )
)]
/// List settings
///
/// Retrieve all settings of an index in a single request. Returns every configurable option and its current or default value.
pub async fn get_all(
    index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_GET }>, Data<IndexScheduler>>,
    index_uid: web::Path<String>,
) -> Result<HttpResponse, ResponseError> {
    let index_uid = IndexUid::try_from(index_uid.into_inner())?;

    let index = index_scheduler.index(&index_uid)?;
    let rtxn = index.read_txn()?;
    let mut new_settings = settings(&index, &rtxn, SecretPolicy::HideSecrets)?;

    let features = index_scheduler.features();

    if features.check_chat_completions("showing index `chat` settings").is_err() {
        new_settings.chat = Setting::NotSet;
    }

    if features.check_foreign_keys_setting("showing index `foreignKeys` settings").is_err() {
        new_settings.foreign_keys = Setting::NotSet;
    }

    debug!(returns = ?new_settings, "Get all settings");
    Ok(HttpResponse::Ok().json(new_settings))
}

#[routes::path(
    summary = "Reset all settings",
    description = "Resets all settings of the index to their default values.",
    security(("Bearer" = ["settings.update", "settings.*", "*"])),
    params(("index_uid" = String, example = "movies", description = "Unique identifier of the index.", nullable = false)),
    responses(
        (status = 202, description = "Task successfully enqueued.", body = SummarizedTaskView, content_type = "application/json", example = json!(
            {
                "taskUid": 147,
                "indexUid": "movies",
                "status": "enqueued",
                "type": "settingsUpdate",
                "enqueuedAt": "2024-08-08T17:05:55.791772Z"
            }
        )),
        (status = 401, description = "The authorization header is missing.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "The Authorization header is missing. It must use the bearer authorization method.",
                "code": "missing_authorization_header",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_authorization_header"
            }
        )),
        (status = 404, description = "Index not found.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "Index `movies` not found.",
                "code": "index_not_found",
                "type": "invalid_request",
                "link": "https://docs.meilisearch.com/errors#index_not_found"
            }
        )),
    )
)]
/// Reset settings
///
/// Reset all settings of an index to their default values.
pub async fn delete_all(
    index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_UPDATE }>, Data<IndexScheduler>>,
    index_uid: web::Path<String>,
    req: HttpRequest,
    opt: web::Data<Opt>,
) -> Result<HttpResponse, ResponseError> {
    let index_uid = IndexUid::try_from(index_uid.into_inner())?;

    let new_settings = Settings::cleared().into_unchecked();

    let task =
        register_new_settings(new_settings, true, index_scheduler, &req, index_uid, opt).await?;

    debug!(returns = ?task, "Delete all settings");
    Ok(HttpResponse::Accepted().json(task))
}

fn validate_settings(
    settings: Settings<Unchecked>,
    index_scheduler: &IndexScheduler,
) -> Result<Settings<Unchecked>, ResponseError> {
    use meilisearch_types::milli::update::Setting;
    use meilisearch_types::milli::vector::settings::EmbedderSource;

    let features = index_scheduler.features();
    if let Setting::Set(embedders) = &settings.embedders {
        for SettingEmbeddingSettings { inner: embedder } in embedders.values() {
            let Setting::Set(embedder) = embedder else {
                continue;
            };
            if matches!(embedder.source, Setting::Set(EmbedderSource::Composite)) {
                features.check_composite_embedders("using `\"composite\"` as source")?;
            }

            if matches!(embedder.search_embedder, Setting::Set(_)) {
                features.check_composite_embedders("setting `searchEmbedder`")?;
            }

            if matches!(embedder.indexing_embedder, Setting::Set(_)) {
                features.check_composite_embedders("setting `indexingEmbedder`")?;
            }

            if matches!(embedder.indexing_fragments, Setting::Set(_)) {
                features.check_multimodal("setting `indexingFragments`")?;
            }

            if matches!(embedder.search_fragments, Setting::Set(_)) {
                features.check_multimodal("setting `searchFragments`")?;
            }
        }
    }

    if let Setting::Set(_chat) = &settings.chat {
        features.check_chat_completions("setting `chat` in the index settings")?;
    }

    if let Setting::Set(_) = &settings.foreign_keys {
        features.check_foreign_keys_setting("setting `foreignKeys` in the index settings")?;
    }

    Ok(settings.validate()?)
}
