//! §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};

/// One resolved, runnable interceptor: a script **sealed to the owner that
/// declared the marker** (its lexical `import` origin), plus its per-hook
/// timeout. `timeout_ms` is threaded through the chain now but unused until
/// M5 — always `None` for the M2 slice.
#[derive(Debug, Clone)]
pub struct ResolvedInterceptor {
    pub script: Box<ResolvedScript>,
    pub timeout_ms: Option<u32>,
}

/// One entry in an interceptor chain. A **fail-closed** contract: a
/// registered-but-broken hook is a [`Dangling`](Self::Dangling) entry the
/// caller must DENY on — it is never dropped in favour of a farther, weaker
/// marker.
#[derive(Debug, Clone)]
pub enum InterceptorEntry {
    /// Run this script as a before/after hook.
    Run(ResolvedInterceptor),
    /// A marker exists but its sealed script is missing/disabled → the caller
    /// DENIES (a mis-declared guard must never fail open). Carries the script
    /// name for the error message.
    Dangling(String),
}

/// The ordered before + after interceptors guarding a `(service, op)` for the
/// calling app. `before` runs ancestor→app (an outer/group guard runs first,
/// so a group denial cannot be bypassed by a descendant); `after` runs
/// app→ancestor. An empty chain means un-hooked (allow, no work). `after` is
/// populated but UNUSED until §9.4 M3.
#[derive(Debug, Clone, Default)]
pub struct InterceptorChain {
    pub before: Vec<InterceptorEntry>,
    pub after: Vec<InterceptorEntry>,
}

#[async_trait]
pub trait InterceptorService: Send + Sync {
    /// Resolve the before + after interceptors guarding `(service, op)` for the
    /// calling app — every marker visible on `cx.app_id`'s chain, each script
    /// sealed to the owner that declared it, ordered per phase. `Err` is a
    /// backend failure (the caller fails closed: turns it into an operation
    /// error rather than silently allowing).
    async fn resolve(
        &self,
        cx: &SdkCallCx,
        service: &str,
        op: &str,
    ) -> Result<InterceptorChain, 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(
        &self,
        _cx: &SdkCallCx,
        _service: &str,
        _op: &str,
    ) -> Result<InterceptorChain, String> {
        Ok(InterceptorChain::default())
    }
}
