#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]

// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use ordinary_types::{Field, Kind};
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fmt::Write;

fn default_public_dns_ip() -> [u8; 4] {
    // todo: make this something other than goog
    [8, 8, 8, 8]
}

fn default_env_name() -> String {
    "development".to_string()
}

#[derive(Deserialize, Serialize, Clone)]
pub struct OrdinaryApiConfig {
    #[serde(default = "default_public_dns_ip")]
    pub public_dns_ip: [u8; 4],
    #[serde(default = "default_env_name")]
    pub env_name: String,
    #[serde(default)]
    pub limits: OrdinaryApiLimits,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AssetsLimits {
    /// Allowed extensions corresponding to
    /// [MIME](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types) types.
    ///
    /// (More to be supported in the future)
    ///
    /// Supported:
    /// - "txt"
    /// - "xml"
    /// - "html"
    /// - "css"
    /// - "csv"
    /// - "js"
    /// - "png"
    /// - "apng"
    /// - "gif"
    /// - "svg"
    /// - "jpg"
    /// - "jpeg"
    /// - "bmp"
    /// - "tif"
    /// - "tiff"
    /// - "webp"
    /// - "avif"
    /// - "ico"
    /// - "pdf"
    /// - "json"
    /// - "wasm"
    ///
    /// Special:
    /// - "none" (for no extension; uses "application/octet-stream")
    /// - "any" (for unsupported extensions; also uses "application/octet-stream")
    pub allowed_extensions: Vec<String>,
    /// Limit on total storage for all assets within an app
    /// (bytes).
    pub max_store_size: u64,
    /// Limit on individual asset size (bytes).
    pub max_asset_size: u64,
}

impl Default for AssetsLimits {
    fn default() -> Self {
        Self {
            allowed_extensions: vec![
                "txt".into(),
                "xml".into(),
                "html".into(),
                "css".into(),
                "csv".into(),
                "js".into(),
                "png".into(),
                "apng".into(),
                "gif".into(),
                "svg".into(),
                "jpg".into(),
                "jpeg".into(),
                "bmp".into(),
                "tif".into(),
                "tiff".into(),
                "webp".into(),
                "avif".into(),
                "ico".into(),
                "pdf".into(),
                "json".into(),
                "wasm".into(),
            ],
            max_store_size: 100_000_000,
            max_asset_size: 1_500_000,
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ArtifactLimits {
    pub max_store_size: u64,
    pub max_artifact_size: u64,
}

impl Default for ArtifactLimits {
    fn default() -> Self {
        Self {
            max_store_size: 10_000_000,
            max_artifact_size: 500_000,
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct CacheLimits {
    pub max_size_range: (u64, u64),
    pub max_count_range: (usize, usize),

    pub sync_interval_ranges: ((u64, u64), (u64, u64)),
    pub clean_interval_ranges: ((u64, u64), (u64, u64)),
}

impl Default for CacheLimits {
    fn default() -> Self {
        Self {
            max_size_range: (1_000_000, 10_000_000),
            max_count_range: (100, 500),

            sync_interval_ranges: ((2, 14), (18, 30)),
            clean_interval_ranges: ((5, 10), (15, 20)),
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ContentLimits {
    pub search_enabled: bool,

    pub max_content_definitions: u8,
    pub max_content_fields: u8,

    pub max_store_size: u64,
    pub max_object_size: u64,
    pub max_field_size: u64,
}

impl Default for ContentLimits {
    fn default() -> Self {
        Self {
            search_enabled: true,

            max_content_definitions: 255,
            max_content_fields: 255,

            max_store_size: 10_000_000,
            max_object_size: 400_000,
            max_field_size: 200_000,
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ModelLimits {
    pub search_enabled: bool,

    pub max_model_definitions: u8,
    pub max_model_fields: u8,

    pub max_item_size: u64,
    pub max_field_size: u64,
}

impl Default for ModelLimits {
    fn default() -> Self {
        Self {
            search_enabled: true,

            max_model_definitions: 255,
            max_model_fields: 255,

            max_item_size: 400_000,
            max_field_size: 100_000,
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct StorageLimits {
    /// Maximum total storage for all apps and api.
    ///
    /// Unit: bytes.
    ///
    /// Default: 10 GB
    pub max_storage: u64,
    /// Maximum per-app storage.
    ///
    /// Unit: bytes.
    ///
    /// Default: 50 MB
    pub max_app_storage: u64,

    pub assets: AssetsLimits,
    pub artifact: ArtifactLimits,
    pub cache: CacheLimits,
    pub content: ContentLimits,
    pub model: ModelLimits,
}

impl Default for StorageLimits {
    fn default() -> Self {
        Self {
            max_storage: 20_000_000_000,
            max_app_storage: 50_000_000,

            assets: AssetsLimits::default(),
            artifact: ArtifactLimits::default(),
            cache: CacheLimits::default(),
            content: ContentLimits::default(),
            model: ModelLimits::default(),
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct MonitorLimits {}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct IntegrationLimits {
    /// Max number of integrations per app.
    pub count: u8,

    /// Integration request timeout.
    ///
    /// Unit: seconds.
    pub max_timeout: u16,
}

impl Default for IntegrationLimits {
    fn default() -> Self {
        Self {
            count: 255,
            max_timeout: 10,
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ActionLimits {
    /// Max number of actions per app.
    pub count: u8,

    /// Action request timeout.
    ///
    /// Unit: seconds.
    pub max_timeout: u16,
}

impl Default for ActionLimits {
    fn default() -> Self {
        Self {
            count: 255,
            max_timeout: 10,
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct AuthLimits {}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateLimits {
    /// Max number of templates per app.
    pub count: u8,

    /// Template request timeout.
    ///
    /// Unit: seconds.
    pub max_timeout: u16,
}

impl Default for TemplateLimits {
    fn default() -> Self {
        Self {
            count: 255,
            max_timeout: 10,
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct OrdinaryApiLimits {
    /// Max default timeout for all HTTP requests.
    ///
    /// Default: 10
    pub max_default_timeout: u16,

    pub action: ActionLimits,
    pub auth: AuthLimits,
    pub integration: IntegrationLimits,
    pub monitor: MonitorLimits,
    pub storage: StorageLimits,
    pub template: TemplateLimits,
}

impl Default for OrdinaryApiLimits {
    fn default() -> Self {
        Self {
            max_default_timeout: 10,

            action: ActionLimits::default(),
            auth: AuthLimits::default(),
            integration: IntegrationLimits::default(),
            monitor: MonitorLimits::default(),
            storage: StorageLimits::default(),
            template: TemplateLimits::default(),
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ClientLoggingConfig {
    /// bottom end of the delayed delivery range (seconds)
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    min_delay: Option<u32>,
    /// top end of delayed delivery range (seconds)
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    max_delay: Option<u32>,
    /// max number of events to be buffered on the client
    /// prior to flush.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    max_buffer: Option<u16>,
    /// sets the max number of events in a given request.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    max_batch: Option<u16>,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum RedactedHashAlg {
    Blake2,
    Blake3,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ServerLoggingConfig {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub ips: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub headers: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub credentials: Option<RedactedHashAlg>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub timing: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub sizes: Option<bool>,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct LoggingConfig {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub client: Option<ClientLoggingConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub server: Option<ServerLoggingConfig>,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Check {
    // todo: what to validate the token against
    // ?? i.e "token.fields.account is included in list of post.author.followers.accounts"
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum SignedTokenAlgorithm {
    DsaEd25519,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HmacTokenAlgorithm {
    HmacBlake2b256,
}

/// Configuration for signed tokens.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct SignedTokenConfig {
    /// Algorithm used for verifying the token.
    pub algorithm: SignedTokenAlgorithm,
    /// How frequently the keys are rotated (seconds).
    pub rotation: u32,
}

impl Default for SignedTokenConfig {
    fn default() -> SignedTokenConfig {
        SignedTokenConfig {
            algorithm: SignedTokenAlgorithm::DsaEd25519,
            rotation: 60 * 60 * 24 * 30,
        }
    }
}

/// Configuration for refresh tokens.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct RefreshTokenConfig {
    /// How long a token should be valid for (seconds).
    pub lifetime: u32,
}

impl Default for RefreshTokenConfig {
    fn default() -> RefreshTokenConfig {
        RefreshTokenConfig {
            lifetime: 60 * 60 * 24 * 7,
        }
    }
}

/// Configuration for HMAC'd tokens.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct HmacTokenConfig {
    /// Algorithm used for verifying the token.
    pub algorithm: HmacTokenAlgorithm,
    /// How frequently the key is rotated (seconds).
    pub rotation: u32,
}

impl Default for HmacTokenConfig {
    fn default() -> HmacTokenConfig {
        HmacTokenConfig {
            algorithm: HmacTokenAlgorithm::HmacBlake2b256,
            rotation: 60 * 60 * 24 * 30,
        }
    }
}

/// Configuration for access tokens.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AccessTokenConfig {
    /// How long a token should be valid for (seconds).
    pub lifetime: u32,
    /// Token claims structuring.
    ///
    /// Note: `idx` starts at 1 to create space for system claims (id, domain, and account).
    pub claims: Vec<Field>,
}

impl Default for AccessTokenConfig {
    fn default() -> AccessTokenConfig {
        AccessTokenConfig {
            lifetime: 60 * 60 * 24,
            claims: vec![],
        }
    }
}

/// Configuration for client password hashing.
///
/// When JavaScript or WASM modes are enabled, passwords are
/// hashed with the application name, and account, before transit
/// (or in the case of WASM prior to the Opaque client operations
/// if Opaque is selected for the `PasswordProtocol`).
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ClientPasswordHash {
    /// Limited by what browsers can support, SHA-256 is
    /// a good option for a client-side hash to enable
    /// slightly better password protection when in
    /// javascript-only mode, without the WASM for Opaque.
    Sha256,
}

/// Configuration for password protocol.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum PasswordProtocol {
    /// When WASM is enabled, this will run the client portions
    /// browser-side. When JavaScript-only, passwords will be hashed
    /// and then sent to the server where the client portion will
    /// be done on behalf of the user. In noscript mode, the password
    /// is sent only protected by TLS, hashed and then the client operations
    /// are done server side.
    ///
    /// If a user later decides to enable JavaScript or WASM, they'll be
    /// able to opt in to the no-plain-password-sent modes without interruption.
    Opaque,
    // todo: SRP and Bcrypt/Argon2 hashing?
}

/// Configuration for passwords.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct PasswordConfig {
    pub protocol: PasswordProtocol,
}

impl Default for PasswordConfig {
    fn default() -> PasswordConfig {
        PasswordConfig {
            protocol: PasswordProtocol::Opaque,
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TotpAlgorithm {
    /// only allowing SHA1 for now
    /// because many of the major MFA authenticator
    /// apps don't support SHA256 or SHA512, and fail silently.
    Sha1,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TotpConfig {
    /// Used for response after registration form is submitted.
    /// Will just return a QR code SVG if not set.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub template: Option<String>,
    pub algorithm: TotpAlgorithm,
}

impl Default for TotpConfig {
    fn default() -> TotpConfig {
        TotpConfig {
            template: None,
            algorithm: TotpAlgorithm::Sha1,
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct MfaConfig {
    pub totp: TotpConfig,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum InviteMode {
    /// anyone who has been invited can invite anyone else
    Viral,
    // ?? anyone who has been invited has a limited set of invites they
    // ?? can share on an optional interval.
    // todo: Limited,
    // ?? only those invited with permission to invite can invite others
    // todo: Selective,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct InviteConfig {
    pub mode: InviteMode,
    /// How long a token is valid for.
    ///
    /// Defaults to 7 days.
    pub lifetime: u32,
    /// On what interval to clean up expired token ids.
    ///
    /// Defaults to 30 - 90 seconds.
    pub clean_interval: (u32, u32),
    /// Values that can be used internally in the API server to
    /// set default permissions for new accounts.
    ///
    /// TODO: in the future, these can also be set in order to pre-validate
    /// TODO: or constrain `app` account registrations. This will require the
    /// TODO: use of an `InviteCreate` action trigger (for validating and setting
    /// TODO: the invite token claims), and a pre-registration `InviteValidate`,
    /// TODO: as well as passing the invite token claims to the `Registration` trigger
    /// TODO: (allowing for the validated invite claims to be used in the account
    /// TODO: claims setting operation).
    ///
    /// Note: `idx` starts at 1 to create space for system claims (id, domain, and account).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub claims: Option<Vec<Field>>,
}

impl Default for InviteConfig {
    fn default() -> InviteConfig {
        InviteConfig {
            mode: InviteMode::Viral,
            lifetime: 60 * 60 * 24 * 7,
            clean_interval: (30, 90),
            claims: None,
        }
    }
}

/// Auth configuration.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AuthConfig {
    /// Configuration for passwords.
    pub password: PasswordConfig,
    /// MFA configuration for auth.
    pub mfa: MfaConfig,
    /// Configures all hmac tokens
    pub hmac_token: HmacTokenConfig,
    /// Configures all signed tokens
    pub signed_token: SignedTokenConfig,
    /// Configuration for refresh tokens.
    pub refresh_token: RefreshTokenConfig,
    /// Configuration for access tokens.
    pub access_token: AccessTokenConfig,
    /// Determines whether cookies should be used
    /// for browser based template navigation, and form submissions
    ///
    /// Note: when set to `false`, `protected` templates, and actions triggered
    /// by form submission will fail. Form based registration and login will
    /// necessarily be disabled, also.
    ///
    /// !! Important: when set to `true`, tokens retrieved for `js` and `noscript` flavors
    /// !! do not support client signatures, because http-only cookies cannot be signed by
    /// !! the client.
    pub cookies_enabled: bool,
    /// what algorithm is used to hash passwords and MFA codes
    pub client_hash: ClientPasswordHash,

    /// invite config
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub invite: Option<InviteConfig>,
}

impl Default for AuthConfig {
    fn default() -> AuthConfig {
        AuthConfig {
            password: PasswordConfig::default(),
            mfa: MfaConfig::default(),
            hmac_token: HmacTokenConfig::default(),
            signed_token: SignedTokenConfig::default(),
            refresh_token: RefreshTokenConfig::default(),
            access_token: AccessTokenConfig::default(),
            cookies_enabled: false,
            client_hash: ClientPasswordHash::Sha256,
            invite: None,
        }
    }
}

/// Compression algorithms
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum CompressionAlgorithm {
    Gzip,
    Zstd { level: u8 },
    Brotli,
    Deflate,
}

impl CompressionAlgorithm {
    #[must_use]
    pub fn as_u8(&self) -> u8 {
        match self {
            Self::Gzip => 1,
            Self::Zstd { level: _ } => 2,
            Self::Brotli => 3,
            Self::Deflate => 4,
        }
    }

    #[must_use]
    pub fn as_char(&self) -> char {
        match self {
            Self::Gzip => '1',
            Self::Zstd { level: _ } => '2',
            Self::Brotli => '3',
            Self::Deflate => '4',
        }
    }

    #[must_use]
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Gzip => "gzip",
            Self::Zstd { level: _ } => "zstd",
            Self::Brotli => "br",
            Self::Deflate => "deflate",
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum StoredCachePolicy {
    /// No eviction on clean or write. Constrained only by overall storage limit or `max_size`
    /// (if set). Will only be evicted if dependencies change and `evict_on_dependency_change`
    /// is set.
    Permanent,
    /// Prioritize the most Frequently accessed, Recently accessed and smallest Sized items
    ///
    /// (`frequency_equality_threshold` (hit count), `recency_equality_threshold` (seconds))
    FRs(u64, u64),
}

/// Render caching policy.
///
/// IMPORTANT: Very experimental, may not work as described. `policy: Permanent` is currently
/// the most likely to behave correctly.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct StoredCache {
    pub policy: StoredCachePolicy,

    /// Which compression formats should be stored
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub compression: Option<Vec<CompressionAlgorithm>>,

    /// Upper limit on total time a cached item can be stored
    ///
    /// Unit: seconds
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub max_ttl: Option<u64>,

    /// Compared with time since last hit.
    ///
    /// Unit: seconds
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub hit_ttl: Option<u64>,

    /// Upper limit on the cumulative size of all cached responses
    /// for a given template.
    ///
    /// Unit: bytes
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub max_size: Option<u64>,

    /// Upper limit on the number of cached responses stored at
    /// a given time, for a given template.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub max_count: Option<usize>,

    /// How long an LFU "hit" tick is valid for
    ///
    /// Unit: seconds
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub frequency_window: Option<u64>,

    /// Rate at which the cache is cleaned based on other rules.
    ///
    /// Option<(min, max)>
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub clean_interval: Option<(u64, u64)>,

    /// Rate at which the in-memory variables are synced to persistent.
    ///
    /// Option<(min, max)>
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub sync_interval: Option<(u64, u64)>,

    /// Whether a cached item should also track the of models and content
    /// which it depends on, and evict when they are modified.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub evict_on_dependency_change: Option<bool>,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum XXH3Variation {
    Bit64,
    Bit128,
}

/// Hashing algorithm options for generating etags.
///
/// `AHash` is the default if none is selected.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpEtagAlgorithm {
    AHash,
    XXH3(XXH3Variation),
    Rustc,
    Blake3,
}

/// HTTP Cache-Control.
/// See: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control>
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct HttpEtag {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub alg: Option<HttpEtagAlgorithm>,
}

/// HTTP Cache-Control.
/// See: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control>
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct HttpCacheControl {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub max_age: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub s_maxage: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub no_cache: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub no_store: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub no_transform: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub must_revalidate: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub proxy_revalidate: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub private: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub public: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub immutable: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub stale_while_revalidate: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub stale_if_error: Option<bool>,
}

impl HttpCacheControl {
    pub fn header_value(
        &self,
        cache_control: &mut String,
        default: &str,
    ) -> Result<(), Box<dyn Error>> {
        if let Some(max_age) = self.max_age {
            write!(cache_control, "max-age={max_age}, ")?;
        }
        if let Some(s_maxage) = self.s_maxage {
            write!(cache_control, "s-maxage={s_maxage}, ")?;
        }
        if let Some(no_cache) = self.no_cache
            && no_cache
        {
            write!(cache_control, "no-cache, ")?;
        }
        if let Some(no_store) = self.no_store
            && no_store
        {
            write!(cache_control, "no-store, ")?;
        }
        if let Some(no_transform) = self.no_transform
            && no_transform
        {
            write!(cache_control, "no-transform, ")?;
        }
        if let Some(must_revalidate) = self.must_revalidate
            && must_revalidate
        {
            write!(cache_control, "must-revalidate, ")?;
        }
        if let Some(proxy_revalidate) = self.proxy_revalidate
            && proxy_revalidate
        {
            write!(cache_control, "proxy-revalidate, ")?;
        }
        if let Some(stale_while_revalidate) = self.stale_while_revalidate
            && stale_while_revalidate
        {
            write!(cache_control, "stale-while-revalidate, ")?;
        }
        if let Some(private) = self.private
            && private
        {
            write!(cache_control, "private, ")?;
        }
        if let Some(public) = self.public
            && public
        {
            write!(cache_control, "public, ")?;
        }
        if let Some(immutable) = self.immutable
            && immutable
        {
            write!(cache_control, "immutable, ")?;
        }
        if let Some(stale_if_error) = self.stale_if_error
            && stale_if_error
        {
            write!(cache_control, "stale-if-error, ")?;
        }

        if cache_control.is_empty() {
            cache_control.push_str(default);
        } else {
            cache_control.pop();
            cache_control.pop();
        }

        Ok(())
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct HttpCache {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub cache_control: Option<HttpCacheControl>,

    /// Corresponds to the HTTP Expires header.
    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Expires>
    ///
    /// Unit: seconds.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub expires: Option<u64>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub etag: Option<HttpEtag>,
}

/// Field used within the scope of a single template.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateField {
    /// Field name
    pub name: String,
    /// Specifies the type of the value.
    pub kind: Kind,
    /// Validated by `kind` at build time.
    ///
    /// Fixed to `String` type for now.
    pub value: String,
    // todo: pub value: serde_json::Value;
}

/// Query expression to be used in template bindings.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum QueryExpression {
    Gte,
    Gt,
    Lte,
    Lt,
    Eq,
    BeginsWith,
}

/// Binding options for template field refs.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TemplateRefFieldBind {
    /// Bind this property to a token field.
    Token {
        /// Name of the field that this property should bind to.
        /// (i.e "account" if you'd like to have an "/account" route
        /// that interprets the request based on the logged-in user's
        /// token claims/fields).
        field: String,
        /// Include if the parent field is queryable.
        #[serde(skip_serializing_if = "Option::is_none")]
        #[serde(default)]
        expression: Option<QueryExpression>,
    },
    /// Bind this property to a route segment (i.e /{something})
    Segment {
        /// Specifies the name of the route segment that this property is binding to.
        name: String,
        /// Include if the parent field is queryable.
        #[serde(skip_serializing_if = "Option::is_none")]
        #[serde(default)]
        expression: Option<QueryExpression>,
    },
}

/// Declaration for which fields from a content definition
/// or data model to include.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateRefField {
    /// Specifies index for reference field. Index
    /// must be unique across fields for a given reference.
    pub idx: u8,
    /// Name of field to be included.
    pub name: String,
    /// Option to bind this field's value to a route
    /// segment, querystring parameter or token field.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub bind: Option<TemplateRefFieldBind>,
    /// List of any nested subfields to include for this field.
    #[cfg_attr(feature = "utoipa", schema(no_recursion))]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub fields: Option<Vec<TemplateRefField>>,
}

/// How templates reference Content Definitions
/// and Data Models, and the specific fields it
/// wants to include.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateRef {
    /// Specifies the index position for the referenced
    /// data. Index must be unique across flags, params, data models and
    /// content definitions.
    pub idx: u8,
    /// Name of the model or content definition.
    pub name: String,
    /// Which fields to include.
    pub fields: Vec<TemplateRefField>,
    /// for requesting multiple top-level items
    ///
    /// note: only supported for content. models use relationships.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub all: Option<String>,
}

/// Server flags can only be used in templates whose cache is
/// set to "Never".
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateFlagRef {
    /// Specifies the index position for the referenced
    /// data. Index must be unique across flags, data models and
    /// content definitions.
    pub idx: u8,
    /// Name of the flag.
    pub name: String,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateParamRef {
    /// Specifies the index position for the referenced
    /// data. Index must be unique across flags, data models, params, and
    /// content definitions.
    pub idx: u8,
    /// Name of the param.
    pub name: String,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateCache {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub stored: Option<StoredCache>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub http: Option<HttpCache>,
}

/// Corresponds to <https://docs.rs/wasm-opt/latest/wasm_opt/struct.OptimizationOptions.html#impl-OptimizationOptions>.
///
/// Certain levels may not work with `wasmtime`/`cranelift`.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum WasmOpt {
    Size,
    SizeAggressive,
    Level0,
    Level1,
    Level2,
    Level3,
    Level4,
}

impl WasmOpt {
    #[must_use]
    pub fn as_flag(&self) -> &'static str {
        match self {
            Self::Size => "-Os",
            Self::SizeAggressive => "-Oz",
            Self::Level0 => "-O0",
            Self::Level1 => "-O1",
            Self::Level2 => "-O2",
            Self::Level3 => "-O3",
            Self::Level4 => "-O4",
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TemplateFfiVersion {
    V1,
}

/// Input serialization format. Output is always an array of bytes.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TemplateFfiSerialization {
    FlexBufferVector,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateFfi {
    pub version: TemplateFfiVersion,
    pub serialization: TemplateFfiSerialization,
}

/// Template configuration for Ordinary Applications.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateConfig {
    /// Foreign function interface config
    pub ffi: TemplateFfi,

    /// Unique index for template.
    pub idx: u8,
    /// Template's name.
    pub name: String,
    /// Used as the `content-type` header for HTTP responses.
    /// Validated against file extension (if `path` is present).
    // todo: switch this to an enum of mime types
    pub mime: String,
    /// Specifies whether the content in the file should
    /// be "minified"/have whitespace removed.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub minify: Option<bool>,
    /// Relative path to the template file
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub path: Option<String>,
    /// The route used in the HTTP server to serve this template.
    /// Can use segments to bind to properties on models or content
    /// definitions:
    ///
    /// ```json
    /// {
    ///     ...
    ///     "route": "/posts/{slug}",
    ///     ...
    ///     "models": [
    ///         {
    ///             "name": "post",
    ///             "fields": [
    ///                 { "name": "slug", "bind": { "Segment": { "name": "slug" } } },
    ///             ]
    ///         }
    ///     ]
    /// }
    /// ```
    pub route: String,
    /// What to check the token fields against. If left blank, route is considered public.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub protected: Option<Check>,
    /// Used to specify the cache policy for this template.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub cache: Option<TemplateCache>,

    /// HTTP Content Security Policy configuration.
    ///
    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP>
    ///
    /// "Base" defaults to `default-src 'self';` and tacks on SHA-256 integrity
    /// hashes for all inlined scripts and styles (generated at build time) to
    /// `script-src 'self' sha256-b64` and `style-src 'self' sha256-b64`, respectively.
    ///
    /// `https:` is used when not running in `--insecure` mode.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub csp: Option<HttpCsp>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub cors: Option<HttpCors>,

    /// Max duration for the template.
    ///
    /// Unit: seconds
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub timeout: Option<u16>,

    /// Used for template-specific variables that don't need to be shared
    /// beyond the scope of the given template, and don't warrant a content
    /// object.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub fields: Option<Vec<TemplateField>>,
    /// List of global variables to be included with the compiled template
    /// binary. Globals are excluded by default and have to be explicitly
    /// listed in the globals to be accessed from the template.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub globals: Option<Vec<String>>,
    /// List of flags to be referenced by the template.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub flags: Option<Vec<TemplateFlagRef>>,
    /// List of params to be referenced by the template.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub params: Option<Vec<TemplateParamRef>>,
    /// List of models and what fields the template needs from the models.
    /// This is effectively a query definition.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub models: Option<Vec<TemplateRef>>,
    /// List of content definitions and the content definition fields
    /// that this template will use.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub content: Option<Vec<TemplateRef>>,

    /// Specifies which actions this template triggers, and adds
    /// this template's route pattern to the action's list of valid
    /// origins.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub actions: Option<Vec<String>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub wasm_opt: Option<WasmOpt>,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AssetsConfig {
    /// Relative path to assets directory.
    pub dir_path: String,

    /// HTTP cache configuration.
    ///
    /// TODO: provide a way to pattern match files for which to apply
    /// TODO: specific cache controls.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub http: Option<HttpCache>,

    /// Which encodings to use when precompressing assets.
    ///
    /// Serving priority is dictated by specified order.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub precompression: Option<Vec<CompressionAlgorithm>>,

    /// Determines whether CSS files should be minified,
    /// prior to write.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub minify_css: Option<bool>,

    /// Determines whether JS files should be minified,
    /// prior to write.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub minify_js: Option<bool>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub internal_cache_control_header_value: Option<String>,
}

impl AssetsConfig {
    pub fn init(&mut self, default_cache_control_header_value: &str) -> Result<(), Box<dyn Error>> {
        if let Some(http_cache) = &self.http
            && let Some(http_cache_control) = &http_cache.cache_control
        {
            let mut header = String::new();
            http_cache_control.header_value(&mut header, default_cache_control_header_value)?;

            self.internal_cache_control_header_value = Some(header);
        }

        Ok(())
    }
}

/// Global constant definitions, for use in [`ordinary_template::Template`]s.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Global {
    /// Name of the global constant.
    pub name: String,
    /// Type definition for the global variable.
    pub kind: Kind,
    /// Value of the global constant (validated by against the `kind`
    /// at build time).
    ///
    /// Fixed to `String` type for now.
    pub value: String,
    // todo: pub value: serde_json::Value;
}

/// Where Ordinary will look for the secret.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum SecretSource {
    /// Name of the host provided environment variable/secret.
    ///
    /// `Env` secrets are available to every tenant for a given
    /// API server (when running in "multi" mode).
    ///
    /// This is useful in scenarios where a provider running the API
    /// server would like to expose convenient integrations with 3rd parties, for
    /// which it only wants to maintain a single set of credentials.
    ///
    /// `Env` secrets are also useful when you're running a standalone
    /// application and do not need a more complicated secrets management
    /// paradigm.
    Env(String),
    /// Name of a stored secret.
    ///
    /// `Stored` secrets are application scoped, and can be set
    /// through an API server on which the application runs.
    ///
    /// `Stored` secrets live in their own database and are only
    /// accessible to components with permissions.
    Stored(String),
    // `Manager` mode allows you to select an external secrets
    // manager, by setting a `Stored` secret with the external `Manager`'s
    // token/password.
    // todo: Manager
}

/// Where the secret is accessible from.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum SecretVisibility {
    // `Actions` visibility level should only be set in cases
    // where you're confident in the code that's being executed,
    // or the secret is not shared between trusted/untrusted modules.
    //
    // Because `Action` visible secrets are passed into the module,
    // they can be (intentionally OR unintentionally) leaked if
    // included in what's returned by the module.
    // todo: Actions,
    /// `Integrations` level visibility runs the risk of unintentionally
    /// sending a secret to the incorrect endpoint, but does not expose
    /// secrets to any user defined modules.
    Integrations,
}

/// Mechanism for exposing secrets to `Integration`s.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Secret {
    /// Name of the secret.
    pub name: String,
    /// Where to retrieve the secret from.
    pub source: SecretSource,
    /// Where the secret is to be used.
    pub visibility: SecretVisibility,
}

/// Option for the feature flag.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct FlagOption {
    /// Unique index for this flag option.
    pub idx: u8,
    /// Name of this feature flag option.
    pub name: String,
    /// Percentage of users that will have this
    /// flag turned on.
    pub percentage: u8,
}

/// Feature flag definition.
///
/// Note: Flags use cookies for non-logged in users,
/// and use fields set on their token after they're logged in
/// so that users have the ability to configure their preference
/// if they have one.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Flag {
    /// Unique index for the feature flag.
    pub idx: u8,
    /// Name of the feature flag.
    pub name: String,
    /// Options for this flag. Percentage must
    /// total 100.
    pub options: Vec<FlagOption>,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ContentDefinition {
    /// Unique index for the content definition.
    pub idx: u8,

    /// Name of the content definition.
    pub name: String,

    /// Fields for the content definition.
    ///
    /// Note: only fields with the `String` and `Uuid` kinds can
    /// be indexed (for now).
    pub fields: Vec<Field>,
}

/// Content is used for static values that should be updated
/// independent of document structure, stylings or behavior.
///
/// Content is stored in a denormalized format for indexed fields
/// to optimize for maximum read efficiency.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Content {
    /// Specifies the path to the JSON file which contains
    /// the content objects.
    pub file_path: String,
    /// Definitions/structure of the content objects in the
    /// `content.json`.
    pub definitions: Vec<ContentDefinition>,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum UuidVersion {
    V4,
    V7,
}

/// Defines a model in the Ordinary Database.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ModelConfig {
    /// Index of the model. Used for its kind
    /// for storage keys. There can be no gaps in
    /// index values.
    ///
    /// Note: the first (0) index is always skipped as it
    /// is reserved for the item UUID.
    pub idx: u8,
    /// Name of the model.
    pub name: String,
    /// Fields on the model.
    pub fields: Vec<Field>,
    /// Every model is generated with a UUID key. If this value
    /// is blank, it will default to V4. If you'd like records to
    /// be ordered by time, V7 is recommended.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub uuid: Option<UuidVersion>,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum IntegrationProtocolHttpEncoding {
    Json,
    Text,
}

/// The protocol for the integration.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum IntegrationProtocol {
    /// For integrating an external HTTP API.
    Http {
        /// HTTP method.
        method: String,
        /// static http headers
        headers: Vec<(String, String)>,
        /// how to encode the value passed from the action
        send_encoding: IntegrationProtocolHttpEncoding,
        /// how to decode the value passed back to the action
        recv_encoding: IntegrationProtocolHttpEncoding,
    },
    // When integrating an external gRPC API.
    // todo: Grpc { metadata: Vec<(String, String)> },
    // When integrating an external Cap'n Proto API.
    // todo: CapnProto,
    // When integrating an external GraphQL API.
    // todo: GraphQL,
    // When integrating an external PostgreSQL database.
    // todo: Postgres { statement: String },
    // When integrating an external OpenSearch database.
    // todo: OpenSearch,
    // For integrating with SMTP mail server
    // todo: Smtp,
}

/// The mechanism for reverse proxying and calling
/// out to external APIs.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct IntegrationConfig {
    /// Unique index for integration
    pub idx: u8,
    /// Name of the integration.
    pub name: String,
    /// Protocol used for communicating with the external service.
    pub protocol: IntegrationProtocol,
    /// Endpoint that the external service lives at.
    pub endpoint: String,
    /// Definition for the parameters of the receiving service.
    ///
    /// (Automatically translated to the service's protocol/encoding format).
    pub send: Kind,
    /// Definition for the returned value of the external service.
    ///
    /// (Automatically translated from the service's protocol/encoding format).
    pub recv: Kind,
    /// Names of secrets to include
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub secrets: Option<Vec<String>>,

    /// Max duration for the template.
    ///
    /// Unit: seconds
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub timeout: Option<u16>,
}

/// The language in which the action is written.
///
/// Many more languages will be supported in the future.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionLang {
    /// Action is written in the Rust programming language.
    ///
    /// Uses zero-copy `FlexBuffer` vectors for serialization format.
    Rust,
    /// Action is written in JavaScript.
    ///
    /// `QuickJS` runtime embedded in a Rust WASM which itself uses
    /// `FlexBuffer` vectors for FFI serialization, but then translates
    /// to JSON when communicating across the `QuickJS` runtime barrier.
    JavaScript,
    // todo: Golang,
    // todo: DotNet,
    // todo: Kotlin,
}

/// Model operations that an action is allowed to make.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionAccessModelOps {
    /// Can create an item for this model.
    Insert,
    /// Can get an item for this model by UUID or Index.
    Get,
    /// Can query an item for this model by queryable field.
    Query,
    /// Can search an item for this model by searchable field.
    Search,
    /// Can update an item for this model.
    Update,
    /// Can delete an item for this model.
    Delete,
}

/// Auth operations that an action can make.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionAccessAuthOps {
    /// Allows the action the ability to set
    /// a user's access token fields/claims.
    SetTokenFields,
}

/// Defines access permissions that an Ordinary
/// Action can configure.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionAccessPermission {
    /// Provides the action access to a given model.
    Model {
        /// Name of the model.
        name: String,
        /// List of allowed operations that the action
        /// can take on the model.
        ops: Vec<ActionAccessModelOps>,
    },
    /// Provides the action access to a given content def.
    Content {
        /// Content definition name.
        name: String,
    },
    /// Provides the action access to a given integration.
    Integration {
        /// Name of the integration the action can access.
        name: String,
    },
    /// Provides the action access to another action.
    Action {
        /// Name of the other action.
        name: String,
    },
    /// Provides the action access to Ordinary Auth.
    Auth {
        /// Which operations the action is allowed to take.
        ops: Vec<ActionAccessAuthOps>,
    },
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpMethod {
    PUT,
    POST,
    GET,
    DELETE,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionTriggerModelOps {
    Insert,
    Update,
    Delete,
}

/// Action trigger options.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionTrigger {
    /// HTTP request with the Ordinary data format
    Ordinary,
    /// JSON formatted API call
    Json {
        /// API rout
        route: String,
        /// HTTP method
        method: HttpMethod,
    },
    /// Web Form Submission
    Form {
        /// endpoint the form should point at
        route: String,
        /// method for the form to use
        method: HttpMethod,
        /// redirect for after submission.
        ///
        /// note: it is allowed to use return values in the route
        /// via {return} or {`return.some_field_name`}
        redirect: String,
    },
    /// Login event from Auth
    Login,
    /// Registration event from Auth
    Registration,
    /// For when content updates
    Content { name: String },
    // Run on model insert/update/delete
    // with affected item as argument.
    // todo: Model {
    //     name: String,
    //     op: ActionAccessModelOps,
    // },

    // Have the action triggered on a regular cadence.
    // todo: Job { expression: String },

    // gRPC formatted request
    // todo: Grpc,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionFfiVersion {
    V1,
}

/// Input/output serialization for module and host functions.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionFfiSerialization {
    FlexBufferVector,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ActionFfi {
    pub version: ActionFfiVersion,
    pub serialization: ActionFfiSerialization,
}

/// Configuration parameters for Ordinary Actions.
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ActionConfig {
    /// Foreign function interface config
    pub ffi: ActionFfi,
    /// Unique index value for action.
    pub idx: u8,
    /// Action name. Must be unique.
    pub name: String,
    /// The source language the action is written in.
    pub lang: ActionLang,
    /// Relative path to the source directory for the action.
    pub dir_path: String,
    /// What to check the token fields against. If blank
    /// action is public.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub protected: Option<Check>,
    /// whether the storage interactions
    /// are executed under a single transaction.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub transactional: Option<bool>,
    /// Which Ordinary Application resources the action has access to.
    pub access: Vec<ActionAccessPermission>,
    /// Input definition for the action.
    pub accepts: Kind,
    /// Output definition for the action.
    pub returns: Kind,
    /// How this action is called (i.e side effect from DB/Auth,
    /// http API call, browser form submission, etc.)
    pub triggered_by: Vec<ActionTrigger>,

    /// Max duration for the action.
    ///
    /// Unit: seconds
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub timeout: Option<u16>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub cors: Option<HttpCors>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub wasm_opt: Option<WasmOpt>,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ErrorConfig {
    /// Refers to the error template by name.
    pub template: String,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum RuntimeMode {
    /// Application will run on the shared multithreaded
    /// tokio runtime.
    Shared,
    /// Application will run on a separate thread with its
    /// own single-threaded tokio runtime.
    SingleThreaded,
    /// Application will run on a separate thread with its
    /// own multithreaded tokio runtime.
    MultiThreaded,
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsAllowHeaders {
    Any,
    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers>
    Headers(Vec<String>),
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsExposeHeaders {
    Any,
    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers>
    Headers(Vec<String>),
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsAllowMethods {
    Any,
    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods>
    Methods(Vec<String>),
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsAllowOrigin {
    Any,
    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin>
    Origins(Vec<String>),
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct HttpCors {
    pub allow_credentials: Option<bool>,

    pub allow_headers: Option<HttpCorsAllowHeaders>,

    /// unit: Seconds
    pub max_age: Option<u32>,

    pub allow_methods: Option<HttpCorsAllowMethods>,

    pub allow_origin: Option<HttpCorsAllowOrigin>,

    pub expose_headers: Option<HttpCorsExposeHeaders>,

    pub allow_private_network: Option<bool>,
}

impl HttpCors {
    #[must_use]
    pub fn overwrite(&self, base: &Self) -> Self {
        Self {
            allow_credentials: if self.allow_credentials.is_none() {
                base.allow_credentials
            } else {
                self.allow_credentials
            },
            allow_headers: if self.allow_headers.is_none() {
                base.allow_headers.clone()
            } else {
                self.allow_headers.clone()
            },
            max_age: if self.max_age.is_none() {
                base.max_age
            } else {
                self.max_age
            },
            allow_methods: if self.allow_methods.is_none() {
                base.allow_methods.clone()
            } else {
                self.allow_methods.clone()
            },
            allow_origin: if self.allow_origin.is_none() {
                base.allow_origin.clone()
            } else {
                self.allow_origin.clone()
            },
            expose_headers: if self.expose_headers.is_none() {
                base.expose_headers.clone()
            } else {
                self.expose_headers.clone()
            },
            allow_private_network: if self.allow_private_network.is_none() {
                base.allow_private_network
            } else {
                self.allow_private_network
            },
        }
    }
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct HttpCsp {
    /// defaults to `'self'` (`default-src ` does not need to be included)
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub default_src: Option<String>,

    /// defaults to unset unless inline hashes are included,
    /// in which case the directive will start with `'self'` (`script-src ` does not need to be included).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub script_src: Option<String>,

    /// defaults to unset unless inline hashes are included,
    /// in which case the directive will start with `'self'` (`style-src ` does not need to be included).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub style_src: Option<String>,

    /// defaults to unset.
    ///
    /// (`font-src ` does not need to be included).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub font_src: Option<String>,

    /// defaults to unset.
    ///
    /// (`img-src ` does not need to be included).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub img_src: Option<String>,

    /// defaults to `true`.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub include_inline_hashes: Option<bool>,
}

impl HttpCsp {
    #[must_use]
    pub fn build_string(
        &self,
        base: &Self,
        inline_style_hashes: Option<Vec<String>>,
        inline_script_hashes: Option<Vec<String>>,
        secure: bool,
    ) -> String {
        let mut out = String::new();

        let include_inline_hashes = self
            .include_inline_hashes
            .unwrap_or(base.include_inline_hashes.unwrap_or(true));

        let default_src = self
            .default_src
            .clone()
            .unwrap_or(base.default_src.clone().unwrap_or("'self'".to_string()));

        if !default_src.is_empty() {
            out.push_str("default-src ");
            out.push_str(default_src.as_str());
            out.push_str("; ");
        }

        let mut script_src = self
            .script_src
            .clone()
            .unwrap_or(base.script_src.clone().unwrap_or_default());

        if include_inline_hashes
            && let Some(script_hashes) = inline_script_hashes
            && !script_hashes.is_empty()
        {
            if script_src.is_empty() {
                script_src.push_str("'self'");
            }

            for hash in script_hashes {
                script_src.push_str(" '");
                script_src.push_str(hash.as_str());
                script_src.push('\'');
            }
        }

        if !script_src.is_empty() {
            out.push_str("script-src ");
            out.push_str(script_src.as_str());
            out.push_str("; ");
        }

        let mut style_src = self
            .style_src
            .clone()
            .unwrap_or(base.style_src.clone().unwrap_or_default());

        if include_inline_hashes
            && let Some(style_hashes) = inline_style_hashes
            && !style_hashes.is_empty()
        {
            if style_src.is_empty() {
                style_src.push_str("'self'");
            }

            for hash in style_hashes {
                style_src.push_str(" '");
                style_src.push_str(hash.as_str());
                style_src.push('\'');
            }
        }

        if !style_src.is_empty() {
            out.push_str("style-src ");
            out.push_str(style_src.as_str());
            out.push_str("; ");
        }

        let font_src = self
            .font_src
            .clone()
            .unwrap_or(base.font_src.clone().unwrap_or_default());
        if !font_src.is_empty() {
            out.push_str("font-src ");
            out.push_str(font_src.as_str());
            out.push_str("; ");
        }

        let img_src = self
            .img_src
            .clone()
            .unwrap_or(base.img_src.clone().unwrap_or_default());
        if !img_src.is_empty() {
            out.push_str("img-src ");
            out.push_str(img_src.as_str());
            out.push_str("; ");
        }

        if secure {
            out.push_str("upgrade-insecure-requests; ");
        }

        out.push_str("report-to csp");

        out = out.trim().to_string();
        out
    }
}

/// Config definition for an Ordinary Application
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct OrdinaryConfig {
    /// Domain name for the application to be run from the
    /// deployment environment.
    pub domain: String,

    /// additional domains with a CNAME record
    /// pointing at the primary `OrdinaryConfig::domain`.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub cnames: Option<Vec<String>>,

    /// list of email addresses that can be used to contact
    /// the application owner or administrators.
    pub contacts: Vec<String>,
    /// Version of the site build.
    pub version: String,

    /// Storage size in bytes (rounded up to nearest OS page size).
    pub storage_size: u64,

    /// Default request timeout.
    ///
    /// Unit (seconds).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub default_timeout: Option<u16>,

    /// HTTP Content Security Policy configuration.
    ///
    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP>
    ///
    /// "Base" defaults to `default-src 'self';` and tacks on SHA-256 integrity
    /// hashes for all inlined scripts and styles (generated at build time) to
    /// `script-src 'self' sha256-b64` and `style-src 'self' sha256-b64`, respectively.
    ///
    /// `https:` is used when not running in `--insecure` mode.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub csp: Option<HttpCsp>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub cors: Option<HttpCors>,

    /// Specifies runtime mode for application on the host.
    ///
    /// If none is specified, defaults to Shared (or host default).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub runtime: Option<RuntimeMode>,

    /// When set to true, `{{ domain }}/.ordinary/schema`
    /// is not addressable.
    ///
    /// Note: this can break applications which depend on
    /// flags, and `action`/`template` query descriptors.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub hide_schema: Option<bool>,

    /// Include template rendering code in the client WASM.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub client_rendering: Option<bool>,

    /// Include E2EE handler code in the client WASM.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub obfuscation: Option<bool>,

    /// Include E2EE handler code in the client WASM.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub client_events: Option<bool>,

    /// Port to be used for standalone "run" instances.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub port: Option<u16>,
    /// port used for redirecting from http when
    /// standalone is running in secure mode.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub redirect_port: Option<u16>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub logging: Option<LoggingConfig>,
    /// Configures error handling.
    ///
    /// Note: If not included just the error message will be
    /// sent back as text.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub error: Option<ErrorConfig>,
    /// Auth config for the Ordinary application.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub auth: Option<AuthConfig>,
    /// Global constants that can be accessed from templates
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub globals: Option<Vec<Global>>,
    /// Secrets that can be used by integrations.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub secrets: Option<Vec<Secret>>,
    /// Feature flags which can be referenced from templates
    /// to inform application behavior, and run experiments.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub flags: Option<Vec<Flag>>,
    /// Definitions for static content "types"/object structure
    /// that can be used to inform template/page development (i.e
    /// one might define a "post" content definition, and then create
    /// a template for their blog).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub content: Option<Content>,
    /// Definitions for the models that will be stored in the Ordinary database.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub models: Option<Vec<ModelConfig>>,
    /// Definitions for the external APIs that will be integrated
    /// into the Ordinary application.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub integrations: Option<Vec<IntegrationConfig>>,
    /// IO, access and language configuration for actions that
    /// are compiled to and executed as WebAssembly modules.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub actions: Option<Vec<ActionConfig>>,
    /// Specifies the asset directory and per-path configuration
    /// details for assets that require preprocessing (TypeScript, SCSS,
    /// JavaScript minification, etc.)
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub assets: Option<AssetsConfig>,
    /// Configuration for the templates/pages that the application
    /// will render. Each template is compiled to a WebAssembly module
    /// which accepts runtime arguments for models/content/integrations, and can be
    /// executed on either the server or the client.
    ///
    /// With the option to render on the client, only the result of the server
    /// query needs to be sent, in a compact, optimized, format.
    ///
    /// Currently, all rendering is happening server-side, and only HTML is being sent.
    ///
    /// In an ideal/future state multiple modes will be supported, even up to a full
    /// 'noscript' config.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub templates: Option<Vec<TemplateConfig>>,
}

impl OrdinaryConfig {
    #[allow(clippy::too_many_lines)]
    pub fn check_config_against_limits(
        &self,
        limits: &OrdinaryApiLimits,
    ) -> Result<(), Box<dyn Error>> {
        if let Some(default_timeout) = self.default_timeout
            && default_timeout > limits.max_default_timeout
        {
            return Err(format!(
                "default timeout {} greater than limit {}",
                default_timeout, limits.max_default_timeout
            )
            .into());
        }

        if let Some(templates) = &self.templates {
            if templates.len() > limits.template.count as usize {
                return Err(format!(
                    "template count {} greater than limit {}",
                    templates.len(),
                    limits.template.count
                )
                .into());
            }

            for template in templates {
                if let Some(timeout) = template.timeout
                    && timeout > limits.template.max_timeout
                {
                    return Err(format!(
                        "template timeout {} greater than limit {} for {}",
                        timeout, limits.template.max_timeout, template.name
                    )
                    .into());
                }

                if let Some(cache) = &template.cache
                    && let Some(stored_cache) = &cache.stored
                {
                    if let Some(max_size) = stored_cache.max_size
                        && (max_size < limits.storage.cache.max_size_range.0
                            || max_size > limits.storage.cache.max_size_range.1)
                    {
                        return Err(format!(
                            "template cache max_size {} is not within range [{}, {}] for {}",
                            max_size,
                            limits.storage.cache.max_size_range.0,
                            limits.storage.cache.max_size_range.1,
                            template.name
                        )
                        .into());
                    }

                    if let Some(max_count) = stored_cache.max_count
                        && (max_count < limits.storage.cache.max_count_range.0
                            || max_count > limits.storage.cache.max_count_range.1)
                    {
                        return Err(format!(
                            "template cache max_count {} is not within range [{}, {}] for {}",
                            max_count,
                            limits.storage.cache.max_count_range.0,
                            limits.storage.cache.max_count_range.1,
                            template.name
                        )
                        .into());
                    }

                    if let Some((sync_interval_min, sync_interval_max)) = stored_cache.sync_interval
                    {
                        if sync_interval_min < limits.storage.cache.sync_interval_ranges.0.0
                            || sync_interval_min > limits.storage.cache.sync_interval_ranges.0.1
                        {
                            return Err(format!(
                                "template cache sync_interval min {} is not within range [{}, {}] for {}",
                                sync_interval_min,
                                limits.storage.cache.sync_interval_ranges.0.0,
                                limits.storage.cache.sync_interval_ranges.0.1,
                                template.name
                            )
                                .into());
                        }

                        if sync_interval_max < limits.storage.cache.sync_interval_ranges.1.0
                            || sync_interval_max > limits.storage.cache.sync_interval_ranges.1.1
                        {
                            return Err(format!(
                                "template cache sync_interval min {} is not within range [{}, {}] for {}",
                                sync_interval_max,
                                limits.storage.cache.sync_interval_ranges.1.0,
                                limits.storage.cache.sync_interval_ranges.1.1,
                                template.name
                            )
                                .into());
                        }
                    }

                    if let Some((clean_interval_min, clean_interval_max)) =
                        stored_cache.clean_interval
                    {
                        if clean_interval_min < limits.storage.cache.clean_interval_ranges.0.0
                            || clean_interval_min > limits.storage.cache.clean_interval_ranges.0.1
                        {
                            return Err(format!(
                                "template cache clean_interval min {} is not within range [{}, {}] for {}",
                                clean_interval_min,
                                limits.storage.cache.clean_interval_ranges.0.0,
                                limits.storage.cache.clean_interval_ranges.0.1,
                                template.name
                            )
                                .into());
                        }

                        if clean_interval_max < limits.storage.cache.clean_interval_ranges.1.0
                            || clean_interval_max > limits.storage.cache.clean_interval_ranges.1.1
                        {
                            return Err(format!(
                                "template cache clean_interval min {} is not within range [{}, {}] for {}",
                                clean_interval_max,
                                limits.storage.cache.clean_interval_ranges.1.0,
                                limits.storage.cache.clean_interval_ranges.1.1,
                                template.name
                            )
                                .into());
                        }
                    }
                }
            }
        }

        if let Some(integrations) = &self.integrations {
            if integrations.len() > limits.integration.count as usize {
                return Err(format!(
                    "integration count {} greater than limit {}",
                    integrations.len(),
                    limits.integration.count
                )
                .into());
            }

            for integration in integrations {
                if let Some(timeout) = integration.timeout
                    && timeout > limits.integration.max_timeout
                {
                    return Err(format!(
                        "integration timeout {} greater than limit {} for {}",
                        timeout, limits.integration.max_timeout, integration.name,
                    )
                    .into());
                }
            }
        }

        if let Some(actions) = &self.actions {
            if actions.len() > limits.action.count as usize {
                return Err(format!(
                    "action count {} greater than limit {}",
                    actions.len(),
                    limits.action.count
                )
                .into());
            }

            for action in actions {
                if let Some(timeout) = action.timeout
                    && timeout > limits.action.max_timeout
                {
                    return Err(format!(
                        "action timeout {} greater than limit {} for {}",
                        timeout, limits.action.max_timeout, action.name
                    )
                    .into());
                }
            }
        }

        if self.storage_size > limits.storage.max_app_storage {
            return Err("storage size is greater than limit".into());
        }

        if let Some(content) = &self.content {
            if content.definitions.len() > limits.storage.content.max_content_definitions as usize {
                return Err(format!(
                    "content definition count {} greater than limit {}",
                    content.definitions.len(),
                    limits.storage.content.max_content_definitions
                )
                .into());
            }

            for content_def in &content.definitions {
                if content_def.fields.len() > limits.storage.content.max_content_fields as usize {
                    return Err(format!(
                        "content field count {} greater than limit {} for {}",
                        content_def.fields.len(),
                        limits.storage.content.max_content_fields,
                        content_def.name,
                    )
                    .into());
                }

                for field in &content_def.fields {
                    if field.searchable == Some(true) && !limits.storage.content.search_enabled {
                        return Err(format!(
                            "content field cannot be 'searchable' for field {} on definition {}",
                            field.name, content_def.name,
                        )
                        .into());
                    }
                }
            }
        }

        if let Some(models) = &self.models {
            if models.len() > limits.storage.model.max_model_definitions as usize {
                return Err(format!(
                    "model definition count {} greater than limit {}",
                    models.len(),
                    limits.storage.model.max_model_definitions
                )
                .into());
            }

            for model in models {
                if model.fields.len() > limits.storage.model.max_model_fields as usize {
                    return Err(format!(
                        "model field count {} greater than limit {} for {}",
                        model.fields.len(),
                        limits.storage.content.max_content_fields,
                        model.name,
                    )
                    .into());
                }

                for field in &model.fields {
                    if field.searchable == Some(true) && !limits.storage.content.search_enabled {
                        return Err(format!(
                            "content field cannot be 'searchable' for field {} on definition {}",
                            field.name, model.name,
                        )
                        .into());
                    }
                }
            }
        }

        Ok(())
    }
}
