//! `vars::*` — read-only access from scripts to the app's resolved
//! configuration (Phase 3). Values are inherited down the group tree and
//! env-filtered per the §3 resolution rule; the resolution happens in
//! manager-core (`config_resolver`). Writes go through the admin API, not
//! the SDK — scripts only read.

use std::collections::BTreeMap;

use async_trait::async_trait;
use serde_json::Value;

use crate::SdkCallCx;

#[derive(Debug, thiserror::Error)]
pub enum VarsError {
    /// Caller principal lacked `AppVarsRead`. Only raised when
    /// `cx.principal.is_some()` (public-HTTP scripts skip the check —
    /// script-as-gate).
    #[error("forbidden")]
    Forbidden,
    /// Postgres unavailable, malformed row, etc.
    #[error("vars backend error: {0}")]
    Backend(String),
}

#[async_trait]
pub trait VarsService: Send + Sync {
    /// Resolve a single config key for the calling app's environment.
    /// `None` if no level defines it (or a tombstone suppresses it).
    async fn get(&self, cx: &SdkCallCx, key: &str) -> Result<Option<Value>, VarsError>;

    /// The app's fully-resolved config map (every inherited + own key).
    async fn all(&self, cx: &SdkCallCx) -> Result<BTreeMap<String, Value>, VarsError>;
}

/// All-noop fallback for engines that don't wire vars (tests). Every call
/// surfaces an explicit error rather than silently returning empty.
pub struct NoopVarsService;

#[async_trait]
impl VarsService for NoopVarsService {
    async fn get(&self, _cx: &SdkCallCx, _key: &str) -> Result<Option<Value>, VarsError> {
        Err(VarsError::Backend("vars is not wired in".into()))
    }
    async fn all(&self, _cx: &SdkCallCx) -> Result<BTreeMap<String, Value>, VarsError> {
        Err(VarsError::Backend("vars is not wired in".into()))
    }
}
