//! `GroupKvServiceImpl` — wires `GroupKvRepo` + the group-collection registry
//! underneath the `picloud_shared::GroupKvService` trait that scripts reach via
//! the `kv::shared_collection("name")` Rhai handle (§11.6).
//!
//! Layers added over the raw repo:
//!
//! 1. Empty-collection rejection.
//! 2. **Owner resolution** (the structural isolation boundary): the collection
//!    name is resolved to the nearest ancestor group that declares it shared,
//!    walking the calling app's chain. No declaration on the chain → a clean
//!    `CollectionNotShared`, BEFORE any authz or storage access.
//! 3. **Reads-open / writes-authed authz**: reads use `script_gate`
//!    (anonymous public scripts skip); writes use `script_gate_require_principal`
//!    (anonymous fails closed — shared mutation always needs an authenticated
//!    editor+ on the owning group).
//! 4. Per-value size cap (reuses `PICLOUD_KV_MAX_VALUE_BYTES`) + a per-group
//!    row quota (§11.6 M3, `PICLOUD_GROUP_KV_MAX_ROWS`).
//! 5. §11.6 M2 shared-collection triggers: a write fires `shared = true`
//!    triggers on the owning group, under the writer app (`emit_shared`).

use std::sync::Arc;

use async_trait::async_trait;
use picloud_shared::{
    GroupId, GroupKvError, GroupKvService, KvListPage, NoopEventEmitter, SdkCallCx,
    ServiceEventEmitter,
};

use crate::atomic_write::{
    BestEffortGroupKvWriter, GroupKvTarget, GroupKvWriter, PostgresGroupKvWriter,
};
use crate::authz::{self, AuthzRepo, Capability};
use crate::group_collection_repo::GroupCollectionResolver;
use crate::group_kv_repo::{GroupKvRepo, GroupKvRepoError};
use crate::kv_service::kv_max_value_bytes_from_env;
use crate::quota::GroupWriteQuota;

/// The registry `kind` this service resolves.
const KIND_KV: &str = "kv";

pub struct GroupKvServiceImpl {
    repo: Arc<dyn GroupKvRepo>,
    resolver: Arc<dyn GroupCollectionResolver>,
    authz: Arc<dyn AuthzRepo>,
    max_value_bytes: usize,
    /// §11.6 per-group quota: max total keys across the group's shared-KV
    /// collections (`PICLOUD_GROUP_KV_MAX_ROWS`). Checked only when inserting a
    /// NEW key.
    max_rows: u64,
    /// §11.6 M4 per-group quota: max total stored bytes across the group's
    /// shared-KV collections (`PICLOUD_GROUP_KV_MAX_TOTAL_BYTES`). Checked on the
    /// projected total (old value subtracted, new added) so a same/smaller update
    /// near the cap is still allowed.
    max_total_bytes: u64,
    /// Mutations: the quota decision, the row write, and the shared-trigger
    /// fan-out, which for the host all commit in one advisory-locked
    /// transaction. Reads stay on `repo`.
    writer: Arc<dyn GroupKvWriter>,
    /// Set once [`Self::with_atomic_writes`] has installed the transactional
    /// writer. `with_events` refuses to overwrite it — silently downgrading a
    /// service back to non-transactional writes (and dropping its quota
    /// enforcement) because a builder call was appended in the wrong order is
    /// exactly the kind of regression the type system will not catch.
    atomic: bool,
}

impl GroupKvServiceImpl {
    #[must_use]
    pub fn new(
        repo: Arc<dyn GroupKvRepo>,
        resolver: Arc<dyn GroupCollectionResolver>,
        authz: Arc<dyn AuthzRepo>,
    ) -> Self {
        Self::with_max_value_bytes(repo, resolver, authz, kv_max_value_bytes_from_env())
    }

    /// Wire the event emitter (§11.6 shared-collection triggers) on the
    /// best-effort writer. Without this — or [`Self::with_atomic_writes`], which
    /// supersedes it — shared writes fire nothing.
    #[must_use]
    pub fn with_events(mut self, events: Arc<dyn ServiceEventEmitter>) -> Self {
        if self.atomic {
            tracing::warn!(
                "with_events() called after with_atomic_writes() — ignored; the \
                 transactional writer already resolves and writes the fan-out itself"
            );
            return self;
        }
        self.writer = Arc::new(BestEffortGroupKvWriter::new(self.repo.clone(), events));
        self
    }

    /// Use the transactional writer: the quota reads, the row write, and the
    /// shared-trigger fan-out all run on ONE connection under a per-group
    /// advisory lock. That makes the quota an actual bound (concurrent writers
    /// to a group serialize, so one sees the other's row) and makes an emit
    /// failure roll the write back instead of silently losing the event.
    ///
    /// Supersedes [`Self::with_events`] — the transactional writer resolves and
    /// writes the fan-out itself and needs no injected emitter.
    #[must_use]
    pub fn with_atomic_writes(mut self, pool: sqlx::PgPool) -> Self {
        self.writer = Arc::new(PostgresGroupKvWriter::new(pool));
        self.atomic = true;
        self
    }

    /// Override the per-group row quota (§11.6). Mainly for tests; the host uses
    /// the env-var default.
    #[must_use]
    pub fn with_max_rows(mut self, max_rows: u64) -> Self {
        self.max_rows = max_rows;
        self
    }

    #[must_use]
    pub fn with_max_value_bytes(
        repo: Arc<dyn GroupKvRepo>,
        resolver: Arc<dyn GroupCollectionResolver>,
        authz: Arc<dyn AuthzRepo>,
        max_value_bytes: usize,
    ) -> Self {
        Self {
            writer: Arc::new(BestEffortGroupKvWriter::new(
                repo.clone(),
                Arc::new(NoopEventEmitter),
            )),
            atomic: false,
            max_rows: crate::quota::group_kv_max_rows_from_env(),
            max_total_bytes: crate::quota::group_kv_max_total_bytes_from_env(),
            repo,
            resolver,
            authz,
            max_value_bytes,
        }
    }

    /// Override the per-group total-bytes quota (§11.6 M4). Mainly for tests.
    #[must_use]
    pub fn with_max_total_bytes(mut self, max_total_bytes: u64) -> Self {
        self.max_total_bytes = max_total_bytes;
        self
    }

    /// The structural boundary: resolve the collection to its owning group on
    /// the calling app's chain. `CollectionNotShared` when no ancestor group
    /// declares it — the same outcome a foreign app gets, by construction.
    async fn owning_group(
        &self,
        cx: &SdkCallCx,
        collection: &str,
    ) -> Result<GroupId, GroupKvError> {
        if collection.is_empty() {
            return Err(GroupKvError::InvalidCollection);
        }
        self.resolver
            .resolve_owning_group(cx.app_id, collection, KIND_KV)
            .await
            .map_err(|e| GroupKvError::Backend(e.to_string()))?
            .ok_or_else(|| GroupKvError::CollectionNotShared(collection.to_string()))
    }

    /// The ceilings a shared-collection write must fit under.
    fn quota(&self) -> GroupWriteQuota {
        GroupWriteQuota {
            max_rows: self.max_rows,
            max_total_bytes: self.max_total_bytes,
            max_value_bytes: self.max_value_bytes,
        }
    }

    /// Enforce the per-value byte cap. The per-GROUP byte quota is measured
    /// canonically in SQL inside the writing transaction — see
    /// `group_quota::check_group_write`.
    fn check_value_size(&self, value: &serde_json::Value) -> Result<(), GroupKvError> {
        let encoded_len = serde_json::to_vec(value)
            .map(|v| v.len())
            .map_err(|e| GroupKvError::Backend(format!("encode value: {e}")))?;
        if encoded_len > self.max_value_bytes {
            return Err(GroupKvError::ValueTooLarge {
                limit: self.max_value_bytes,
                actual: encoded_len,
            });
        }
        Ok(())
    }

    async fn check_read(&self, cx: &SdkCallCx, group_id: GroupId) -> Result<(), GroupKvError> {
        authz::script_gate(
            &*self.authz,
            cx,
            Capability::GroupKvRead(group_id),
            || GroupKvError::Forbidden,
            GroupKvError::Backend,
        )
        .await
    }

    async fn check_write(&self, cx: &SdkCallCx, group_id: GroupId) -> Result<(), GroupKvError> {
        // Fails closed for an anonymous principal: shared mutation always needs
        // an authenticated editor+ on the owning group.
        authz::script_gate_require_principal(
            &*self.authz,
            cx,
            Capability::GroupKvWrite(group_id),
            || GroupKvError::Forbidden,
            GroupKvError::Backend,
        )
        .await
    }
}

impl From<GroupKvRepoError> for GroupKvError {
    fn from(e: GroupKvRepoError) -> Self {
        Self::Backend(e.to_string())
    }
}

#[async_trait]
impl GroupKvService for GroupKvServiceImpl {
    async fn get(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        key: &str,
    ) -> Result<Option<serde_json::Value>, GroupKvError> {
        let group_id = self.owning_group(cx, collection).await?;
        self.check_read(cx, group_id).await?;
        Ok(self.repo.get(group_id, collection, key).await?)
    }

    async fn set(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        key: &str,
        value: serde_json::Value,
    ) -> Result<(), GroupKvError> {
        let group_id = self.owning_group(cx, collection).await?;
        self.check_value_size(&value)?;
        self.check_write(cx, group_id).await?;
        self.writer
            .set(
                cx,
                GroupKvTarget {
                    group_id,
                    collection,
                    key,
                },
                value,
                self.quota(),
            )
            .await
    }

    async fn set_if(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        key: &str,
        expected: Option<serde_json::Value>,
        new: serde_json::Value,
    ) -> Result<bool, GroupKvError> {
        let group_id = self.owning_group(cx, collection).await?;
        self.check_value_size(&new)?;
        self.check_write(cx, group_id).await?;
        self.writer
            .set_if(
                cx,
                GroupKvTarget {
                    group_id,
                    collection,
                    key,
                },
                expected,
                new,
                self.quota(),
            )
            .await
    }

    async fn delete(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        key: &str,
    ) -> Result<bool, GroupKvError> {
        let group_id = self.owning_group(cx, collection).await?;
        self.check_write(cx, group_id).await?;
        self.writer
            .delete(
                cx,
                GroupKvTarget {
                    group_id,
                    collection,
                    key,
                },
            )
            .await
    }

    async fn has(&self, cx: &SdkCallCx, collection: &str, key: &str) -> Result<bool, GroupKvError> {
        let group_id = self.owning_group(cx, collection).await?;
        self.check_read(cx, group_id).await?;
        Ok(self.repo.has(group_id, collection, key).await?)
    }

    async fn list(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        cursor: Option<&str>,
        limit: u32,
    ) -> Result<KvListPage, GroupKvError> {
        let group_id = self.owning_group(cx, collection).await?;
        self.check_read(cx, group_id).await?;
        Ok(self.repo.list(group_id, collection, cursor, limit).await?)
    }
}

// ----------------------------------------------------------------------------
// Tests — in-memory repo + a fake resolver so unit tests don't need Postgres.
// ----------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::authz::{AuthzError, AuthzRepo};
    use picloud_shared::{
        AdminUserId, AppId, AppRole, ExecutionId, InstanceRole, Principal, RequestId, ScriptId,
        UserId,
    };
    use std::collections::{BTreeMap, HashMap};
    use tokio::sync::Mutex;

    #[derive(Default)]
    struct InMemoryGroupKvRepo {
        data: Mutex<BTreeMap<(GroupId, String, String), serde_json::Value>>,
    }

    #[async_trait]
    impl GroupKvRepo for InMemoryGroupKvRepo {
        async fn get(
            &self,
            group_id: GroupId,
            collection: &str,
            key: &str,
        ) -> Result<Option<serde_json::Value>, GroupKvRepoError> {
            Ok(self
                .data
                .lock()
                .await
                .get(&(group_id, collection.to_string(), key.to_string()))
                .cloned())
        }

        async fn set(
            &self,
            group_id: GroupId,
            collection: &str,
            key: &str,
            value: serde_json::Value,
        ) -> Result<Option<serde_json::Value>, GroupKvRepoError> {
            Ok(self
                .data
                .lock()
                .await
                .insert((group_id, collection.to_string(), key.to_string()), value))
        }

        async fn set_if(
            &self,
            group_id: GroupId,
            collection: &str,
            key: &str,
            expected: Option<serde_json::Value>,
            new: serde_json::Value,
        ) -> Result<bool, GroupKvRepoError> {
            let mut data = self.data.lock().await;
            let k = (group_id, collection.to_string(), key.to_string());
            let matches = match (&expected, data.get(&k)) {
                (None, None) => true,
                (Some(exp), Some(cur)) => exp == cur,
                _ => false,
            };
            if matches {
                data.insert(k, new);
            }
            Ok(matches)
        }

        async fn delete(
            &self,
            group_id: GroupId,
            collection: &str,
            key: &str,
        ) -> Result<Option<serde_json::Value>, GroupKvRepoError> {
            Ok(self
                .data
                .lock()
                .await
                .remove(&(group_id, collection.to_string(), key.to_string())))
        }

        async fn has(
            &self,
            group_id: GroupId,
            collection: &str,
            key: &str,
        ) -> Result<bool, GroupKvRepoError> {
            Ok(self.data.lock().await.contains_key(&(
                group_id,
                collection.to_string(),
                key.to_string(),
            )))
        }

        async fn list(
            &self,
            group_id: GroupId,
            collection: &str,
            _cursor: Option<&str>,
            limit: u32,
        ) -> Result<KvListPage, GroupKvRepoError> {
            let data = self.data.lock().await;
            let mut keys: Vec<String> = data
                .iter()
                .filter(|((g, c, _), _)| *g == group_id && c == collection)
                .map(|((_, _, k), _)| k.clone())
                .collect();
            keys.sort();
            keys.truncate((limit as usize).max(1));
            Ok(KvListPage {
                keys,
                next_cursor: None,
            })
        }

        async fn count_rows(&self, group_id: GroupId) -> Result<u64, GroupKvRepoError> {
            let data = self.data.lock().await;
            Ok(data.keys().filter(|(g, _, _)| *g == group_id).count() as u64)
        }

        async fn total_bytes(&self, group_id: GroupId) -> Result<u64, GroupKvRepoError> {
            let data = self.data.lock().await;
            Ok(data
                .iter()
                .filter(|((g, _, _), _)| *g == group_id)
                .map(|(_, v)| serde_json::to_vec(v).map_or(0, |b| b.len() as u64))
                .sum())
        }

        /// Same projection the Postgres impl computes in SQL, but with this
        /// mock's own (compact) metric: every OTHER row in the group, plus the
        /// new value — i.e. `used - old + new`, measured consistently.
        async fn projected_total_bytes(
            &self,
            group_id: GroupId,
            collection: &str,
            key: &str,
            new_value: &serde_json::Value,
        ) -> Result<u64, GroupKvRepoError> {
            let data = self.data.lock().await;
            let target = (group_id, collection.to_string(), key.to_string());
            let others: u64 = data
                .iter()
                .filter(|(k, _)| k.0 == group_id && **k != target)
                .map(|(_, v)| serde_json::to_vec(v).map_or(0, |b| b.len() as u64))
                .sum();
            let new_len = serde_json::to_vec(new_value).map_or(0, |b| b.len() as u64);
            Ok(others + new_len)
        }
    }

    /// Maps `(app_id, lowercased name) -> owning group`. Anything absent is
    /// "not shared with you".
    #[derive(Default)]
    struct FakeResolver {
        map: HashMap<(AppId, String), GroupId>,
    }

    #[async_trait]
    impl GroupCollectionResolver for FakeResolver {
        async fn resolve_owning_group(
            &self,
            app_id: AppId,
            name: &str,
            _kind: &str,
        ) -> Result<Option<GroupId>, sqlx::Error> {
            Ok(self.map.get(&(app_id, name.to_lowercase())).copied())
        }
    }

    #[derive(Default)]
    struct DenyingAuthzRepo;

    #[async_trait]
    impl AuthzRepo for DenyingAuthzRepo {
        async fn membership(
            &self,
            _user_id: UserId,
            _app_id: AppId,
        ) -> Result<Option<AppRole>, AuthzError> {
            Ok(None)
        }
    }

    fn cx_with(app_id: AppId, principal: Option<Principal>) -> SdkCallCx {
        SdkCallCx {
            app_id,
            script_id: ScriptId::new(),
            principal,
            execution_id: ExecutionId::new(),
            request_id: RequestId::new(),
            trigger_depth: 0,
            root_execution_id: ExecutionId::new(),
            is_dead_letter_handler: false,
            event: None,
        }
    }

    fn owner() -> Principal {
        Principal {
            user_id: AdminUserId::new(),
            instance_role: InstanceRole::Owner,
            scopes: None,
            app_binding: None,
        }
    }

    fn member_no_role() -> Principal {
        Principal {
            user_id: AdminUserId::new(),
            instance_role: InstanceRole::Member,
            scopes: None,
            app_binding: None,
        }
    }

    fn svc(resolver: FakeResolver) -> GroupKvServiceImpl {
        GroupKvServiceImpl::new(
            Arc::new(InMemoryGroupKvRepo::default()),
            Arc::new(resolver),
            Arc::new(DenyingAuthzRepo),
        )
    }

    #[tokio::test]
    async fn per_group_row_quota_rejects_new_keys_but_allows_updates() {
        // §11.6: a group's shared-KV store has a row ceiling; a NEW key over the
        // cap is rejected, but updating an existing key (net-zero) still works.
        let app = AppId::new();
        let group = GroupId::new();
        let mut resolver = FakeResolver::default();
        resolver.map.insert((app, "catalog".into()), group);
        let kv = GroupKvServiceImpl::new(
            Arc::new(InMemoryGroupKvRepo::default()),
            Arc::new(resolver),
            Arc::new(DenyingAuthzRepo),
        )
        .with_max_rows(2);
        let cx = cx_with(app, Some(owner()));

        kv.set(&cx, "catalog", "a", serde_json::json!(1))
            .await
            .unwrap();
        kv.set(&cx, "catalog", "b", serde_json::json!(2))
            .await
            .unwrap();
        // A third distinct key exceeds the cap.
        let err = kv
            .set(&cx, "catalog", "c", serde_json::json!(3))
            .await
            .unwrap_err();
        assert!(
            matches!(
                err,
                GroupKvError::QuotaExceeded {
                    limit: 2,
                    actual: 2
                }
            ),
            "got {err:?}"
        );
        // Updating an existing key is exempt (net-zero).
        kv.set(&cx, "catalog", "a", serde_json::json!(11))
            .await
            .expect("update of an existing key must be allowed at the cap");
    }

    #[tokio::test]
    async fn per_group_byte_quota_uses_the_projected_total() {
        // §11.6 M4: the byte ceiling is checked on the PROJECTED total, so a
        // write that would push the group over is rejected, but a same/smaller
        // update near the cap is still allowed (unlike a naive used+new check).
        let app = AppId::new();
        let group = GroupId::new();
        let mut resolver = FakeResolver::default();
        resolver.map.insert((app, "catalog".into()), group);
        // A 20-byte ceiling; each `"xxxxx"` value encodes to 7 bytes ("\"xxxxx\"").
        let kv = GroupKvServiceImpl::new(
            Arc::new(InMemoryGroupKvRepo::default()),
            Arc::new(resolver),
            Arc::new(DenyingAuthzRepo),
        )
        .with_max_rows(1000)
        .with_max_total_bytes(20);
        let cx = cx_with(app, Some(owner()));

        kv.set(&cx, "catalog", "a", serde_json::json!("xxxxx"))
            .await
            .unwrap(); // used = 7
        kv.set(&cx, "catalog", "b", serde_json::json!("xxxxx"))
            .await
            .unwrap(); // used = 14
                       // A third 7-byte value → projected 21 > 20 → rejected.
        let err = kv
            .set(&cx, "catalog", "c", serde_json::json!("xxxxx"))
            .await
            .unwrap_err();
        assert!(
            matches!(err, GroupKvError::TotalBytesQuotaExceeded { limit: 20, .. }),
            "got {err:?}"
        );
        // A same-size update of an existing key stays within budget (projected
        // = 14 - 7 + 7 = 14 ≤ 20) — the delta rule, not used+new.
        kv.set(&cx, "catalog", "a", serde_json::json!("yyyyy"))
            .await
            .expect("a same-size update near the cap must be allowed");
        // Growing an existing value past the cap is still rejected.
        let err = kv
            .set(&cx, "catalog", "a", serde_json::json!("zzzzzzzzzzzzzzz"))
            .await
            .unwrap_err();
        assert!(
            matches!(err, GroupKvError::TotalBytesQuotaExceeded { .. }),
            "got {err:?}"
        );
    }

    #[tokio::test]
    async fn unrelated_app_gets_collection_not_shared() {
        // app_a's chain declares "catalog"; app_b's does not.
        let app_a = AppId::new();
        let app_b = AppId::new();
        let group = GroupId::new();
        let mut resolver = FakeResolver::default();
        resolver.map.insert((app_a, "catalog".into()), group);
        let kv = svc(resolver);

        // app_a (owner principal) can write+read.
        let cx_a = cx_with(app_a, Some(owner()));
        kv.set(&cx_a, "catalog", "k", serde_json::json!(1))
            .await
            .unwrap();
        assert_eq!(
            kv.get(&cx_a, "catalog", "k").await.unwrap(),
            Some(serde_json::json!(1))
        );

        // app_b is off-chain — the name does not resolve.
        let cx_b = cx_with(app_b, Some(owner()));
        let err = kv.get(&cx_b, "catalog", "k").await.unwrap_err();
        assert!(matches!(err, GroupKvError::CollectionNotShared(c) if c == "catalog"));
    }

    #[tokio::test]
    async fn reads_open_writes_require_auth() {
        let app = AppId::new();
        let group = GroupId::new();
        let mut resolver = FakeResolver::default();
        resolver.map.insert((app, "catalog".into()), group);
        let kv = svc(resolver);

        // Seed a value as owner.
        let owner_cx = cx_with(app, Some(owner()));
        kv.set(&owner_cx, "catalog", "k", serde_json::json!("v"))
            .await
            .unwrap();

        // Anonymous (principal=None) READ is allowed (reads-open).
        let anon = cx_with(app, None);
        assert_eq!(
            kv.get(&anon, "catalog", "k").await.unwrap(),
            Some(serde_json::json!("v"))
        );

        // Anonymous WRITE fails closed.
        let err = kv
            .set(&anon, "catalog", "k", serde_json::json!("x"))
            .await
            .unwrap_err();
        assert!(matches!(err, GroupKvError::Forbidden));

        // Authenticated member with no group role: write forbidden.
        let member = cx_with(app, Some(member_no_role()));
        let err = kv.delete(&member, "catalog", "k").await.unwrap_err();
        assert!(matches!(err, GroupKvError::Forbidden));
    }

    #[tokio::test]
    async fn empty_collection_rejected_before_resolve() {
        let kv = svc(FakeResolver::default());
        let cx = cx_with(AppId::new(), None);
        let err = kv.get(&cx, "", "k").await.unwrap_err();
        assert!(matches!(err, GroupKvError::InvalidCollection));
    }

    #[tokio::test]
    async fn set_if_is_compare_and_swap_and_writes_fail_closed() {
        let app = AppId::new();
        let group = GroupId::new();
        let mut resolver = FakeResolver::default();
        resolver.map.insert((app, "catalog".into()), group);
        let kv = svc(resolver);
        let cx = cx_with(app, Some(owner()));

        // Insert-if-absent, then swap-if-equal (as the per-app path).
        assert!(kv
            .set_if(&cx, "catalog", "k", None, serde_json::json!(1))
            .await
            .unwrap());
        assert!(!kv
            .set_if(&cx, "catalog", "k", None, serde_json::json!(2))
            .await
            .unwrap());
        assert!(!kv
            .set_if(
                &cx,
                "catalog",
                "k",
                Some(serde_json::json!(9)),
                serde_json::json!(3)
            )
            .await
            .unwrap());
        assert!(kv
            .set_if(
                &cx,
                "catalog",
                "k",
                Some(serde_json::json!(1)),
                serde_json::json!(3)
            )
            .await
            .unwrap());
        assert_eq!(
            kv.get(&cx, "catalog", "k").await.unwrap(),
            Some(serde_json::json!(3))
        );

        // A shared-collection CAS still fails closed for an anonymous writer.
        let anon = cx_with(app, None);
        let err = kv
            .set_if(&anon, "catalog", "k", None, serde_json::json!(4))
            .await
            .unwrap_err();
        assert!(matches!(err, GroupKvError::Forbidden));
    }
}
