//! `InterceptorServiceImpl` — the Postgres-backed §9.4 interceptor resolver
//! injected into `Services`. It maps `(cx.app_id, service, op)` to the nearest
//! interceptor marker on the calling app's chain and materializes its script
//! **sealed to the declaring owner** (via [`crate::interceptor_repo`]). Running
//! that script is the executor's job (the `invoke()` re-entry core), which keeps
//! `executor-core` Postgres-free.

use async_trait::async_trait;
use picloud_shared::{
    InterceptorChain, InterceptorEntry, InterceptorService, ResolvedInterceptor, ResolvedScript,
    ScriptId, SdkCallCx,
};
use sqlx::PgPool;

use crate::interceptor_repo::SealedInterceptor;

pub struct InterceptorServiceImpl {
    pool: PgPool,
}

impl InterceptorServiceImpl {
    #[must_use]
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }
}

impl InterceptorServiceImpl {
    /// Map one sealed marker to a chain entry: a runnable [`ResolvedScript`]
    /// (sealed to the declaring owner — the lexical `import` origin, §5.5) or a
    /// [`Dangling`](InterceptorEntry::Dangling) entry when the marker's script
    /// is missing/disabled. The execution boundary is always the CALLER's app
    /// (`cx.app_id`); only the `owner` is the sealing owner.
    fn map_entry(app_id: picloud_shared::AppId, sealed: SealedInterceptor) -> InterceptorEntry {
        let owner = sealed.sealing_owner();
        // A negative/oversized DB value is impossible (CHECK > 0), but clamp
        // defensively to `None` (→ instance default) rather than panic.
        let timeout_ms = sealed.timeout_ms.and_then(|v| u32::try_from(v).ok());
        match sealed.script {
            Some((sid, source, updated_at)) => InterceptorEntry::Run(ResolvedInterceptor {
                script: Box::new(ResolvedScript {
                    script_id: ScriptId::from(sid),
                    app_id,
                    owner: Some(owner),
                    source,
                    updated_at,
                    name: sealed.script_name,
                }),
                timeout_ms,
            }),
            None => InterceptorEntry::Dangling(sealed.script_name),
        }
    }
}

#[async_trait]
impl InterceptorService for InterceptorServiceImpl {
    async fn resolve(
        &self,
        cx: &SdkCallCx,
        service: &str,
        op: &str,
    ) -> Result<InterceptorChain, String> {
        // `app_id` derives from `cx` (never a script arg) — the isolation
        // boundary; the chain walk then scopes resolution to this app's subtree.
        let chain = crate::interceptor_repo::resolve_chain(&self.pool, cx.app_id, service, op)
            .await
            .map_err(|e| e.to_string())?;
        Ok(InterceptorChain {
            before: chain
                .before
                .into_iter()
                .map(|s| Self::map_entry(cx.app_id, s))
                .collect(),
            after: chain
                .after
                .into_iter()
                .map(|s| Self::map_entry(cx.app_id, s))
                .collect(),
        })
    }
}
