//! `GroupDocsServiceImpl` — wires `GroupDocsRepo` + the group-collection
//! registry underneath the `picloud_shared::GroupDocsService` trait that scripts
//! reach via the `docs::shared_collection("name")` Rhai handle (§11.6).
//!
//! Combines the group-KV service pattern (owner resolution from `cx.app_id`,
//! reads-open / writes-authed authz) with the docs surface (filter parsing,
//! JSON-object validation, value-size cap). No event emission in the MVP (the
//! "group trigger has no app to watch" deferral).

use std::sync::Arc;

use async_trait::async_trait;
use picloud_shared::{
    DocId, DocRow, DocsListPage, GroupDocsError, GroupDocsService, GroupId, NoopEventEmitter,
    SdkCallCx, ServiceEventEmitter,
};

use crate::atomic_write::{
    BestEffortGroupDocsWriter, GroupDocsTarget, GroupDocsWriter, PostgresGroupDocsWriter,
};
use crate::authz::{self, AuthzRepo, Capability};
use crate::docs_filter::{parse_filter, FilterParseError};
use crate::docs_service::docs_max_value_bytes_from_env;
use crate::group_collection_repo::GroupCollectionResolver;
use crate::group_docs_repo::{GroupDocsRepo, GroupDocsRepoError};
use crate::quota::GroupWriteQuota;

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

pub struct GroupDocsServiceImpl {
    repo: Arc<dyn GroupDocsRepo>,
    resolver: Arc<dyn GroupCollectionResolver>,
    authz: Arc<dyn AuthzRepo>,
    max_value_bytes: usize,
    /// §11.6 per-group quota: max total docs across the group's shared-docs
    /// collections (`PICLOUD_GROUP_DOCS_MAX_ROWS`).
    max_rows: u64,
    /// §11.6 M4 per-group quota: max total stored bytes across the group's
    /// shared-docs collections (`PICLOUD_GROUP_DOCS_MAX_TOTAL_BYTES`). Checked on
    /// the projected total 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 GroupDocsWriter>,
    /// 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 GroupDocsServiceImpl {
    #[must_use]
    pub fn new(
        repo: Arc<dyn GroupDocsRepo>,
        resolver: Arc<dyn GroupCollectionResolver>,
        authz: Arc<dyn AuthzRepo>,
    ) -> Self {
        Self::with_max_value_bytes(repo, resolver, authz, docs_max_value_bytes_from_env())
    }

    /// Wire the event emitter (§11.6 shared-collection triggers) on the
    /// best-effort writer. Superseded by [`Self::with_atomic_writes`].
    #[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(BestEffortGroupDocsWriter::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, so the quota is a real bound and an emit failure rolls the
    /// write back. Supersedes [`Self::with_events`].
    #[must_use]
    pub fn with_atomic_writes(mut self, pool: sqlx::PgPool) -> Self {
        self.writer = Arc::new(PostgresGroupDocsWriter::new(pool));
        self.atomic = true;
        self
    }

    #[must_use]
    pub fn with_max_value_bytes(
        repo: Arc<dyn GroupDocsRepo>,
        resolver: Arc<dyn GroupCollectionResolver>,
        authz: Arc<dyn AuthzRepo>,
        max_value_bytes: usize,
    ) -> Self {
        Self {
            writer: Arc::new(BestEffortGroupDocsWriter::new(
                repo.clone(),
                Arc::new(NoopEventEmitter),
            )),
            atomic: false,
            max_rows: crate::quota::group_docs_max_rows_from_env(),
            max_total_bytes: crate::quota::group_docs_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 ceilings a shared-collection write must fit under. The row/byte
    /// policy itself lives in `group_quota::check_group_write`, shared with KV.
    fn quota(&self) -> GroupWriteQuota {
        GroupWriteQuota {
            max_rows: self.max_rows,
            max_total_bytes: self.max_total_bytes,
            max_value_bytes: self.max_value_bytes,
        }
    }

    async fn owning_group(
        &self,
        cx: &SdkCallCx,
        collection: &str,
    ) -> Result<GroupId, GroupDocsError> {
        if collection.is_empty() {
            return Err(GroupDocsError::InvalidCollection);
        }
        self.resolver
            .resolve_owning_group(cx.app_id, collection, KIND_DOCS)
            .await
            .map_err(|e| GroupDocsError::Backend(e.to_string()))?
            .ok_or_else(|| GroupDocsError::CollectionNotShared(collection.to_string()))
    }

    fn check_data_size(&self, data: &serde_json::Value) -> Result<(), GroupDocsError> {
        let encoded_len = serde_json::to_vec(data)
            .map(|v| v.len())
            .map_err(|e| GroupDocsError::Backend(format!("encode doc data: {e}")))?;
        if encoded_len > self.max_value_bytes {
            return Err(GroupDocsError::ValueTooLarge {
                limit: self.max_value_bytes,
                actual: encoded_len,
            });
        }
        Ok(())
    }

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

    async fn check_write(&self, cx: &SdkCallCx, group_id: GroupId) -> Result<(), GroupDocsError> {
        // 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::GroupDocsWrite(group_id),
            || GroupDocsError::Forbidden,
            GroupDocsError::Backend,
        )
        .await
    }
}

fn validate_data(data: &serde_json::Value) -> Result<(), GroupDocsError> {
    if !data.is_object() {
        return Err(GroupDocsError::InvalidData);
    }
    Ok(())
}

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

impl From<FilterParseError> for GroupDocsError {
    fn from(e: FilterParseError) -> Self {
        match e {
            FilterParseError::InvalidFilter(s) => Self::InvalidFilter(s),
            FilterParseError::UnsupportedOperator(s) => Self::UnsupportedOperator(s),
        }
    }
}

#[async_trait]
impl GroupDocsService for GroupDocsServiceImpl {
    async fn create(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        data: serde_json::Value,
    ) -> Result<DocId, GroupDocsError> {
        let group_id = self.owning_group(cx, collection).await?;
        validate_data(&data)?;
        self.check_data_size(&data)?;
        self.check_write(cx, group_id).await?;
        self.writer
            .create(
                cx,
                GroupDocsTarget {
                    group_id,
                    collection,
                },
                data,
                self.quota(),
            )
            .await
    }

    async fn get(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        id: DocId,
    ) -> Result<Option<DocRow>, GroupDocsError> {
        let group_id = self.owning_group(cx, collection).await?;
        self.check_read(cx, group_id).await?;
        Ok(self.repo.get(group_id, collection, id).await?)
    }

    async fn find(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        filter: serde_json::Value,
    ) -> Result<Vec<DocRow>, GroupDocsError> {
        let group_id = self.owning_group(cx, collection).await?;
        self.check_read(cx, group_id).await?;
        let parsed = parse_filter(&filter)?;
        Ok(self.repo.find(group_id, collection, &parsed).await?)
    }

    async fn find_one(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        filter: serde_json::Value,
    ) -> Result<Option<DocRow>, GroupDocsError> {
        let group_id = self.owning_group(cx, collection).await?;
        self.check_read(cx, group_id).await?;
        let mut parsed = parse_filter(&filter)?;
        if parsed.limit.is_none() {
            parsed.limit = Some(1);
        }
        let rows = self.repo.find(group_id, collection, &parsed).await?;
        Ok(rows.into_iter().next())
    }

    async fn update(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        id: DocId,
        data: serde_json::Value,
    ) -> Result<(), GroupDocsError> {
        let group_id = self.owning_group(cx, collection).await?;
        validate_data(&data)?;
        self.check_data_size(&data)?;
        self.check_write(cx, group_id).await?;
        if self
            .writer
            .update(
                cx,
                GroupDocsTarget {
                    group_id,
                    collection,
                },
                id,
                data,
                self.quota(),
            )
            .await?
        {
            Ok(())
        } else {
            Err(GroupDocsError::NotFound)
        }
    }

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

    async fn list(
        &self,
        cx: &SdkCallCx,
        collection: &str,
        cursor: Option<&str>,
        limit: u32,
    ) -> Result<DocsListPage, GroupDocsError> {
        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 crate::docs_filter::DocsFilter;
    use chrono::Utc;
    use picloud_shared::{
        AdminUserId, AppId, AppRole, ExecutionId, InstanceRole, Principal, RequestId, ScriptId,
        UserId,
    };
    use std::collections::HashMap;
    use tokio::sync::Mutex;
    use uuid::Uuid;

    #[derive(Default)]
    struct InMemoryGroupDocsRepo {
        // (group, collection) -> id -> data
        data: Mutex<HashMap<(GroupId, String), HashMap<Uuid, serde_json::Value>>>,
    }

    fn row(id: Uuid, data: serde_json::Value) -> DocRow {
        DocRow {
            id,
            data,
            created_at: Utc::now(),
            updated_at: Utc::now(),
        }
    }

    #[async_trait]
    impl GroupDocsRepo for InMemoryGroupDocsRepo {
        async fn create(
            &self,
            group_id: GroupId,
            collection: &str,
            data: serde_json::Value,
        ) -> Result<DocRow, GroupDocsRepoError> {
            let id = Uuid::new_v4();
            self.data
                .lock()
                .await
                .entry((group_id, collection.to_string()))
                .or_default()
                .insert(id, data.clone());
            Ok(row(id, data))
        }

        async fn get(
            &self,
            group_id: GroupId,
            collection: &str,
            id: DocId,
        ) -> Result<Option<DocRow>, GroupDocsRepoError> {
            Ok(self
                .data
                .lock()
                .await
                .get(&(group_id, collection.to_string()))
                .and_then(|m| m.get(&id).cloned())
                .map(|d| row(id, d)))
        }

        // The fake ignores the filter (filter SQL is covered by docs_repo tests
        // + the journey); returns all docs in the collection.
        async fn find(
            &self,
            group_id: GroupId,
            collection: &str,
            _filter: &DocsFilter,
        ) -> Result<Vec<DocRow>, GroupDocsRepoError> {
            Ok(self
                .data
                .lock()
                .await
                .get(&(group_id, collection.to_string()))
                .map(|m| m.iter().map(|(id, d)| row(*id, d.clone())).collect())
                .unwrap_or_default())
        }

        async fn update(
            &self,
            group_id: GroupId,
            collection: &str,
            id: DocId,
            data: serde_json::Value,
        ) -> Result<Option<serde_json::Value>, GroupDocsRepoError> {
            let mut guard = self.data.lock().await;
            let coll = guard.entry((group_id, collection.to_string())).or_default();
            Ok(coll.insert(id, data))
        }

        async fn delete(
            &self,
            group_id: GroupId,
            collection: &str,
            id: DocId,
        ) -> Result<Option<serde_json::Value>, GroupDocsRepoError> {
            Ok(self
                .data
                .lock()
                .await
                .get_mut(&(group_id, collection.to_string()))
                .and_then(|m| m.remove(&id)))
        }

        async fn list(
            &self,
            _group_id: GroupId,
            _collection: &str,
            _cursor: Option<&str>,
            _limit: u32,
        ) -> Result<DocsListPage, GroupDocsRepoError> {
            Ok(DocsListPage {
                docs: vec![],
                next_cursor: None,
            })
        }

        async fn total_bytes(&self, group_id: GroupId) -> Result<u64, GroupDocsRepoError> {
            let data = self.data.lock().await;
            Ok(data
                .iter()
                .filter(|((g, _), _)| *g == group_id)
                .flat_map(|(_, docs)| docs.values())
                .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 doc in the group EXCEPT the one
        /// being replaced, plus the new data — i.e. `used - old + new`.
        async fn projected_total_bytes(
            &self,
            group_id: GroupId,
            replacing: Option<DocId>,
            new_data: &serde_json::Value,
        ) -> Result<u64, GroupDocsRepoError> {
            let data = self.data.lock().await;
            let others: u64 = data
                .iter()
                .filter(|((g, _), _)| *g == group_id)
                .flat_map(|(_, docs)| docs.iter())
                .filter(|(id, _)| Some(**id) != replacing)
                .map(|(_, v)| serde_json::to_vec(v).map_or(0, |b| b.len() as u64))
                .sum();
            let new_len = serde_json::to_vec(new_data).map_or(0, |b| b.len() as u64);
            Ok(others + new_len)
        }
    }

    #[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 svc(resolver: FakeResolver) -> GroupDocsServiceImpl {
        GroupDocsServiceImpl::new(
            Arc::new(InMemoryGroupDocsRepo::default()),
            Arc::new(resolver),
            Arc::new(DenyingAuthzRepo),
        )
    }

    #[tokio::test]
    async fn unrelated_app_gets_collection_not_shared() {
        let app_a = AppId::new();
        let app_b = AppId::new();
        let group = GroupId::new();
        let mut resolver = FakeResolver::default();
        resolver.map.insert((app_a, "articles".into()), group);
        let docs = svc(resolver);

        let cx_a = cx_with(app_a, Some(owner()));
        let id = docs
            .create(&cx_a, "articles", serde_json::json!({"t": "hi"}))
            .await
            .unwrap();
        assert!(docs.get(&cx_a, "articles", id).await.unwrap().is_some());

        let cx_b = cx_with(app_b, Some(owner()));
        let err = docs
            .find(&cx_b, "articles", serde_json::json!({}))
            .await
            .unwrap_err();
        assert!(matches!(err, GroupDocsError::CollectionNotShared(c) if c == "articles"));
    }

    #[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, "articles".into()), group);
        let docs = svc(resolver);

        let owner_cx = cx_with(app, Some(owner()));
        docs.create(&owner_cx, "articles", serde_json::json!({"t": "hi"}))
            .await
            .unwrap();

        // Anonymous READ (find) is allowed.
        let anon = cx_with(app, None);
        let hits = docs
            .find(&anon, "articles", serde_json::json!({}))
            .await
            .unwrap();
        assert_eq!(hits.len(), 1);

        // Anonymous WRITE (create) fails closed.
        let err = docs
            .create(&anon, "articles", serde_json::json!({"t": "x"}))
            .await
            .unwrap_err();
        assert!(matches!(err, GroupDocsError::Forbidden));
    }

    #[tokio::test]
    async fn non_object_data_rejected() {
        let app = AppId::new();
        let group = GroupId::new();
        let mut resolver = FakeResolver::default();
        resolver.map.insert((app, "articles".into()), group);
        let docs = svc(resolver);
        let cx = cx_with(app, Some(owner()));
        let err = docs
            .create(&cx, "articles", serde_json::json!("not-an-object"))
            .await
            .unwrap_err();
        assert!(matches!(err, GroupDocsError::InvalidData));
    }

    #[tokio::test]
    async fn per_group_byte_quota_uses_the_projected_total() {
        // §11.6 M4: the shared-docs byte ceiling is checked on the PROJECTED
        // total — a create over the cap is rejected, and a shrinking update near
        // the cap is allowed (the delta rule).
        let app = AppId::new();
        let group = GroupId::new();
        let mut resolver = FakeResolver::default();
        resolver.map.insert((app, "articles".into()), group);
        let docs = GroupDocsServiceImpl::new(
            Arc::new(InMemoryGroupDocsRepo::default()),
            Arc::new(resolver),
            Arc::new(DenyingAuthzRepo),
        )
        .with_max_total_bytes(40);
        let cx = cx_with(app, Some(owner()));

        // {"t":"xxxxx"} encodes to 13 bytes.
        let id = docs
            .create(&cx, "articles", serde_json::json!({"t": "xxxxx"}))
            .await
            .unwrap(); // used = 13
        docs.create(&cx, "articles", serde_json::json!({"t": "xxxxx"}))
            .await
            .unwrap(); // used = 26
                       // A third 13-byte doc → projected 39 ≤ 40 → ok.
        docs.create(&cx, "articles", serde_json::json!({"t": "xxxxx"}))
            .await
            .unwrap(); // used = 39
                       // A fourth → projected 52 > 40 → rejected.
        let err = docs
            .create(&cx, "articles", serde_json::json!({"t": "xxxxx"}))
            .await
            .unwrap_err();
        assert!(
            matches!(
                err,
                GroupDocsError::TotalBytesQuotaExceeded { limit: 40, .. }
            ),
            "got {err:?}"
        );
        // Shrinking an existing doc frees budget (projected 39 - 13 + 9 = 35).
        docs.update(&cx, "articles", id, serde_json::json!({"t": "y"}))
            .await
            .expect("a shrinking update near the cap must be allowed");
    }
}
