#[cfg(feature = "governor")]
use std::num::NonZero;

#[cfg(feature = "cf-worker")]
#[allow(unsafe_code)]
mod json {
    use wasm_bindgen::prelude::*;
    #[wasm_bindgen]
    extern "C" {
        #[wasm_bindgen(js_namespace = JSON)]
        pub fn stringify(value: &JsValue) -> String;
    }
}

/// Application configuration
#[derive(Debug, Clone, serde::Deserialize)]
pub struct Config {
    /// The listen address
    pub listen: Option<String>,

    /// Sandbox configuration
    #[serde(default)]
    pub sandbox: SandboxConfig,

    /// Send Cache-Control headers
    pub enable_cache: bool,

    /// Index page configuration
    pub index_redirect: IndexConfig,

    /// Whether to only serve media with a known safe signature
    pub allow_unknown: bool,

    /// Fetch configuration
    pub fetch: FetchConfig,

    /// Post-processing configuration
    pub post_process: PostProcessConfig,

    #[cfg(not(feature = "cf-worker"))]
    /// The maximum number of X-Forwarded-For headers to allow
    pub max_x_forwarded_for: u8,

    #[cfg(feature = "governor")]
    /// Governor configuration
    pub rate_limit: Vec<RateLimitConfig>,
}

/// Sandbox configuration
#[derive(Debug, Clone, serde::Deserialize)]
#[non_exhaustive]
pub enum SandboxConfig {
    /// No sandboxing
    #[serde(rename = "none")]
    NoSandbox,
    /// AppArmor sandboxing
    #[cfg(feature = "apparmor")]
    #[serde(rename = "apparmor")]
    AppArmor(AppArmorConfig),
}

impl Default for SandboxConfig {
    fn default() -> Self {
        Self::NoSandbox
    }
}

/// AppArmor configuration
#[cfg(feature = "apparmor")]
#[derive(Debug, Clone, serde::Deserialize)]
pub struct AppArmorConfig {
    /// The profile to transition to after initialization is complete
    pub serve: String,
    /// The AppArmor hat to use when processing media
    pub image_hat: String,
}

/// Governor configuration
#[cfg(feature = "governor")]
#[derive(Debug, Clone, serde::Deserialize)]
pub struct RateLimitConfig {
    /// The key to use for rate limiting headers
    pub key: Option<String>,
    /// The rate limit replenish interval in milliseconds
    pub replenish_every: u64,
    /// The rate limit burst size
    pub burst: NonZero<u32>,

    /// The minimum request duration in milliseconds for this rate limit to apply
    pub min_request_duration: Option<u64>,
}

#[cfg(feature = "cf-worker")]
#[derive(Debug, thiserror::Error)]
/// Configuration error
pub enum CfConfigError {
    #[error("Failed to convert env")]
    /// Failed to convert the environment to a JS object
    EnvConversion,
    #[error("Failed to parse JSON")]
    /// Failed to parse JSON
    JsonParse(#[from] serde_json::Error),
}

#[cfg(feature = "cf-worker")]
impl Config {
    /// Load the configuration from the Cloudflare Worker environment
    pub fn load_from_cf_env(env: worker::Env) -> Result<Self, CfConfigError> {
        use wasm_bindgen::JsCast;
        let obj = env.dyn_into().map_err(|_| CfConfigError::EnvConversion)?;
        let json = json::stringify(&obj);
        serde_json::from_str(&json).map_err(CfConfigError::JsonParse)
    }
}

impl Default for Config {
    fn default() -> Self {
        Config {
            listen: Some("127.0.0.1:3000".to_string()),
            enable_cache: false,
            sandbox: SandboxConfig::default(),
            fetch: FetchConfig {
                #[cfg(not(feature = "cf-worker"))]
                addr_family: AddrFamilyConfig::Both,
                allow_http: false,
                via: concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")).to_string(),
                user_agent: concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))
                    .to_string(),
            },
            index_redirect: IndexConfig::Message(format!(
                "Welcome to {}",
                concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
            )),
            allow_unknown: false,
            post_process: PostProcessConfig {
                enable_redirects: false,
                normalization: NormalizationPolicy::Opportunistic,
                allow_svg_passthrough: false,
            },
            #[cfg(not(feature = "cf-worker"))]
            max_x_forwarded_for: 0,
            #[cfg(feature = "governor")]
            rate_limit: vec![RateLimitConfig {
                key: None,
                replenish_every: 2000,
                burst: NonZero::new(32).unwrap(),
                min_request_duration: None,
            }],
        }
    }
}

/// Fetch configuration
#[derive(Debug, Clone, serde::Deserialize)]
pub struct FetchConfig {
    /// The address family to use
    #[cfg(not(feature = "cf-worker"))]
    #[serde(default)]
    pub addr_family: AddrFamilyConfig,

    /// Whether to allow HTTP requests
    pub allow_http: bool,
    /// The via string to use when fetching media
    pub via: String,
    /// The user agent to use when fetching media
    pub user_agent: String,
}

/// Address family configuration
#[cfg(not(feature = "cf-worker"))]
#[derive(Debug, Clone, Copy, serde::Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AddrFamilyConfig {
    /// Prefer IPv4
    V4Only,
    /// Prefer IPv6
    V6Only,
    /// Use both IPv4 and IPv6
    Both,
}

#[cfg(not(feature = "cf-worker"))]
impl Default for AddrFamilyConfig {
    fn default() -> Self {
        Self::Both
    }
}

/// Post-processing configuration
#[derive(Debug, Clone, serde::Deserialize)]
pub struct PostProcessConfig {
    /// Opportunistically redirect to the original URL if the media is not modified
    ///
    /// Potentially leaks the user's IP address and other metadata
    pub enable_redirects: bool,
    /// Whether to normalize media files when the request specifically asks for a format
    pub normalization: NormalizationPolicy,
    /// Whether to allow SVG passthrough
    ///
    /// This opens up the possibility of SVG-based attacks
    pub allow_svg_passthrough: bool,
}

/// Normalization policy
#[derive(Copy, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NormalizationPolicy {
    /// Ignore the requested format and return the original
    Never,
    /// Don't convert the media if we don't have to
    ///
    ///
    /// This is the default for Cloudflare Workers
    Lazy,
    /// Only return the requested format if other conversions are necessary
    ///
    /// This is the default for local environments
    Opportunistic,
    /// Always make an attempt to return the requested format
    Aggressive,
}

impl Default for NormalizationPolicy {
    fn default() -> Self {
        Self::Opportunistic
    }
}

#[derive(Debug, Clone, serde::Deserialize)]
#[serde(untagged)]
/// Index page configuration
pub enum IndexConfig {
    /// Redirect to a URL
    #[allow(missing_docs)]
    Redirect { permanent: bool, url: String },
    /// Display a message
    Message(String),
}