//! Remove-by-scope and explainability operations on `TidalDb` (M10p3).
//!
//! `remove_from_personalization` performs precise, non-global removal of
//! community contributions by user or by agent, reusing the M9p3 deterministic
//! rematerialization (removal of per-contributor entries *is* the rebuild). The
//! local profile is never touched. `explain_community_contributions` attributes
//! a community aggregate to the exact provenance-tagged contributions that
//! compose it.

use super::TidalDb;
use crate::{
    governance::{
        AgentPurgeTombstone, PurgeReceipt, RemoveScope, community_ledger::ContributionProvenance,
        scope::CommunityId,
    },
    schema::{EntityId, TidalError, Timestamp},
    storage::{Tag, encode_key},
};

impl TidalDb {
    /// Remove a precisely-scoped set of community contributions and
    /// deterministically rematerialize the affected aggregate.
    ///
    /// - [`RemoveScope::CommunityUser`] removes a user's contributions in an
    ///   epoch range (a retroactive purge with a durable tombstone).
    /// - [`RemoveScope::CommunityAgent`] removes everything written by an agent
    ///   (e.g. after the agent is revoked).
    ///
    /// Never global, never touches the local profile. The removed state survives
    /// restart via the community-ledger checkpoint, and the purge watermark is
    /// surfaced to community queries.
    ///
    /// # Errors
    ///
    /// - `TidalError::Internal` if the database is not writeable.
    /// - `TidalError::Storage` on tombstone persistence failure.
    pub fn remove_from_personalization(&self, scope: RemoveScope) -> crate::Result<PurgeReceipt> {
        match scope {
            RemoveScope::CommunityUser {
                user,
                community,
                epoch_range,
            } => self.purge_prior_contributions(user, community, epoch_range),
            RemoveScope::CommunityAgent {
                writer_agent,
                community,
            } => {
                self.require_writeable("remove_from_personalization")?;
                // Reject an empty agent id at the trust boundary. The empty
                // string is the WRITER_DIRECT sentinel for direct (non-agent)
                // user writes, so an empty `writer_agent` would purge — and stamp
                // a durable, replay-persistent tombstone that re-purges on every
                // open — EVERY direct user contribution to the community. Refuse
                // it rather than silently deleting real user data (CRITICAL fix).
                if writer_agent.is_empty() {
                    return Err(TidalError::invalid_input(
                        "RemoveScope::CommunityAgent writer_agent must be non-empty; \
                         an empty agent id matches direct (non-agent) user writes",
                    ));
                }
                let watermark_ns = Timestamp::now().as_nanos();

                // Durability-before-mutation, mirroring the user-purge path:
                // persist an agent-scoped tombstone BEFORE mutating the in-memory
                // aggregate so a crash before the next community checkpoint replays
                // to the same outcome (the revoked agent's contributions stay gone)
                // rather than restoring the pre-purge snapshot.
                let tombstone =
                    AgentPurgeTombstone::new(community, writer_agent.clone(), watermark_ns);
                self.persist_agent_tombstone(&tombstone)?;

                let contributions_removed =
                    self.community_ledger.purge_writer(community, &writer_agent);
                self.community_ledger
                    .set_purge_watermark(community, watermark_ns);

                // Durability is provided by tombstone replay on open (db/mod.rs
                // replays every agent-purge tombstone and re-applies it
                // idempotently); the periodic/lifecycle checkpoint GCs the stale
                // rows. This mirrors the user-purge path (communities.rs) exactly —
                // neither eagerly re-checkpoints, so the two paths cannot drift and
                // a future editor reads ONE consistent crash-recovery story.
                Ok(PurgeReceipt {
                    contributions_removed,
                    watermark_ns,
                })
            }
        }
    }

    /// Persist an agent-scope purge tombstone under `Tag::AgentPurgeTombstone`.
    /// No-op in ephemeral mode.
    fn persist_agent_tombstone(&self, t: &AgentPurgeTombstone) -> crate::Result<()> {
        let Some(ref storage) = self.storage else {
            return Ok(());
        };
        let key = encode_key(
            EntityId::new(0),
            Tag::AgentPurgeTombstone,
            &t.storage_suffix(),
        );
        storage
            .items_engine()
            .put(&key, &t.to_bytes()?)
            .map_err(TidalError::from)
    }

    /// Attribute a community aggregate to its contributions (M10p3
    /// explainability): each surviving `(user, writer_agent, epoch, score, count)`
    /// that composes the `(community, entity, signal_type)` aggregate, sorted
    /// deterministically.
    ///
    /// # Errors
    ///
    /// - `TidalError::Schema` if `signal_type` is undefined.
    pub fn explain_community_contributions(
        &self,
        community: CommunityId,
        entity_id: EntityId,
        signal_type: &str,
    ) -> crate::Result<Vec<ContributionProvenance>> {
        self.community_ledger.explain(
            community,
            entity_id,
            signal_type,
            Timestamp::now().as_nanos(),
        )
    }
}
