use std::convert::Infallible;
use std::hash::Hash;
use std::str::FromStr;

use deserr::{DeserializeError, MergeWithError, ValuePointerRef};
use enum_iterator::Sequence;
use milli::update::Setting;
use serde::{Deserialize, Serialize};
use time::format_description::well_known::Rfc3339;
use time::macros::{format_description, time};
use time::{Date, OffsetDateTime, PrimitiveDateTime};
use uuid::Uuid;

use crate::deserr::{immutable_field_error, DeserrError, DeserrJsonError};
use crate::error::deserr_codes::*;
use crate::error::{Code, ErrorCode, ParseOffsetDateTimeError};
use crate::index_uid_pattern::{IndexUidPattern, IndexUidPatternFormatError};

pub type KeyId = Uuid;

impl<C: Default + ErrorCode> MergeWithError<IndexUidPatternFormatError> for DeserrJsonError<C> {
    fn merge(
        _self_: Option<Self>,
        other: IndexUidPatternFormatError,
        merge_location: deserr::ValuePointerRef,
    ) -> std::ops::ControlFlow<Self, Self> {
        DeserrError::error::<Infallible>(
            None,
            deserr::ErrorKind::Unexpected { msg: other.to_string() },
            merge_location,
        )
    }
}

#[routes::request]
#[derive(Debug)]
pub struct CreateApiKey {
    /// A description for the key. `null` if empty.
    #[request(default, example = json!(null), error = DeserrJsonError<InvalidApiKeyDescription>)]
    pub description: Option<String>,
    /// A human-readable name for the key. `null` if empty.
    #[request(default, example = "Indexing Products API key", error = DeserrJsonError<InvalidApiKeyName>)]
    pub name: Option<String>,
    /// A uuid v4 to identify the API Key. If not specified, it's generated by Meilisearch.
    #[request(
        default = Uuid::new_v4(),
        schema_type = Option<String>,
        example = "01b4bc42-eb33-4041-b481-254d00cce834",
        error = DeserrJsonError<InvalidApiKeyUid>,
        try_from(&String) = Uuid::from_str -> uuid::Error
    )]
    pub uid: KeyId,
    /// A list of actions permitted for the key. `["*"]` for all actions. The
    /// `*` character can be used as a wildcard when located at the last
    /// position. e.g. `documents.*` to authorize access on all documents
    /// endpoints.
    ///
    /// Valid actions include: `search`, `documents.add`,
    /// `documents.get`, `documents.delete`, `indexes.create`, `indexes.get`,
    /// `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.get`,
    /// `tasks.cancel`, `tasks.delete`, `settings.get`, `settings.update`,
    /// `stats.get`, `dumps.create`, `snapshots.create`, `version`, `keys.get`,
    /// `keys.create`, `keys.update`, `keys.delete`. Use `*` to grant all actions.
    #[request(
        required,
        example = json!(["documents.add"]),
        error = DeserrJsonError<InvalidApiKeyActions>,
        missing_field_error = DeserrJsonError::missing_api_key_actions
    )]
    pub actions: Vec<Action>,
    /// A list of accessible indexes permitted for the key. `["*"]` for all
    /// indexes. The `*` character can be used as a wildcard when located at
    /// the last position. e.g. `products_*` to allow access to all indexes
    /// whose names start with `products_`.
    #[request(
        required,
        error = DeserrJsonError<InvalidApiKeyIndexes>,
        missing_field_error = DeserrJsonError::missing_api_key_indexes,
        schema_type = Vec<String>,
        example = json!(["products"])
    )]
    pub indexes: Vec<IndexUidPattern>,
    /// Represent the expiration date and time as RFC 3339 format. `null`
    /// equals to no expiration time. Set to `null` to create a key that never
    /// expires. Once a key is past its expiry date, using it returns an error.
    #[request(required,
        error = DeserrJsonError<InvalidApiKeyExpiresAt>,
        try_from(Option<String>) = parse_expiration_date -> ParseOffsetDateTimeError,
        missing_field_error = DeserrJsonError::missing_api_key_expires_at
    )]
    pub expires_at: Option<OffsetDateTime>,
}

impl CreateApiKey {
    pub fn to_key(self) -> Key {
        let CreateApiKey { description, name, uid, actions, indexes, expires_at } = self;
        let now = OffsetDateTime::now_utc();
        Key {
            description,
            name,
            uid,
            actions,
            indexes,
            expires_at,
            created_at: now,
            updated_at: now,
        }
    }
}

fn deny_immutable_fields_api_key(
    field: &str,
    accepted: &[&str],
    location: ValuePointerRef,
) -> DeserrJsonError {
    match field {
        "uid" => immutable_field_error(field, accepted, Code::ImmutableApiKeyUid),
        "actions" => immutable_field_error(field, accepted, Code::ImmutableApiKeyActions),
        "indexes" => immutable_field_error(field, accepted, Code::ImmutableApiKeyIndexes),
        "expiresAt" => immutable_field_error(field, accepted, Code::ImmutableApiKeyExpiresAt),
        "createdAt" => immutable_field_error(field, accepted, Code::ImmutableApiKeyCreatedAt),
        "updatedAt" => immutable_field_error(field, accepted, Code::ImmutableApiKeyUpdatedAt),
        _ => deserr::take_cf_content(DeserrJsonError::<BadRequest>::error::<Infallible>(
            None,
            deserr::ErrorKind::UnknownKey { key: field, accepted },
            location,
        )),
    }
}

/// Request body for updating an existing API key. Only the name and
/// description can be modified - other properties like actions, indexes,
/// and expiration are immutable after creation.
#[routes::request(deny_unknown_fields = deny_immutable_fields_api_key)]
#[derive(Debug)]
pub struct PatchApiKey {
    /// A new description for the API key. Pass `null` to remove the existing
    /// description. Useful for documenting the purpose or usage of the key.
    #[request(default, error = DeserrJsonError<InvalidApiKeyDescription>, schema_type = Option<String>, example = "This key is used to update documents in the products index")]
    pub description: Setting<String>,
    /// A new human-readable name for the API key. Pass `null` to remove the
    /// existing name. Use this to identify keys by purpose, such as
    /// "Production Search Key" or "CI/CD Indexing Key".
    #[request(default, error = DeserrJsonError<InvalidApiKeyName>, schema_type = Option<String>, example = "Indexing Products API key")]
    pub name: Setting<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct Key {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    pub uid: KeyId,
    pub actions: Vec<Action>,
    pub indexes: Vec<IndexUidPattern>,
    #[serde(with = "time::serde::rfc3339::option")]
    pub expires_at: Option<OffsetDateTime>,
    #[serde(with = "time::serde::rfc3339")]
    pub created_at: OffsetDateTime,
    #[serde(with = "time::serde::rfc3339")]
    pub updated_at: OffsetDateTime,
}

impl Key {
    pub fn default_admin() -> Self {
        let now = OffsetDateTime::now_utc();
        let uid = Uuid::new_v4();
        Self {
            name: Some("Default Admin API Key".to_string()),
            description: Some("Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend".to_string()),
            uid,
            actions: vec![Action::All],
            indexes: vec![IndexUidPattern::all()],
            expires_at: None,
            created_at: now,
            updated_at: now,
        }
    }

    pub fn default_read_only_admin() -> Self {
        let now = OffsetDateTime::now_utc();
        let uid = Uuid::new_v4();
        Self {
            name: Some("Default Read-Only Admin API Key".to_string()),
            description: Some("Use it to read information across the whole database. Caution! Do not expose this key on a public frontend".to_string()),
            uid,
            actions: vec![Action::AllGet, Action::KeysGet],
            indexes: vec![IndexUidPattern::all()],
            expires_at: None,
            created_at: now,
            updated_at: now,
        }
    }

    pub fn default_search() -> Self {
        let now = OffsetDateTime::now_utc();
        let uid = Uuid::new_v4();
        Self {
            name: Some("Default Search API Key".to_string()),
            description: Some("Use it to search from the frontend".to_string()),
            uid,
            actions: vec![Action::Search],
            indexes: vec![IndexUidPattern::all()],
            expires_at: None,
            created_at: now,
            updated_at: now,
        }
    }

    pub fn default_chat() -> Self {
        let now = OffsetDateTime::now_utc();
        let uid = Uuid::new_v4();
        Self {
            name: Some("Default Chat API Key".to_string()),
            description: Some("Use it to chat and search from the frontend".to_string()),
            uid,
            actions: vec![Action::ChatCompletions, Action::Search],
            indexes: vec![IndexUidPattern::all()],
            expires_at: None,
            created_at: now,
            updated_at: now,
        }
    }
}

fn parse_expiration_date(
    string: Option<String>,
) -> std::result::Result<Option<OffsetDateTime>, ParseOffsetDateTimeError> {
    let Some(string) = string else { return Ok(None) };
    let datetime = if let Ok(datetime) = OffsetDateTime::parse(&string, &Rfc3339) {
        datetime
    } else if let Ok(primitive_datetime) = PrimitiveDateTime::parse(
        &string,
        format_description!(
            "[year repr:full base:calendar]-[month repr:numerical]-[day]T[hour]:[minute]:[second]"
        ),
    ) {
        primitive_datetime.assume_utc()
    } else if let Ok(primitive_datetime) = PrimitiveDateTime::parse(
        &string,
        format_description!(
            "[year repr:full base:calendar]-[month repr:numerical]-[day] [hour]:[minute]:[second]"
        ),
    ) {
        primitive_datetime.assume_utc()
    } else if let Ok(date) = Date::parse(
        &string,
        format_description!("[year repr:full base:calendar]-[month repr:numerical]-[day]"),
    ) {
        PrimitiveDateTime::new(date, time!(00:00)).assume_utc()
    } else {
        return Err(ParseOffsetDateTimeError(string));
    };
    if datetime > OffsetDateTime::now_utc() {
        Ok(Some(datetime))
    } else {
        Err(ParseOffsetDateTimeError(string))
    }
}

#[routes::request(db, no_error)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Sequence)]
#[repr(u8)]
pub enum Action {
    #[request(rename = "*")]
    All = 0,
    #[request(rename = "search")]
    Search,
    #[request(rename = "documents.*")]
    DocumentsAll,
    #[request(rename = "documents.add")]
    DocumentsAdd,
    #[request(rename = "documents.get")]
    DocumentsGet,
    #[request(rename = "documents.delete")]
    DocumentsDelete,
    #[request(rename = "indexes.*")]
    IndexesAll,
    #[request(rename = "indexes.create")]
    IndexesAdd,
    #[request(rename = "indexes.get")]
    IndexesGet,
    #[request(rename = "indexes.update")]
    IndexesUpdate,
    #[request(rename = "indexes.delete")]
    IndexesDelete,
    #[request(rename = "indexes.swap")]
    IndexesSwap,
    #[request(rename = "tasks.*")]
    TasksAll,
    #[request(rename = "tasks.cancel")]
    TasksCancel,
    #[request(rename = "tasks.delete")]
    TasksDelete,
    #[request(rename = "tasks.get")]
    TasksGet,
    #[request(rename = "settings.*")]
    SettingsAll,
    #[request(rename = "settings.get")]
    SettingsGet,
    #[request(rename = "settings.update")]
    SettingsUpdate,
    #[request(rename = "stats.*")]
    StatsAll,
    #[request(rename = "stats.get")]
    StatsGet,
    #[request(rename = "metrics.*")]
    MetricsAll,
    #[request(rename = "metrics.get")]
    MetricsGet,
    #[request(rename = "dumps.*")]
    DumpsAll,
    #[request(rename = "dumps.create")]
    DumpsCreate,
    #[request(rename = "snapshots.*")]
    SnapshotsAll,
    #[request(rename = "snapshots.create")]
    SnapshotsCreate,
    #[request(rename = "version")]
    Version,
    #[request(rename = "keys.create")]
    KeysAdd,
    #[request(rename = "keys.get")]
    KeysGet,
    #[request(rename = "keys.update")]
    KeysUpdate,
    #[request(rename = "keys.delete")]
    KeysDelete,
    #[request(rename = "experimental.get")]
    ExperimentalFeaturesGet,
    #[request(rename = "experimental.update")]
    ExperimentalFeaturesUpdate,
    #[request(rename = "export")]
    Export,
    #[request(rename = "network.get")]
    NetworkGet,
    #[request(rename = "network.update")]
    NetworkUpdate,
    #[request(rename = "chatCompletions")]
    ChatCompletions,
    #[request(rename = "chats.*")]
    ChatsAll,
    #[request(rename = "chats.get")]
    ChatsGet,
    #[request(rename = "chats.delete")]
    ChatsDelete,
    #[request(rename = "chatsSettings.*")]
    ChatsSettingsAll,
    #[request(rename = "chatsSettings.get")]
    ChatsSettingsGet,
    #[request(rename = "chatsSettings.update")]
    ChatsSettingsUpdate,
    #[request(rename = "*.get")]
    AllGet,
    #[request(rename = "webhooks.get")]
    WebhooksGet,
    #[request(rename = "webhooks.update")]
    WebhooksUpdate,
    #[request(rename = "webhooks.delete")]
    WebhooksDelete,
    #[request(rename = "webhooks.create")]
    WebhooksCreate,
    #[request(rename = "webhooks.*")]
    WebhooksAll,
    #[request(rename = "indexes.compact")]
    IndexesCompact,
    #[request(rename = "fields.post")]
    FieldsPost,
    #[request(rename = "tasks.compact")]
    TasksCompact,
    #[request(rename = "dynamicSearchRules.get")]
    DynamicSearchRulesGet,
    #[request(rename = "dynamicSearchRules.create")]
    DynamicSearchRulesCreate,
    #[request(rename = "dynamicSearchRules.update")]
    DynamicSearchRulesUpdate,
    #[request(rename = "dynamicSearchRules.delete")]
    DynamicSearchRulesDelete,
    #[request(rename = "dynamicSearchRules.*")]
    DynamicSearchRulesAll,
}

impl Action {
    pub const fn from_repr(repr: u8) -> Option<Self> {
        use actions::*;
        match repr {
            ALL => Some(Self::All),
            SEARCH => Some(Self::Search),
            DOCUMENTS_ALL => Some(Self::DocumentsAll),
            DOCUMENTS_ADD => Some(Self::DocumentsAdd),
            DOCUMENTS_GET => Some(Self::DocumentsGet),
            DOCUMENTS_DELETE => Some(Self::DocumentsDelete),
            INDEXES_ALL => Some(Self::IndexesAll),
            INDEXES_CREATE => Some(Self::IndexesAdd),
            INDEXES_GET => Some(Self::IndexesGet),
            INDEXES_UPDATE => Some(Self::IndexesUpdate),
            INDEXES_DELETE => Some(Self::IndexesDelete),
            INDEXES_SWAP => Some(Self::IndexesSwap),
            INDEXES_COMPACT => Some(Self::IndexesCompact),
            TASKS_ALL => Some(Self::TasksAll),
            TASKS_CANCEL => Some(Self::TasksCancel),
            TASKS_DELETE => Some(Self::TasksDelete),
            TASKS_GET => Some(Self::TasksGet),
            TASKS_COMPACT => Some(Self::TasksCompact),
            SETTINGS_ALL => Some(Self::SettingsAll),
            SETTINGS_GET => Some(Self::SettingsGet),
            SETTINGS_UPDATE => Some(Self::SettingsUpdate),
            CHAT_COMPLETIONS => Some(Self::ChatCompletions),
            CHATS_ALL => Some(Self::ChatsAll),
            CHATS_GET => Some(Self::ChatsGet),
            CHATS_DELETE => Some(Self::ChatsDelete),
            CHATS_SETTINGS_ALL => Some(Self::ChatsSettingsAll),
            CHATS_SETTINGS_GET => Some(Self::ChatsSettingsGet),
            CHATS_SETTINGS_UPDATE => Some(Self::ChatsSettingsUpdate),
            STATS_ALL => Some(Self::StatsAll),
            STATS_GET => Some(Self::StatsGet),
            METRICS_ALL => Some(Self::MetricsAll),
            METRICS_GET => Some(Self::MetricsGet),
            DUMPS_ALL => Some(Self::DumpsAll),
            DUMPS_CREATE => Some(Self::DumpsCreate),
            SNAPSHOTS_ALL => Some(Self::SnapshotsAll),
            SNAPSHOTS_CREATE => Some(Self::SnapshotsCreate),
            VERSION => Some(Self::Version),
            KEYS_CREATE => Some(Self::KeysAdd),
            KEYS_GET => Some(Self::KeysGet),
            KEYS_UPDATE => Some(Self::KeysUpdate),
            KEYS_DELETE => Some(Self::KeysDelete),
            EXPERIMENTAL_FEATURES_GET => Some(Self::ExperimentalFeaturesGet),
            EXPERIMENTAL_FEATURES_UPDATE => Some(Self::ExperimentalFeaturesUpdate),
            EXPORT => Some(Self::Export),
            NETWORK_GET => Some(Self::NetworkGet),
            NETWORK_UPDATE => Some(Self::NetworkUpdate),
            ALL_GET => Some(Self::AllGet),
            WEBHOOKS_GET => Some(Self::WebhooksGet),
            WEBHOOKS_UPDATE => Some(Self::WebhooksUpdate),
            WEBHOOKS_DELETE => Some(Self::WebhooksDelete),
            WEBHOOKS_CREATE => Some(Self::WebhooksCreate),
            WEBHOOKS_ALL => Some(Self::WebhooksAll),
            FIELDS_POST => Some(Self::FieldsPost),
            DYNAMIC_SEARCH_RULES_GET => Some(Self::DynamicSearchRulesGet),
            DYNAMIC_SEARCH_RULES_CREATE => Some(Self::DynamicSearchRulesCreate),
            DYNAMIC_SEARCH_RULES_UPDATE => Some(Self::DynamicSearchRulesUpdate),
            DYNAMIC_SEARCH_RULES_DELETE => Some(Self::DynamicSearchRulesDelete),
            DYNAMIC_SEARCH_RULES_ALL => Some(Self::DynamicSearchRulesAll),
            _otherwise => None,
        }
    }

    /// Whether the action should be included in [Action::AllRead].
    pub fn is_read(&self) -> bool {
        use Action::*;

        // It's using an exhaustive match to force the addition of new actions.
        match self {
            // Any action that expands to others must return false, as it wouldn't be able to expand recursively.
            All
            | AllGet
            | DocumentsAll
            | IndexesAll
            | ChatsAll
            | TasksAll
            | SettingsAll
            | StatsAll
            | MetricsAll
            | DumpsAll
            | SnapshotsAll
            | ChatsSettingsAll
            | WebhooksAll
            | DynamicSearchRulesAll => false,

            Search => true,
            DocumentsAdd => false,
            DocumentsGet => true,
            DocumentsDelete => false,
            Export => true,
            IndexesAdd => false,
            IndexesGet => true,
            IndexesUpdate => false,
            IndexesDelete => false,
            IndexesSwap => false,
            IndexesCompact => false,
            TasksCancel => false,
            TasksDelete => false,
            TasksGet => true,
            SettingsGet => true,
            SettingsUpdate => false,
            StatsGet => true,
            MetricsGet => true,
            DumpsCreate => false,
            SnapshotsCreate => false,
            Version => true,
            KeysAdd => false,
            KeysGet => false, // Disabled in order to prevent privilege escalation
            KeysUpdate => false,
            KeysDelete => false,
            ExperimentalFeaturesGet => true,
            ExperimentalFeaturesUpdate => false,
            NetworkGet => true,
            NetworkUpdate => false,
            ChatCompletions => false, // Disabled because it might trigger generation of new chats
            ChatsGet => true,
            ChatsDelete => false,
            ChatsSettingsGet => true,
            ChatsSettingsUpdate => false,
            WebhooksGet => true,
            WebhooksUpdate => false,
            WebhooksDelete => false,
            WebhooksCreate => false,
            FieldsPost => true,
            TasksCompact => false,
            DynamicSearchRulesGet => true,
            DynamicSearchRulesCreate => false,
            DynamicSearchRulesUpdate => false,
            DynamicSearchRulesDelete => false,
        }
    }

    pub fn index_scope(&self) -> IndexScope {
        match self {
            // let global wildcard expand to the relevant permissions
            Action::All => IndexScope::MultipleScope,
            Action::AllGet => IndexScope::MultipleScope,

            Action::DocumentsAll => IndexScope::MultipleScope,
            // used by render-template which is not index scoped
            Action::DocumentsGet => IndexScope::RouteHandlerChecksIndex,

            // remaining documents are index-scoped
            Action::DocumentsAdd | Action::DocumentsDelete => IndexScope::ControllerChecksIndex,
            // search is checked by route handler in the case of multi-search
            Action::Search => IndexScope::RouteHandlerChecksIndex,

            // indexes are supported global or index-scoped
            Action::IndexesAll => IndexScope::MultipleScope,
            Action::IndexesAdd => IndexScope::RouteHandlerChecksIndex,
            Action::IndexesGet => IndexScope::RouteHandlerChecksIndex, // used in list index
            Action::IndexesUpdate => IndexScope::ControllerChecksIndex,
            Action::IndexesDelete => IndexScope::ControllerChecksIndex,
            Action::IndexesSwap => IndexScope::RouteHandlerChecksIndex,

            // index compaction, fields, are scoped
            Action::IndexesCompact => IndexScope::ControllerChecksIndex,
            Action::FieldsPost => IndexScope::ControllerChecksIndex,

            // tasks are complicated due to compact
            Action::TasksAll => IndexScope::MultipleScope,
            Action::TasksCancel => IndexScope::RouteHandlerChecksIndex,
            Action::TasksDelete => IndexScope::RouteHandlerChecksIndex,
            Action::TasksGet => IndexScope::RouteHandlerChecksIndex,
            Action::TasksCompact => IndexScope::DenyIndexScope,

            // settings.get is handler-checked, as required by render-template
            Action::SettingsGet => IndexScope::RouteHandlerChecksIndex,
            Action::SettingsAll => IndexScope::MultipleScope,
            // remaining settings are index scoped
            Action::SettingsUpdate => IndexScope::ControllerChecksIndex,

            // stats are always supported global
            Action::StatsAll => IndexScope::RouteHandlerChecksIndex,
            Action::StatsGet => IndexScope::RouteHandlerChecksIndex,

            Action::ChatCompletions => IndexScope::RouteHandlerChecksIndex,

            // metrics, dumps, snapshots, version, keys, experimental features, export, network, chats, webhooks and DSR are global
            Action::MetricsAll
            | Action::MetricsGet
            | Action::DumpsAll
            | Action::DumpsCreate
            | Action::SnapshotsAll
            | Action::SnapshotsCreate
            | Action::Version
            | Action::KeysAdd
            | Action::KeysGet
            | Action::KeysUpdate
            | Action::KeysDelete
            | Action::ExperimentalFeaturesGet
            | Action::ExperimentalFeaturesUpdate
            | Action::Export
            | Action::NetworkGet
            | Action::NetworkUpdate
            | Action::ChatsAll
            | Action::ChatsGet
            | Action::ChatsDelete
            | Action::ChatsSettingsAll
            | Action::ChatsSettingsGet
            | Action::ChatsSettingsUpdate
            | Action::WebhooksGet
            | Action::WebhooksUpdate
            | Action::WebhooksDelete
            | Action::WebhooksCreate
            | Action::WebhooksAll
            | Action::DynamicSearchRulesGet
            | Action::DynamicSearchRulesCreate
            | Action::DynamicSearchRulesUpdate
            | Action::DynamicSearchRulesDelete
            | Action::DynamicSearchRulesAll => IndexScope::DenyIndexScope,
        }
    }

    pub fn deny_index_scope(&self) -> bool {
        matches!(self.index_scope(), IndexScope::DenyIndexScope)
    }

    pub fn route_handler_is_checking_index_scope(&self) -> bool {
        matches!(self.index_scope(), IndexScope::RouteHandlerChecksIndex)
    }

    pub const fn repr(&self) -> u8 {
        *self as u8
    }
}

pub enum IndexScope {
    /// The action is global and doesn't support index-scoped API keys
    ///
    /// - API key creation will be blocked for index-scoped keys
    /// - Usage will be blocked by auth controller for indexless routes on index-scoped keys
    DenyIndexScope,
    /// The action is global but **does** support index-scoped API keys
    ///
    /// - API key creation will be allowed for index-scoped keys
    /// - Usage will be allowed by auth controller for indexless routes on index-scoped keys (manually checked by the route)
    RouteHandlerChecksIndex,
    /// The action is always index-scoped
    ///
    /// - API key creation will be allowed for index-scoped keys
    /// - Usage will be blocked by auth controller for indexless routes on index-scoped keys (should be called on index-scoped routes)
    ControllerChecksIndex,
    /// The action expands to multiple actions with different scopes
    ///
    /// - API key creation will be allowed for index-scoped keys
    /// - Usage will be blocked by auth controller for indexless routes on index-scoped keys (should be called on single-scope actions)
    MultipleScope,
}

pub mod actions {
    use super::Action::*;

    pub(crate) const ALL: u8 = All.repr();
    pub const ALL_GET: u8 = AllGet.repr();
    pub const SEARCH: u8 = Search.repr();
    pub const DOCUMENTS_ALL: u8 = DocumentsAll.repr();
    pub const DOCUMENTS_ADD: u8 = DocumentsAdd.repr();
    pub const DOCUMENTS_GET: u8 = DocumentsGet.repr();
    pub const DOCUMENTS_DELETE: u8 = DocumentsDelete.repr();
    pub const INDEXES_ALL: u8 = IndexesAll.repr();
    pub const INDEXES_CREATE: u8 = IndexesAdd.repr();
    pub const INDEXES_GET: u8 = IndexesGet.repr();
    pub const INDEXES_UPDATE: u8 = IndexesUpdate.repr();
    pub const INDEXES_DELETE: u8 = IndexesDelete.repr();
    pub const INDEXES_SWAP: u8 = IndexesSwap.repr();
    pub const INDEXES_COMPACT: u8 = IndexesCompact.repr();
    pub const TASKS_ALL: u8 = TasksAll.repr();
    pub const TASKS_CANCEL: u8 = TasksCancel.repr();
    pub const TASKS_DELETE: u8 = TasksDelete.repr();
    pub const TASKS_GET: u8 = TasksGet.repr();
    pub const TASKS_COMPACT: u8 = TasksCompact.repr();
    pub const SETTINGS_ALL: u8 = SettingsAll.repr();
    pub const SETTINGS_GET: u8 = SettingsGet.repr();
    pub const SETTINGS_UPDATE: u8 = SettingsUpdate.repr();
    pub const STATS_ALL: u8 = StatsAll.repr();
    pub const STATS_GET: u8 = StatsGet.repr();
    pub const FIELDS_POST: u8 = FieldsPost.repr();
    pub const METRICS_ALL: u8 = MetricsAll.repr();
    pub const METRICS_GET: u8 = MetricsGet.repr();
    pub const DUMPS_ALL: u8 = DumpsAll.repr();
    pub const DUMPS_CREATE: u8 = DumpsCreate.repr();
    pub const SNAPSHOTS_ALL: u8 = SnapshotsAll.repr();
    pub const SNAPSHOTS_CREATE: u8 = SnapshotsCreate.repr();
    pub const VERSION: u8 = Version.repr();
    pub const KEYS_CREATE: u8 = KeysAdd.repr();
    pub const KEYS_GET: u8 = KeysGet.repr();
    pub const KEYS_UPDATE: u8 = KeysUpdate.repr();
    pub const KEYS_DELETE: u8 = KeysDelete.repr();
    pub const EXPERIMENTAL_FEATURES_GET: u8 = ExperimentalFeaturesGet.repr();
    pub const EXPERIMENTAL_FEATURES_UPDATE: u8 = ExperimentalFeaturesUpdate.repr();

    pub const EXPORT: u8 = Export.repr();

    pub const NETWORK_GET: u8 = NetworkGet.repr();
    pub const NETWORK_UPDATE: u8 = NetworkUpdate.repr();

    pub const CHAT_COMPLETIONS: u8 = ChatCompletions.repr();
    pub const CHATS_ALL: u8 = ChatsAll.repr();
    pub const CHATS_GET: u8 = ChatsGet.repr();
    pub const CHATS_DELETE: u8 = ChatsDelete.repr();
    pub const CHATS_SETTINGS_ALL: u8 = ChatsSettingsAll.repr();
    pub const CHATS_SETTINGS_GET: u8 = ChatsSettingsGet.repr();
    pub const CHATS_SETTINGS_UPDATE: u8 = ChatsSettingsUpdate.repr();

    pub const WEBHOOKS_GET: u8 = WebhooksGet.repr();
    pub const WEBHOOKS_UPDATE: u8 = WebhooksUpdate.repr();
    pub const WEBHOOKS_DELETE: u8 = WebhooksDelete.repr();
    pub const WEBHOOKS_CREATE: u8 = WebhooksCreate.repr();
    pub const WEBHOOKS_ALL: u8 = WebhooksAll.repr();

    pub const DYNAMIC_SEARCH_RULES_GET: u8 = DynamicSearchRulesGet.repr();
    pub const DYNAMIC_SEARCH_RULES_CREATE: u8 = DynamicSearchRulesCreate.repr();
    pub const DYNAMIC_SEARCH_RULES_UPDATE: u8 = DynamicSearchRulesUpdate.repr();
    pub const DYNAMIC_SEARCH_RULES_DELETE: u8 = DynamicSearchRulesDelete.repr();
    pub const DYNAMIC_SEARCH_RULES_ALL: u8 = DynamicSearchRulesAll.repr();
}

#[cfg(test)]
pub(crate) mod test {
    use super::actions::*;
    use super::Action::*;
    use super::*;

    #[test]
    fn test_action_repr_and_constants() {
        assert!(All.repr() == 0 && ALL == 0);
        assert!(Search.repr() == 1 && SEARCH == 1);
        assert!(DocumentsAll.repr() == 2 && DOCUMENTS_ALL == 2);
        assert!(DocumentsAdd.repr() == 3 && DOCUMENTS_ADD == 3);
        assert!(DocumentsGet.repr() == 4 && DOCUMENTS_GET == 4);
        assert!(DocumentsDelete.repr() == 5 && DOCUMENTS_DELETE == 5);
        assert!(IndexesAll.repr() == 6 && INDEXES_ALL == 6);
        assert!(IndexesAdd.repr() == 7 && INDEXES_CREATE == 7);
        assert!(IndexesGet.repr() == 8 && INDEXES_GET == 8);
        assert!(IndexesUpdate.repr() == 9 && INDEXES_UPDATE == 9);
        assert!(IndexesDelete.repr() == 10 && INDEXES_DELETE == 10);
        assert!(IndexesSwap.repr() == 11 && INDEXES_SWAP == 11);
        assert!(TasksAll.repr() == 12 && TASKS_ALL == 12);
        assert!(TasksCancel.repr() == 13 && TASKS_CANCEL == 13);
        assert!(TasksDelete.repr() == 14 && TASKS_DELETE == 14);
        assert!(TasksGet.repr() == 15 && TASKS_GET == 15);
        assert!(SettingsAll.repr() == 16 && SETTINGS_ALL == 16);
        assert!(SettingsGet.repr() == 17 && SETTINGS_GET == 17);
        assert!(SettingsUpdate.repr() == 18 && SETTINGS_UPDATE == 18);
        assert!(StatsAll.repr() == 19 && STATS_ALL == 19);
        assert!(StatsGet.repr() == 20 && STATS_GET == 20);
        assert!(MetricsAll.repr() == 21 && METRICS_ALL == 21);
        assert!(MetricsGet.repr() == 22 && METRICS_GET == 22);
        assert!(DumpsAll.repr() == 23 && DUMPS_ALL == 23);
        assert!(DumpsCreate.repr() == 24 && DUMPS_CREATE == 24);
        assert!(SnapshotsAll.repr() == 25 && SNAPSHOTS_ALL == 25);
        assert!(SnapshotsCreate.repr() == 26 && SNAPSHOTS_CREATE == 26);
        assert!(Version.repr() == 27 && VERSION == 27);
        assert!(KeysAdd.repr() == 28 && KEYS_CREATE == 28);
        assert!(KeysGet.repr() == 29 && KEYS_GET == 29);
        assert!(KeysUpdate.repr() == 30 && KEYS_UPDATE == 30);
        assert!(KeysDelete.repr() == 31 && KEYS_DELETE == 31);
        assert!(ExperimentalFeaturesGet.repr() == 32 && EXPERIMENTAL_FEATURES_GET == 32);
        assert!(ExperimentalFeaturesUpdate.repr() == 33 && EXPERIMENTAL_FEATURES_UPDATE == 33);
        assert!(Export.repr() == 34 && EXPORT == 34);
        assert!(NetworkGet.repr() == 35 && NETWORK_GET == 35);
        assert!(NetworkUpdate.repr() == 36 && NETWORK_UPDATE == 36);
        assert!(ChatCompletions.repr() == 37 && CHAT_COMPLETIONS == 37);
        assert!(ChatsAll.repr() == 38 && CHATS_ALL == 38);
        assert!(ChatsGet.repr() == 39 && CHATS_GET == 39);
        assert!(ChatsDelete.repr() == 40 && CHATS_DELETE == 40);
        assert!(ChatsSettingsAll.repr() == 41 && CHATS_SETTINGS_ALL == 41);
        assert!(ChatsSettingsGet.repr() == 42 && CHATS_SETTINGS_GET == 42);
        assert!(ChatsSettingsUpdate.repr() == 43 && CHATS_SETTINGS_UPDATE == 43);
        assert!(AllGet.repr() == 44 && ALL_GET == 44);
        assert!(WebhooksGet.repr() == 45 && WEBHOOKS_GET == 45);
        assert!(WebhooksUpdate.repr() == 46 && WEBHOOKS_UPDATE == 46);
        assert!(WebhooksDelete.repr() == 47 && WEBHOOKS_DELETE == 47);
        assert!(WebhooksCreate.repr() == 48 && WEBHOOKS_CREATE == 48);
        assert!(WebhooksAll.repr() == 49 && WEBHOOKS_ALL == 49);
        assert!(IndexesCompact.repr() == 50 && INDEXES_COMPACT == 50);
        assert!(FieldsPost.repr() == 51 && FIELDS_POST == 51);
        assert!(TasksCompact.repr() == 52 && TASKS_COMPACT == 52);
        assert!(DynamicSearchRulesGet.repr() == 53 && DYNAMIC_SEARCH_RULES_GET == 53);
        assert!(DynamicSearchRulesCreate.repr() == 54 && DYNAMIC_SEARCH_RULES_CREATE == 54);
        assert!(DynamicSearchRulesUpdate.repr() == 55 && DYNAMIC_SEARCH_RULES_UPDATE == 55);
        assert!(DynamicSearchRulesDelete.repr() == 56 && DYNAMIC_SEARCH_RULES_DELETE == 56);
        assert!(DynamicSearchRulesAll.repr() == 57 && DYNAMIC_SEARCH_RULES_ALL == 57);
    }

    #[test]
    fn test_from_repr() {
        for action in enum_iterator::all::<Action>() {
            let repr = action.repr();
            let action_from_repr = Action::from_repr(repr);
            assert_eq!(Some(action), action_from_repr, "Failed for action: {:?}", action);
        }
    }
}
