//! Domain types for the tool-scoped memory layer.
//!
//! A [`ToolMemoryRule`] is a durable, actionable instruction attached to a
//! specific tool (e.g. `email`, `shell`, `web_search`). Unlike the per-tool
//! statistics stored in the `tool_effectiveness` namespace, these rules
//! capture **guidance** — corrections, safety constraints, and learned
//! operational rules that the agent should obey when considering or
//! invoking that tool.
//!
//! Rules carry a [`ToolMemoryPriority`] level so the retrieval pipeline can
//! distinguish safety-critical instructions from soft suggestions:
//!
//! - [`ToolMemoryPriority::Critical`] — pinned into the system prompt and
//!   therefore not subject to mid-session context compression.
//! - [`ToolMemoryPriority::High`] — surfaced alongside critical rules at
//!   tool-selection time.
//! - [`ToolMemoryPriority::Normal`] — available on demand via the recall
//!   APIs, but not eagerly injected.

use serde::{Deserialize, Serialize};

/// Priority/criticality of a [`ToolMemoryRule`].
///
/// Used by both storage (to filter what is pinned into the system prompt)
/// and retrieval (to sort high-priority guidance ahead of advisory notes).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolMemoryPriority {
    /// Soft suggestion — surfaced on demand, not eagerly injected.
    Normal,
    /// Important guidance — eagerly injected at tool-selection time.
    High,
    /// Safety-critical rule — pinned into the (compression-resistant)
    /// system prompt so it survives the agent's full session.
    Critical,
}

impl Default for ToolMemoryPriority {
    fn default() -> Self {
        Self::Normal
    }
}

impl ToolMemoryPriority {
    /// True for priorities that must be eagerly surfaced to the agent
    /// (Critical/High rules are both pinned into the system prompt and
    /// prefetched at session start, so they survive context compression).
    pub fn is_eager(self) -> bool {
        matches!(self, Self::Critical | Self::High)
    }
}

/// Where a [`ToolMemoryRule`] originated from.
///
/// Recorded for provenance and so consumers (UI / debugging) can tell user
/// edicts apart from auto-captured observations.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolMemorySource {
    /// User explicitly asked the agent to remember this rule.
    UserExplicit,
    /// Captured automatically from a post-turn observation (tool failure,
    /// repeated correction, etc.).
    PostTurn,
    /// Written by another subsystem (e.g. an integration provisioner).
    Programmatic,
}

impl Default for ToolMemorySource {
    fn default() -> Self {
        Self::Programmatic
    }
}

/// A single tool-scoped memory rule.
///
/// Stored under the `tool-{tool_name}` namespace as a KV entry keyed by
/// `rule/{rule_id}`. The id is stable across updates so callers can
/// upsert by replaying the same id.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolMemoryRule {
    /// Stable identifier within `(tool_name)`. Generated by callers via
    /// [`ToolMemoryRule::generate_id`] when one is not supplied.
    pub id: String,
    /// Tool this rule applies to (e.g. `email`, `shell`).
    pub tool_name: String,
    /// Natural-language guidance that should reach the agent.
    pub rule: String,
    /// Criticality level for retrieval and compression behaviour.
    #[serde(default)]
    pub priority: ToolMemoryPriority,
    /// Where this rule came from.
    #[serde(default)]
    pub source: ToolMemorySource,
    /// Optional free-form tags for filtering (e.g. `safety`, `permission`).
    #[serde(default)]
    pub tags: Vec<String>,
    /// RFC3339 timestamp of when the rule was first written.
    pub created_at: String,
    /// RFC3339 timestamp of the last update.
    pub updated_at: String,
}

impl ToolMemoryRule {
    /// Build a new rule with a freshly generated id and `created_at` /
    /// `updated_at` set to "now".
    pub fn new(
        tool_name: impl Into<String>,
        rule: impl Into<String>,
        priority: ToolMemoryPriority,
        source: ToolMemorySource,
    ) -> Self {
        let now = chrono::Utc::now().to_rfc3339();
        Self {
            id: Self::generate_id(),
            tool_name: tool_name.into(),
            rule: rule.into(),
            priority,
            source,
            tags: Vec::new(),
            created_at: now.clone(),
            updated_at: now,
        }
    }

    /// Generate a fresh, opaque rule id.
    pub fn generate_id() -> String {
        uuid::Uuid::new_v4().to_string()
    }

    /// Storage key used inside the tool namespace.
    pub fn storage_key(id: &str) -> String {
        format!("rule/{id}")
    }
}

/// Namespace string for a given tool. Trimmed and lower-cased so callers
/// can pass user-supplied tool names without leaking whitespace into
/// downstream queries.
///
/// The `tool-` prefix is intentionally distinct from `global`, `skill-…`
/// and `tool_effectiveness` so retrieval and clearing operations can
/// reason about the namespace without ambiguity.
pub fn tool_memory_namespace(tool_name: &str) -> String {
    format!("tool-{}", tool_name.trim().to_lowercase())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn priority_default_is_normal() {
        assert_eq!(ToolMemoryPriority::default(), ToolMemoryPriority::Normal);
    }

    #[test]
    fn priority_ordering_puts_critical_above_high() {
        assert!(ToolMemoryPriority::Critical > ToolMemoryPriority::High);
        assert!(ToolMemoryPriority::High > ToolMemoryPriority::Normal);
    }

    #[test]
    fn priority_is_eager_for_high_and_critical_only() {
        assert!(ToolMemoryPriority::Critical.is_eager());
        assert!(ToolMemoryPriority::High.is_eager());
        assert!(!ToolMemoryPriority::Normal.is_eager());
    }

    #[test]
    fn priority_snake_case_serde() {
        assert_eq!(
            serde_json::to_string(&ToolMemoryPriority::Critical).unwrap(),
            "\"critical\""
        );
        assert_eq!(
            serde_json::to_string(&ToolMemoryPriority::Normal).unwrap(),
            "\"normal\""
        );
    }

    #[test]
    fn source_default_is_programmatic() {
        assert_eq!(ToolMemorySource::default(), ToolMemorySource::Programmatic);
    }

    #[test]
    fn rule_new_fills_id_and_timestamps() {
        let rule = ToolMemoryRule::new(
            "email",
            "never email Sarah",
            ToolMemoryPriority::Critical,
            ToolMemorySource::UserExplicit,
        );
        assert!(!rule.id.is_empty());
        assert_eq!(rule.tool_name, "email");
        assert_eq!(rule.rule, "never email Sarah");
        assert_eq!(rule.priority, ToolMemoryPriority::Critical);
        assert_eq!(rule.source, ToolMemorySource::UserExplicit);
        assert!(rule.created_at == rule.updated_at);
    }

    #[test]
    fn rule_generate_id_produces_unique_values() {
        let a = ToolMemoryRule::generate_id();
        let b = ToolMemoryRule::generate_id();
        assert_ne!(a, b);
    }

    #[test]
    fn rule_storage_key_uses_rule_prefix() {
        assert_eq!(ToolMemoryRule::storage_key("abc"), "rule/abc");
    }

    #[test]
    fn rule_serde_roundtrip_preserves_fields() {
        let rule = ToolMemoryRule {
            id: "id-1".into(),
            tool_name: "shell".into(),
            rule: "never run sudo".into(),
            priority: ToolMemoryPriority::High,
            source: ToolMemorySource::PostTurn,
            tags: vec!["safety".into()],
            created_at: "2026-05-11T00:00:00Z".into(),
            updated_at: "2026-05-11T00:00:01Z".into(),
        };
        let json = serde_json::to_string(&rule).unwrap();
        let back: ToolMemoryRule = serde_json::from_str(&json).unwrap();
        assert_eq!(back, rule);
    }

    #[test]
    fn namespace_uses_tool_prefix_and_trims_whitespace() {
        assert_eq!(tool_memory_namespace("email"), "tool-email");
        assert_eq!(tool_memory_namespace("  shell  "), "tool-shell");
        assert_eq!(tool_memory_namespace("Send_Email"), "tool-send_email");
        assert_eq!(tool_memory_namespace("WebSearch"), "tool-websearch");
    }
}
