//! §9.4 Service Interceptors — the injected resolver seam.
//!
//! An interceptor is a script registered (declaratively, per app/group) to run
//! **before** a data-plane operation and allow or deny it. This trait is the
//! narrow, executor-facing seam: it RESOLVES the interceptor for a
//! `(service, op)` on the calling app's chain — the nearest MARKER wins
//! (nearest-owner, like extension points), and the marker's script is then
//! resolved **at the owner that declared it**. That seal is the compliance
//! guarantee: a descendant app cannot shadow a group's interceptor with a
//! same-named local script (contrast a bare `invoke()` name, which
//! nearest-owner-wins and *would* let the app win). Resolution returns a fully
//! materialized [`ResolvedScript`] so the executor runs it straight through the
//! existing `invoke()` re-entry core — no second dispatch mechanism, no re-
//! resolution by name, and `executor-core` stays Postgres-free.
//!
//! MVP scope (v1.2): `service = "kv"`, `op ∈ {set, delete}`, allow/deny only —
//! no data transform, no chaining, no `after_*` hooks. See blueprint §9.4.

use async_trait::async_trait;

use crate::{ResolvedScript, SdkCallCx};

/// Outcome of resolving the before-op interceptor for a `(service, op)`.
///
/// The three arms encode a **fail-closed** contract: a registered-but-broken
/// hook DENIES rather than silently allowing (only [`None`](Self::None) — no
/// marker at all — allows without running anything).
#[derive(Debug, Clone)]
pub enum InterceptorResolution {
    /// No marker registered for `(service, op)` on the chain — allow, no work.
    None,
    /// A marker IS registered but its sealed script is missing or disabled.
    /// The caller must DENY (a mis-declared guard must never fail open).
    /// Carries the script name for the error message.
    Dangling(String),
    /// Run this script (sealed to the declaring owner) as the before-op hook.
    Run(Box<ResolvedScript>),
}

#[async_trait]
pub trait InterceptorService: Send + Sync {
    /// Resolve the interceptor guarding `(service, op)` for the calling app —
    /// the nearest marker on `cx.app_id`'s chain, its script sealed to the
    /// declaring owner. `Err` is a backend failure (the caller fails closed:
    /// turns it into an operation error rather than silently allowing).
    async fn resolve_before(
        &self,
        cx: &SdkCallCx,
        service: &str,
        op: &str,
    ) -> Result<InterceptorResolution, String>;
}

/// Default: nothing is ever intercepted. The shape every non-picloud `Services`
/// (tests, cluster skeletons) gets for free — an un-hooked write pays exactly
/// one `Ok(None)` here, no I/O.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopInterceptorService;

#[async_trait]
impl InterceptorService for NoopInterceptorService {
    async fn resolve_before(
        &self,
        _cx: &SdkCallCx,
        _service: &str,
        _op: &str,
    ) -> Result<InterceptorResolution, String> {
        Ok(InterceptorResolution::None)
    }
}
