use picloud_shared::ScriptSandbox;

/// Resource and capability limits applied to every script execution.
///
/// Defaults are conservative and safe to expose to untrusted Rhai sources.
/// Per-script overrides (via `Limits::with_overrides`) replace individual
/// fields; the manager clamps every override against the admin ceiling at
/// write time, so the executor trusts what arrives in `ExecRequest`.
#[derive(Debug, Clone, Copy)]
pub struct Limits {
    /// Hard cap on Rhai operations executed per invocation.
    /// Doubles as a CPU-time proxy without needing real timers.
    pub max_operations: u64,

    /// Max length of any single string the script constructs.
    pub max_string_size: usize,

    /// Max number of elements in any array.
    pub max_array_size: usize,

    /// Max number of properties in any object/map.
    pub max_map_size: usize,

    /// Max call/expression nesting depth.
    pub max_call_levels: usize,
    pub max_expr_depth: usize,

    /// v1.1.3: hard ceiling on `import` chain depth (A→B→C→…). Independent
    /// of cycle detection — guards against deep but acyclic graphs.
    /// Not script-overridable (this is a platform-level guard, not a
    /// per-script knob).
    pub module_import_depth_max: u32,

    /// v1.1.9: hard ceiling on `cx.trigger_depth` (shared between
    /// trigger fan-out and `invoke()` re-entry). The dispatcher uses
    /// `TriggerConfig::max_trigger_depth` for the SAME bound; the
    /// invoke bridge uses this Limits-side mirror to short-circuit
    /// before a DB round-trip. Defaults to 8 (matches
    /// TriggerConfig::conservative); the picloud binary keeps the two
    /// in sync by passing `TriggerConfig::from_env().max_trigger_depth`
    /// through.
    pub trigger_depth_max: u32,
}

impl Default for Limits {
    fn default() -> Self {
        Self {
            max_operations: 1_000_000,
            max_string_size: 64 * 1024,
            max_array_size: 10_000,
            max_map_size: 10_000,
            max_call_levels: 64,
            max_expr_depth: 64,
            module_import_depth_max: 8,
            trigger_depth_max: 8,
        }
    }
}

impl Limits {
    /// Returns a new `Limits` with each field replaced by the matching
    /// override if present, otherwise the existing field. Overrides
    /// arrive as `u64` for JSONB round-tripping cleanliness; they're
    /// narrowed to `usize` here, saturating on the unlikely overflow
    /// (these caps come from admin-clamped writes, so the values are
    /// always small).
    #[must_use]
    pub fn with_overrides(&self, overrides: &ScriptSandbox) -> Self {
        Self {
            max_operations: overrides.max_operations.unwrap_or(self.max_operations),
            max_string_size: overrides
                .max_string_size
                .map_or(self.max_string_size, narrow_usize),
            max_array_size: overrides
                .max_array_size
                .map_or(self.max_array_size, narrow_usize),
            max_map_size: overrides
                .max_map_size
                .map_or(self.max_map_size, narrow_usize),
            max_call_levels: overrides
                .max_call_levels
                .map_or(self.max_call_levels, narrow_usize),
            max_expr_depth: overrides
                .max_expr_depth
                .map_or(self.max_expr_depth, narrow_usize),
            // module_import_depth_max is platform-level — overrides
            // never touch it. Carry through unchanged.
            module_import_depth_max: self.module_import_depth_max,
            // trigger_depth_max is also platform-level (v1.1.9 invoke
            // depth bound mirrors the dispatcher's trigger-depth cap).
            trigger_depth_max: self.trigger_depth_max,
        }
    }
}

fn narrow_usize(v: u64) -> usize {
    usize::try_from(v).unwrap_or(usize::MAX)
}
