//! Admin-set ceiling for per-script sandbox overrides.
//!
//! The orchestrator-core's default `Limits` is what scripts get when they
//! don't override. The ceiling here is the per-field maximum that a
//! script's override is allowed to request. Validation runs at write
//! time so the executor can trust whatever's stored.

use std::env;

use picloud_shared::ScriptSandbox;
use thiserror::Error;

/// Maximum allowed value per sandbox knob. Loaded from env vars at
/// startup (with conservative built-in defaults). A `None` field means
/// "unbounded" — only useful if the operator explicitly clears the
/// ceiling for a given knob (it must still fit `u64`).
#[derive(Debug, Clone, Copy)]
pub struct SandboxCeiling {
    pub max_operations: u64,
    pub max_string_size: u64,
    pub max_array_size: u64,
    pub max_map_size: u64,
    pub max_call_levels: u64,
    pub max_expr_depth: u64,
}

impl SandboxCeiling {
    /// Conservative built-in ceiling. Matches the executor's defaults —
    /// scripts can request anything between zero and this, but no
    /// higher. Operators can widen via env vars (see `from_env`).
    #[must_use]
    pub const fn conservative() -> Self {
        Self {
            max_operations: 10_000_000,
            max_string_size: 1024 * 1024, // 1 MiB
            max_array_size: 100_000,
            max_map_size: 100_000,
            max_call_levels: 128,
            max_expr_depth: 128,
        }
    }

    /// Read overrides from env vars, falling back to `conservative()`
    /// for any unset knob. Invalid values are ignored with a warning
    /// (we'd rather start with the conservative default than refuse to
    /// boot on a typo).
    #[must_use]
    pub fn from_env() -> Self {
        let mut c = Self::conservative();
        macro_rules! load {
            ($field:ident, $key:expr) => {
                if let Ok(v) = env::var($key) {
                    match v.parse::<u64>() {
                        Ok(n) => c.$field = n,
                        Err(e) => tracing::warn!(env = $key, error = %e, "ignoring invalid sandbox ceiling value"),
                    }
                }
            };
        }
        load!(max_operations, "PICLOUD_SANDBOX_MAX_OPERATIONS");
        load!(max_string_size, "PICLOUD_SANDBOX_MAX_STRING_SIZE");
        load!(max_array_size, "PICLOUD_SANDBOX_MAX_ARRAY_SIZE");
        load!(max_map_size, "PICLOUD_SANDBOX_MAX_MAP_SIZE");
        load!(max_call_levels, "PICLOUD_SANDBOX_MAX_CALL_LEVELS");
        load!(max_expr_depth, "PICLOUD_SANDBOX_MAX_EXPR_DEPTH");
        c
    }

    /// Returns `Err` if any override exceeds the ceiling on the same
    /// field. Empty overrides (`ScriptSandbox::empty()`) always pass.
    pub fn check(&self, s: &ScriptSandbox) -> Result<(), CeilingError> {
        check_field("max_operations", s.max_operations, self.max_operations)?;
        check_field("max_string_size", s.max_string_size, self.max_string_size)?;
        check_field("max_array_size", s.max_array_size, self.max_array_size)?;
        check_field("max_map_size", s.max_map_size, self.max_map_size)?;
        check_field("max_call_levels", s.max_call_levels, self.max_call_levels)?;
        check_field("max_expr_depth", s.max_expr_depth, self.max_expr_depth)?;
        Ok(())
    }
}

fn check_field(name: &'static str, value: Option<u64>, ceiling: u64) -> Result<(), CeilingError> {
    if let Some(v) = value {
        if v > ceiling {
            return Err(CeilingError::Exceeded {
                field: name,
                requested: v,
                ceiling,
            });
        }
    }
    Ok(())
}

#[derive(Debug, Error, Clone)]
pub enum CeilingError {
    #[error("sandbox override `{field}` = {requested} exceeds admin ceiling of {ceiling}")]
    Exceeded {
        field: &'static str,
        requested: u64,
        ceiling: u64,
    },
}
