//! Data-driven agent definitions.
//!
//! An [`AgentDefinition`] fully specifies a sub-agent: its core prompt, model,
//! allowed tool set, runtime limits, and which sections of the parent system
//! prompt to omit. Built-in definitions live in
//! [`crate::openhuman::agent::agents`] — one subfolder per agent, each
//! holding an `agent.toml` (metadata) and `prompt.md` (system prompt). A
//! thin wrapper in [`super::builtin_definitions`] loads them and appends
//! the synthetic `fork` definition. Users can ship custom definitions as
//! TOML files under `$OPENHUMAN_WORKSPACE/agents/*.toml` (with a fallback
//! to `~/.openhuman/agents/*.toml` for user-global specialists) which
//! override built-ins on id collision. See [`super::definition_loader`]
//! for the directory scan + TOML parsing contract.
//!
//! Sub-agents are dispatched at runtime by the `spawn_subagent` tool, which
//! looks up an [`AgentDefinition`] by id in the global
//! [`AgentDefinitionRegistry`] and hands it to
//! [`super::subagent_runner::run_subagent`].
//!
//! This file intentionally has zero references to the rest of the agent
//! runtime — it is pure data so the model can be unit-tested in isolation
//! and serialised straight from disk.

use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

// ─────────────────────────────────────────────────────────────────────────────
// Agent definition
// ─────────────────────────────────────────────────────────────────────────────

/// A fully specified sub-agent archetype: what it knows, what it can do, and how to prompt it.
///
/// Definitions are used by the `spawn_subagent` tool to initialize a new
/// specialized agent. They can be built-in or loaded from custom TOML files.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDefinition {
    // ── identity ────────────────────────────────────────────────────────
    /// Unique identifier for this archetype (e.g., `researcher`, `code_executor`).
    pub id: String,

    /// Human-readable description explaining when this agent should be used.
    /// Shown to the parent model to help it decide whether to delegate.
    pub when_to_use: String,

    /// Optional display name for UI and log output.
    #[serde(default)]
    pub display_name: Option<String>,

    // ── prompt ──────────────────────────────────────────────────────────
    /// The core system prompt body for this specialized agent.
    #[serde(default = "defaults::empty_inline_prompt")]
    pub system_prompt: PromptSource,

    /// If `true`, the parent's identity section is stripped from the prompt.
    #[serde(default = "defaults::true_")]
    pub omit_identity: bool,

    /// If `true`, the parent's memory context is stripped.
    #[serde(default = "defaults::true_")]
    pub omit_memory_context: bool,

    /// If `true`, the standard safety preamble is stripped.
    #[serde(default = "defaults::true_")]
    pub omit_safety_preamble: bool,

    /// If `true`, the global skills catalog is stripped.
    #[serde(default = "defaults::true_")]
    pub omit_skills_catalog: bool,

    /// If `true`, the user's `PROFILE.md` (generated by the onboarding
    /// enrichment pipeline — LinkedIn scrape, etc.) is NOT injected into
    /// the rendered prompt. Defaults to `true` so sub-agents stay lean:
    /// only agents that need to personalise user-facing output (welcome,
    /// orchestrator, the trigger pair) opt in with `omit_profile = false`.
    #[serde(default = "defaults::true_")]
    pub omit_profile: bool,

    /// If `true`, the archivist-curated `MEMORY.md` (long-term distilled
    /// memory file) is NOT injected into the rendered prompt. Defaults
    /// to `true` for the same reason as `omit_profile` — narrow
    /// specialists stay lean; user-facing agents opt in.
    ///
    /// **KV-cache contract:** like every workspace file, once MEMORY.md
    /// is rendered into a session's system prompt the bytes are frozen
    /// for that session's lifetime. Archivist writes that land
    /// mid-session do not retroactively update the in-flight prompt —
    /// they are picked up on the next session. This matches the
    /// byte-stability invariant documented on
    /// [`crate::openhuman::context::prompt::render_subagent_system_prompt`].
    #[serde(default = "defaults::true_")]
    pub omit_memory_md: bool,

    // ── model ───────────────────────────────────────────────────────────
    /// Strategy for picking which model to use for this sub-agent.
    #[serde(default)]
    pub model: ModelSpec,

    /// Sampling temperature for the model.
    #[serde(default = "defaults::subagent_temperature")]
    pub temperature: f64,

    // ── tools ───────────────────────────────────────────────────────────
    /// Which tools from the parent's registry should be available to the sub-agent.
    #[serde(default)]
    pub tools: ToolScope,

    /// Explicit list of tool names to block, even if they match the scope.
    #[serde(default)]
    pub disallowed_tools: Vec<String>,

    /// Filter to only tools belonging to a specific skill (e.g., `notion`).
    #[serde(default)]
    pub skill_filter: Option<String>,

    /// Named tools that should always be visible to this agent in
    /// addition to its [`ToolScope`]. Historically this was a bypass
    /// list for the now-removed `category_filter`; kept as a generic
    /// "also include these" hook for custom definitions.
    ///
    /// Entries are still subject to [`AgentDefinition::disallowed_tools`].
    #[serde(default)]
    pub extra_tools: Vec<String>,

    // ── runtime limits ──────────────────────────────────────────────────
    /// Maximum number of tool iterations for this sub-agent's task.
    #[serde(default = "defaults::max_iterations")]
    pub max_iterations: usize,

    /// Maximum character length for this sub-agent's output before the
    /// harness truncates it before feeding it back as a tool result to the
    /// parent. `None` means no cap (the default for most agents). Set to
    /// a value for research/planner/code agents to prevent context flooding
    /// from large outputs.
    #[serde(default)]
    pub max_result_chars: Option<usize>,

    /// Wall-clock timeout for the sub-agent's execution (seconds).
    #[serde(default)]
    pub timeout_secs: Option<u64>,

    /// Sandbox level for tool execution.
    #[serde(default)]
    pub sandbox_mode: SandboxMode,

    /// Reserved for background (asynchronous) execution support.
    #[serde(default)]
    pub background: bool,

    // ── delegation surface ─────────────────────────────────────────────
    /// Subagents this agent is allowed to spawn via synthesised
    /// `delegate_*` tools. Each entry expands at agent-build time into
    /// one tool the LLM can call in its function-calling schema:
    ///
    /// * [`SubagentEntry::AgentId`] — one [`ArchetypeDelegationTool`]
    ///   whose name defaults to `delegate_{agent_id}` (or the target
    ///   agent's `delegate_name` override) and whose description is the
    ///   target agent's [`AgentDefinition::when_to_use`].
    ///
    /// * [`SubagentEntry::Skills`] — one [`SkillDelegationTool`] per
    ///   connected Composio toolkit, each named `delegate_{toolkit}`,
    ///   all routing to the generic `integrations_agent` with an appropriate
    ///   `skill_filter` pre-populated.
    ///
    /// `subagents` is intentionally separate from [`AgentDefinition::tools`]
    /// so that reading a TOML makes the distinction obvious: `tools` is
    /// "what I execute directly", `subagents` is "what I can delegate to".
    ///
    /// [`ArchetypeDelegationTool`]: crate::openhuman::tools::impl::agent::ArchetypeDelegationTool
    /// [`SkillDelegationTool`]: crate::openhuman::tools::impl::agent::SkillDelegationTool
    #[serde(default)]
    pub subagents: Vec<SubagentEntry>,

    /// Optional override for the tool name this agent is exposed as when
    /// another agent lists it in its [`subagents`]. Defaults to
    /// `delegate_{id}` when absent. Kept separate from `display_name` so
    /// the UI display and the LLM tool name can diverge (e.g.
    /// `display_name = "Researcher"`, `delegate_name = "research"`).
    #[serde(default)]
    pub delegate_name: Option<String>,

    // ── source bookkeeping ──────────────────────────────────────────────
    /// Tracks where the definition was loaded from (Builtin vs. File).
    #[serde(skip)]
    pub source: DefinitionSource,
}

// ─────────────────────────────────────────────────────────────────────────────
// Subagent delegation entries
// ─────────────────────────────────────────────────────────────────────────────

/// One entry in [`AgentDefinition::subagents`]. Parses from TOML as either
/// a bare string (agent id) or an inline table (`{ skills = "*" }`) thanks
/// to `#[serde(untagged)]`.
///
/// # TOML shapes
///
/// ```toml
/// subagents = [
///     "researcher",            # AgentId("researcher")
///     "code_executor",         # AgentId("code_executor")
///     { skills = "*" },        # Skills { pattern: "*" }
/// ]
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum SubagentEntry {
    /// Delegate to a specific built-in or custom agent by id.
    AgentId(String),
    /// Expand at build time to one `delegate_{toolkit}` tool per
    /// connected Composio toolkit, each routing to the generic
    /// `integrations_agent` with `skill_filter` pre-set.
    Skills(SkillsWildcard),
}

/// The `{ skills = "*" }` inline table in a `subagents` list.
///
/// Today only `"*"` is meaningful (expand to every connected toolkit).
/// Future: a `Vec<String>` variant to restrict expansion to specific
/// toolkit slugs (e.g. `{ skills = ["gmail", "notion"] }`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkillsWildcard {
    /// Glob / wildcard pattern. Only `"*"` is currently supported.
    pub skills: String,
}

impl SkillsWildcard {
    /// True when this wildcard should expand to every connected toolkit.
    pub fn matches_all(&self) -> bool {
        self.skills == "*"
    }
}

impl AgentDefinition {
    /// Display name with fallback to id.
    pub fn display_name(&self) -> &str {
        self.display_name.as_deref().unwrap_or(&self.id)
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Prompt source
// ─────────────────────────────────────────────────────────────────────────────

/// Builder function signature for [`PromptSource::Dynamic`]. Takes the
/// full runtime [`crate::openhuman::context::prompt::PromptContext`]
/// (tools, skills, memory, connected integrations, dispatcher, model,
/// …) and returns the final system prompt body — typically assembled
/// by calling the `render_*` section helpers in
/// [`crate::openhuman::context::prompt`] in the order the builder
/// wants.
pub type PromptBuilder =
    fn(&crate::openhuman::context::prompt::PromptContext<'_>) -> anyhow::Result<String>;

/// Where the sub-agent's core system prompt comes from.
#[derive(Clone)]
pub enum PromptSource {
    /// Inline prompt string (custom TOML-defined agents).
    Inline(String),
    /// Relative path under the workspace's `prompts/` directory or under
    /// `src/openhuman/agent/prompts/` for built-ins. Resolved by the runner
    /// at spawn time.
    File { path: String },
    /// Function-driven prompt: the builder is invoked at spawn time with
    /// a [`PromptContext`] so the returned body can depend on runtime
    /// state (available tools, user profile, connected skills, etc.).
    ///
    /// Only constructed in-process (by built-in agent loaders). Not
    /// deserializable from TOML — TOML-authored agents must use `inline`
    /// or `file`.
    Dynamic(PromptBuilder),
}

impl std::fmt::Debug for PromptSource {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PromptSource::Inline(s) => f.debug_tuple("Inline").field(&s).finish(),
            PromptSource::File { path } => f.debug_struct("File").field("path", path).finish(),
            PromptSource::Dynamic(_) => f.debug_tuple("Dynamic").field(&"<fn>").finish(),
        }
    }
}

impl Serialize for PromptSource {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut map = serializer.serialize_map(Some(1))?;
        match self {
            PromptSource::Inline(s) => map.serialize_entry("inline", s)?,
            PromptSource::File { path } => {
                #[derive(Serialize)]
                struct FileBody<'a> {
                    path: &'a str,
                }
                map.serialize_entry("file", &FileBody { path })?;
            }
            // Opaque marker — runtime-only. Round-trips back through
            // Deserialize would produce an error (Dynamic is unsupported
            // there) which is intentional: RPC consumers treat Dynamic
            // sources as "built-in, runtime-generated".
            PromptSource::Dynamic(_) => map.serialize_entry("dynamic", &serde_json::Value::Null)?,
        }
        map.end()
    }
}

impl<'de> Deserialize<'de> for PromptSource {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        #[derive(Deserialize)]
        #[serde(rename_all = "snake_case")]
        enum Shape {
            Inline(String),
            File { path: String },
        }
        Shape::deserialize(deserializer).map(|s| match s {
            Shape::Inline(body) => PromptSource::Inline(body),
            Shape::File { path } => PromptSource::File { path },
        })
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Model spec
// ─────────────────────────────────────────────────────────────────────────────

/// Model selection for a sub-agent.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ModelSpec {
    /// Use the parent agent's currently-selected model at spawn time.
    #[default]
    Inherit,
    /// Exact model name (e.g. `"neocortex-mk1"`).
    Exact(String),
    /// Router hint (e.g. `"reasoning"`, `"coding"`, `"local"`). Resolved
    /// to a real model by the routing provider.
    Hint(String),
}

impl ModelSpec {
    /// Resolve this spec into the model name string the provider expects.
    /// `parent_model` is the model the parent agent is using right now.
    ///
    /// Hints are resolved to `{hint}-v1` (e.g. `"agentic"` → `"agentic-v1"`)
    /// which matches the backend's standard model naming convention. When
    /// a `RouterProvider` is present its route table takes priority over
    /// this default; when no router is configured (empty `model_routes`)
    /// the resolved name goes directly to the backend.
    pub fn resolve(&self, parent_model: &str) -> String {
        match self {
            Self::Inherit => parent_model.to_string(),
            Self::Exact(name) => name.clone(),
            Self::Hint(hint) => format!("{hint}-v1"),
        }
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tool scope
// ─────────────────────────────────────────────────────────────────────────────

/// Which tools a sub-agent is allowed to call.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ToolScope {
    /// All tools the parent has (subject to `disallowed_tools` and
    /// `skill_filter`).
    #[default]
    Wildcard,
    /// An explicit allowlist of tool names. Names not present in the parent
    /// registry at spawn time are silently dropped (logged at debug).
    Named(Vec<String>),
}

// ─────────────────────────────────────────────────────────────────────────────
// Sandbox mode
// ─────────────────────────────────────────────────────────────────────────────

/// Sandbox mode for a sub-agent's tool execution. Serialises as a simple
/// `snake_case` string in TOML (`none` / `read_only` / `sandboxed`). In
/// the future this may map directly into a `SecurityPolicy` builder.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum SandboxMode {
    /// No additional sandboxing beyond what the parent already enforces.
    #[default]
    None,
    /// Read-only — write/execute tools are filtered out.
    ReadOnly,
    /// Drop privileges, restrict filesystem (Landlock / Bubblewrap).
    Sandboxed,
}

// ─────────────────────────────────────────────────────────────────────────────
// Definition source
// ─────────────────────────────────────────────────────────────────────────────

/// Where an [`AgentDefinition`] was loaded from. Used for telemetry and
/// the `agent::list_definitions` RPC reply.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(tag = "kind", content = "path")]
pub enum DefinitionSource {
    /// Built-in definition shipped as part of the binary (loaded from
    /// [`crate::openhuman::agent::agents`]).
    #[default]
    Builtin,
    /// Loaded from a TOML file at the given absolute path.
    File(PathBuf),
}

// ─────────────────────────────────────────────────────────────────────────────
// Defaults module — referenced by `#[serde(default = ...)]`
// ─────────────────────────────────────────────────────────────────────────────

pub(crate) mod defaults {
    use super::PromptSource;

    pub(crate) fn true_() -> bool {
        true
    }

    pub(crate) fn subagent_temperature() -> f64 {
        0.4
    }

    pub(crate) fn max_iterations() -> usize {
        8
    }

    /// Placeholder for [`super::AgentDefinition::system_prompt`] when the
    /// TOML omits the field. The built-in loader overwrites this with
    /// the rendered sibling `prompt.md`; custom TOMLs that omit the
    /// field get a no-op empty prompt (and should not).
    pub(crate) fn empty_inline_prompt() -> PromptSource {
        PromptSource::Inline(String::new())
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Registry
// ─────────────────────────────────────────────────────────────────────────────

use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use std::sync::OnceLock;

/// In-memory registry of all known [`AgentDefinition`]s.
///
/// One singleton instance is initialised at startup via
/// [`AgentDefinitionRegistry::init_global`]. Built-ins are registered
/// unconditionally; custom TOML definitions (if a workspace is provided)
/// are loaded next and override built-ins on `id` collision.
#[derive(Debug, Default)]
pub struct AgentDefinitionRegistry {
    by_id: HashMap<String, AgentDefinition>,
    /// Insertion-stable order for predictable `list()` output.
    order: Vec<String>,
}

static GLOBAL: OnceLock<AgentDefinitionRegistry> = OnceLock::new();

impl AgentDefinitionRegistry {
    /// Build a registry containing only the built-in definitions
    /// (no TOML loading). Useful for tests.
    pub fn builtins_only() -> Self {
        let mut reg = Self::default();
        for def in super::builtin_definitions::all() {
            reg.insert(def);
        }
        reg
    }

    /// Build a registry containing built-ins plus any custom TOML
    /// definitions found under `<workspace>/agents/*.toml` (and the
    /// `~/.openhuman/agents/*.toml` fallback). Custom definitions
    /// override built-ins on `id` collision. Files that fail to parse
    /// are logged and skipped rather than aborting startup.
    pub fn load(workspace: &Path) -> Result<Self> {
        let mut reg = Self::builtins_only();
        let custom = super::definition_loader::load_from_workspace(workspace)?;
        for def in custom {
            tracing::info!(
                id = %def.id,
                source = ?def.source,
                "[agent_defs] loaded custom definition (overrides any built-in with the same id)"
            );
            reg.insert(def);
        }
        Ok(reg)
    }

    /// Convenience: resolve the default workspace via
    /// [`crate::openhuman::config::Config::load_or_init`] and load from
    /// it. Built for sync CLI call sites (`openhuman agent list`,
    /// future inspection tools) so they don't re-implement the Config
    /// → workspace resolution dance. Must NOT be called from an
    /// existing tokio runtime — construct a runtime and `block_on`.
    pub async fn load_for_default_workspace() -> Result<Self> {
        let config = crate::openhuman::config::Config::load_or_init().await?;
        Self::load(&config.workspace_dir)
    }

    /// Insert (or replace) a definition by id.
    pub fn insert(&mut self, def: AgentDefinition) {
        let id = def.id.clone();
        if self.by_id.insert(id.clone(), def).is_none() {
            self.order.push(id);
        }
    }

    /// Look up a definition by id.
    pub fn get(&self, id: &str) -> Option<&AgentDefinition> {
        self.by_id.get(id)
    }

    /// All definitions, in insertion order.
    pub fn list(&self) -> Vec<&AgentDefinition> {
        self.order
            .iter()
            .filter_map(|id| self.by_id.get(id))
            .collect()
    }

    /// Number of registered definitions.
    pub fn len(&self) -> usize {
        self.by_id.len()
    }

    /// True when the registry has no definitions.
    pub fn is_empty(&self) -> bool {
        self.by_id.is_empty()
    }

    // ── singleton API ──────────────────────────────────────────────────

    /// Initialise the global registry. Subsequent calls are no-ops (the
    /// `OnceLock` only fires once); use [`Self::reload_global`] to refresh
    /// custom definitions during development.
    pub fn init_global(workspace: &Path) -> Result<()> {
        let registry = Self::load(workspace)?;
        match GLOBAL.set(registry) {
            Ok(()) => {
                tracing::info!(
                    "[agent_defs] global registry initialised with {} definitions",
                    GLOBAL.get().map(|r| r.len()).unwrap_or(0)
                );
                Ok(())
            }
            Err(_) => {
                tracing::debug!("[agent_defs] global registry already initialised; ignoring");
                Ok(())
            }
        }
    }

    /// Initialise the global registry with builtins only (no workspace
    /// scan). Used by tests and by callers that don't have a workspace.
    pub fn init_global_builtins() -> Result<()> {
        let registry = Self::builtins_only();
        let _ = GLOBAL.set(registry);
        Ok(())
    }

    /// Borrow the global registry, if initialised.
    pub fn global() -> Option<&'static Self> {
        GLOBAL.get()
    }
}

#[cfg(test)]
#[path = "definition_tests.rs"]
mod tests;
