//! `Services` — bundle of stateful SDK service handles plumbed from the
//! host binary into every Rhai execution.
//!
//! Constructed once at startup in the picloud binary; cloned (cheap —
//! every field is an `Arc`) into the per-call sdk bridge so script
//! invocations don't need to re-resolve dependencies. The bundle is
//! handed to `executor-core::sdk::register_all` alongside an
//! `SdkCallCx` to wire each `::` namespace.
//!
//! v1.1.0 shipped this empty; v1.1.1 added the first two service fields
//! (`kv`, `dead_letters`) plus the `events` emitter that bound services
//! use to publish events into the triggers outbox. v1.1.3 adds the
//! `modules` field — the `ModuleSource` consulted by the per-call
//! `PicloudModuleResolver` to load `import`ed module scripts.
//!
//! `#[non_exhaustive]` so adding fields is a non-breaking change for
//! consumers that only *pattern-match* a `&Services`; only crates that
//! *construct* a `Services` (the picloud binary and tests) update.

use std::sync::Arc;

use crate::{
    DeadLetterService, DocsService, EmailService, FilesService, GroupDocsService,
    GroupFilesService, GroupKvService, GroupPubsubService, GroupQueueService, HttpService,
    InterceptorService, InvokeService, KvService, ModuleSource, NoopDeadLetterService,
    NoopDocsService, NoopEmailService, NoopEventEmitter, NoopFilesService, NoopGroupDocsService,
    NoopGroupFilesService, NoopGroupKvService, NoopGroupPubsubService, NoopGroupQueueService,
    NoopHttpService, NoopInterceptorService, NoopInvokeService, NoopKvService, NoopModuleSource,
    NoopPubsubService, NoopQueueService, NoopSecretsService, NoopUsersService, NoopVarsService,
    NoopWorkflowService, PubsubService, QueueService, SecretsService, ServiceEventEmitter,
    UsersService, VarsService, WorkflowService,
};

/// SDK service bundle. See module docs for the lifecycle and the v1.1.x
/// expansion plan.
#[non_exhaustive]
pub struct Services {
    /// KV store (v1.1.1). Backed by Postgres in the picloud binary;
    /// in-memory in tests.
    pub kv: Arc<dyn KvService>,

    /// Document store (v1.1.2). Backed by Postgres in the picloud
    /// binary; in-memory in tests.
    pub docs: Arc<dyn DocsService>,

    /// Dead-letter management (v1.1.1). Scripts get
    /// `dead_letters::replay(id)` and `dead_letters::resolve(id, reason)`.
    pub dead_letters: Arc<dyn DeadLetterService>,

    /// Event emitter for the triggers outbox. Mutating service methods
    /// (`KvService::set/delete`, `DocsService::create/update/delete`,
    /// future `files::*`, etc.) call `events.emit(cx, event)` after
    /// the write succeeds. The outbox-backed impl in
    /// `manager-core::outbox_event_emitter` replaces v1.1.0's
    /// `NoopEventEmitter`.
    pub events: Arc<dyn ServiceEventEmitter>,

    /// Module source (v1.1.3). The `PicloudModuleResolver` consults
    /// this to load `kind = 'module'` scripts that other scripts
    /// `import`. Backed by Postgres in the picloud binary; in-memory
    /// fakes in resolver tests.
    pub modules: Arc<dyn ModuleSource>,

    /// Outbound HTTP (v1.1.4). Scripts get `http::{get,post,…}`.
    /// Backed by a reqwest client with the SSRF deny-list resolver in
    /// the picloud binary; `NoopHttpService` in tests that don't make
    /// network calls.
    pub http: Arc<dyn HttpService>,

    /// Filesystem-backed blob storage (v1.1.5). Scripts get
    /// `files::collection(name).{create,head,get,update,delete,list}`.
    /// Backed by a Postgres-metadata + on-disk-bytes repo in the
    /// picloud binary; `NoopFilesService` in tests that don't touch
    /// files.
    pub files: Arc<dyn FilesService>,

    /// Durable pub/sub (v1.1.5). Scripts get
    /// `pubsub::publish_durable(topic, message)`. Backed by a
    /// publish-time outbox fan-out in the picloud binary;
    /// `NoopPubsubService` in tests that don't publish.
    pub pubsub: Arc<dyn PubsubService>,

    /// Encrypted per-app secrets (v1.1.7). Scripts get
    /// `secrets::{get,set,delete,list}(name)`. Backed by an
    /// AES-256-GCM-at-rest Postgres repo in the picloud binary;
    /// `NoopSecretsService` in tests that don't touch secrets.
    pub secrets: Arc<dyn SecretsService>,

    /// Outbound email (v1.1.7). Scripts get `email::{send,send_html}`.
    /// Backed by an SMTP relay (lettre) in the picloud binary;
    /// `NoopEmailService` (always `NotConfigured`) in tests that don't
    /// send mail.
    pub email: Arc<dyn EmailService>,

    /// Per-app data-plane user management (v1.1.8). Scripts get the
    /// full `users::*` namespace (CRUD, login/verify/logout, email
    /// verification, password reset, invitations, string-tagged
    /// roles). Backed by Postgres-stored Argon2id hashes + SHA-256
    /// session tokens in the picloud binary; `NoopUsersService` in
    /// tests that don't exercise users.
    pub users: Arc<dyn UsersService>,

    /// Durable per-app named queues (v1.1.9). Scripts get
    /// `queue::{enqueue,depth,depth_pending}`. Backed by Postgres in
    /// the picloud binary; `NoopQueueService` in tests that don't
    /// touch queues. Consumers register via `queue:receive` triggers
    /// (one per `(app_id, queue_name)`).
    pub queue: Arc<dyn QueueService>,

    /// Same-app function composition (v1.1.9). Backs `invoke()` (sync)
    /// and `invoke_async()` (fire-and-forget through the outbox).
    /// Cross-app invokes are rejected at the service entry point.
    pub invoke: Arc<dyn InvokeService>,

    /// Group-inherited, env-scoped config (Phase 3). Scripts get
    /// read-only `vars::{get,all}`; values resolve down the group tree
    /// (§3). Backed by `config_resolver` over Postgres in the picloud
    /// binary; `NoopVarsService` in tests that don't read config.
    pub vars: Arc<dyn VarsService>,

    /// Shared cross-app group collections (§11.6). Scripts get
    /// `kv::shared_collection(name)` — read/write KV scoped to the nearest ancestor
    /// group that declares the collection shared. Backed by Postgres in the
    /// picloud binary (wired via [`Services::with_group_kv`]); defaults to
    /// `NoopGroupKvService` so test bundles that don't exercise sharing build
    /// unchanged.
    pub group_kv: Arc<dyn GroupKvService>,

    /// Shared cross-app group DOCS collections (§11.6). Scripts get
    /// `docs::shared_collection(name)`. Wired via [`Services::with_group_docs`];
    /// defaults to `NoopGroupDocsService`.
    pub group_docs: Arc<dyn GroupDocsService>,

    /// Shared cross-app group FILES collections (§11.6). Scripts get
    /// `files::shared_collection(name)`. Wired via [`Services::with_group_files`];
    /// defaults to `NoopGroupFilesService`.
    pub group_files: Arc<dyn GroupFilesService>,

    /// Shared cross-app group TOPICS (§11.6 D2). Scripts get
    /// `pubsub::shared_topic(name)` — publish to a group-scoped topic watched by
    /// `shared = true` group pubsub triggers. Wired via
    /// [`Services::with_group_pubsub`]; defaults to `NoopGroupPubsubService`.
    pub group_pubsub: Arc<dyn GroupPubsubService>,

    /// Shared cross-app group QUEUES (§11.6 D3). Scripts get
    /// `queue::shared_collection(name)` — enqueue into a group-keyed store
    /// drained by competing per-descendant consumers. Wired via
    /// [`Services::with_group_queue`]; defaults to `NoopGroupQueueService`.
    pub group_queue: Arc<dyn GroupQueueService>,

    /// v1.2 Workflows (M5). Scripts get `workflow::start(name, input)` — start a
    /// run of a named workflow in the caller's app. Wired via
    /// [`Services::with_workflow`]; defaults to `NoopWorkflowService`.
    pub workflow: Arc<dyn WorkflowService>,

    /// §9.4 Service Interceptors — resolves which (if any) interceptor script
    /// guards a `(service, op)` before it runs. Wired via
    /// [`Services::with_interceptors`]; defaults to `NoopInterceptorService`
    /// (nothing intercepted).
    pub interceptors: Arc<dyn InterceptorService>,
}

impl Services {
    /// Construct a bundle from already-constructed `Arc<dyn …>` handles.
    /// The picloud binary's `main` wires this up after the DB pool is
    /// open; tests build it from in-memory fakes.
    #[must_use]
    #[allow(clippy::too_many_arguments)] // one Arc per stateful service; a builder would just move the noise
    pub fn new(
        kv: Arc<dyn KvService>,
        docs: Arc<dyn DocsService>,
        dead_letters: Arc<dyn DeadLetterService>,
        events: Arc<dyn ServiceEventEmitter>,
        modules: Arc<dyn ModuleSource>,
        http: Arc<dyn HttpService>,
        files: Arc<dyn FilesService>,
        pubsub: Arc<dyn PubsubService>,
        secrets: Arc<dyn SecretsService>,
        email: Arc<dyn EmailService>,
        users: Arc<dyn UsersService>,
        queue: Arc<dyn QueueService>,
        invoke: Arc<dyn InvokeService>,
        vars: Arc<dyn VarsService>,
    ) -> Self {
        Self {
            kv,
            docs,
            dead_letters,
            events,
            modules,
            http,
            files,
            pubsub,
            secrets,
            email,
            users,
            queue,
            invoke,
            vars,
            // §11.6 group collections default to noop; the picloud binary opts
            // in via `with_group_kv`. Keeps the ~14 `Services::new` call sites
            // (mostly tests) unchanged — a field addition, not a param.
            group_kv: Arc::new(NoopGroupKvService),
            group_docs: Arc::new(NoopGroupDocsService),
            group_files: Arc::new(NoopGroupFilesService),
            group_pubsub: Arc::new(NoopGroupPubsubService),
            group_queue: Arc::new(NoopGroupQueueService),
            workflow: Arc::new(NoopWorkflowService),
            interceptors: Arc::new(NoopInterceptorService),
        }
    }

    /// Set the §9.4 interceptor resolver (picloud binary wires the
    /// Postgres-backed impl; tests leave the noop default = nothing hooked).
    #[must_use]
    pub fn with_interceptors(mut self, interceptors: Arc<dyn InterceptorService>) -> Self {
        self.interceptors = interceptors;
        self
    }

    /// Set the v1.2 Workflows service (picloud binary wires the Postgres-backed
    /// impl; tests leave the noop default).
    #[must_use]
    pub fn with_workflow(mut self, workflow: Arc<dyn WorkflowService>) -> Self {
        self.workflow = workflow;
        self
    }

    /// Set the §11.6 shared group-KV service (the picloud binary wires the
    /// Postgres-backed impl; tests leave the noop default).
    #[must_use]
    pub fn with_group_kv(mut self, group_kv: Arc<dyn GroupKvService>) -> Self {
        self.group_kv = group_kv;
        self
    }

    /// Set the §11.6 shared group-DOCS service (picloud binary; tests leave noop).
    #[must_use]
    pub fn with_group_docs(mut self, group_docs: Arc<dyn GroupDocsService>) -> Self {
        self.group_docs = group_docs;
        self
    }

    /// Set the §11.6 shared group-FILES service (picloud binary; tests leave noop).
    #[must_use]
    pub fn with_group_files(mut self, group_files: Arc<dyn GroupFilesService>) -> Self {
        self.group_files = group_files;
        self
    }

    /// Set the §11.6 D2 shared group-TOPIC service (picloud binary; tests leave noop).
    #[must_use]
    pub fn with_group_pubsub(mut self, group_pubsub: Arc<dyn GroupPubsubService>) -> Self {
        self.group_pubsub = group_pubsub;
        self
    }

    /// Set the §11.6 D3 shared group-QUEUE service (picloud binary; tests leave noop).
    #[must_use]
    pub fn with_group_queue(mut self, group_queue: Arc<dyn GroupQueueService>) -> Self {
        self.group_queue = group_queue;
        self
    }

    /// All-noop bundle for tests that build an `Engine` but don't
    /// exercise the stateful services. Returns the same shape as
    /// `Services::new` so callers can't accidentally rely on a stub
    /// silently doing the right thing — every call into a noop
    /// service surfaces an explicit error.
    #[must_use]
    pub fn with_noop_services() -> Self {
        Self::new(
            Arc::new(NoopKvService),
            Arc::new(NoopDocsService),
            Arc::new(NoopDeadLetterService),
            Arc::new(NoopEventEmitter),
            Arc::new(NoopModuleSource),
            Arc::new(NoopHttpService),
            Arc::new(NoopFilesService),
            Arc::new(NoopPubsubService),
            Arc::new(NoopSecretsService),
            Arc::new(NoopEmailService),
            Arc::new(NoopUsersService),
            Arc::new(NoopQueueService),
            Arc::new(NoopInvokeService),
            Arc::new(NoopVarsService),
        )
    }
}

impl Default for Services {
    fn default() -> Self {
        Self::with_noop_services()
    }
}
