//! Parslee Core — the flagship, general-purpose agent that ships in the `car`
//! binary and works out of the box (`car do`).
//!
//! Unlike the coder (coding-specific) or the create-car-agent skill (build your
//! own), this is a batteries-included assistant: files + a real shell + web +
//! durable memory, sandbox-first for safety, driven by CAR inference through a
//! full [`Runtime`] (validator, policy, permission tiers, event log). One core
//! backs three entry modes — one-shot, REPL, and the conversational
//! `agent.chat` surface.
//!
//! ## Module map
//! - [`executor`] — [`GeneralExecutor`], the substrate-bound tool executor
//!   (agent_basics + `calculate` + `shell` + network delegate).
//! - [`net_tools`] — host-side `http_request` / `web_search` (bypass the
//!   sandbox's `--network none`).
//! - [`substrate`] — sandbox-first environment selection ([`bind_default_substrate`]).
//! - [`policy`] — the assistant inspector chain (reuses the coder's footgun set).
//! - [`prompt`] — batch vs. conversational system prompts.
//! - [`agent_loop`] — the propose→validate→execute→observe loop.
//!
//! [`Runtime`]: car_engine::Runtime
//! [`GeneralExecutor`]: executor::GeneralExecutor
//! [`bind_default_substrate`]: substrate::bind_default_substrate

pub mod agent_loop;
pub mod automation_tools;
pub mod chat;
pub mod device_tools;
pub mod executor;
pub mod media_tools;
pub mod memory;
pub mod net_tools;
pub mod policy;
pub mod prompt;
pub mod register;
pub mod studio_tools;
pub mod substrate;
pub mod vision_tools;

use std::path::PathBuf;
use std::sync::Arc;

use async_trait::async_trait;
use car_engine::{Runtime, ToolEntry, ToolExecutor, ToolSchema};
use car_eventlog::EventLog;
use car_inference::InferenceEngine;
use car_policy::permission::PermissionTier;
use serde_json::Value;

use memory::MemoryTools;

pub use agent_loop::{
    run_assistant_goal_loop, run_assistant_loop, run_assistant_loop_cancellable, ApprovalDecision,
    ApprovalGate, AssistantConfig, AssistantEvent, AssistantOutcome, GoalLoopResult,
};
pub use chat::{AssistantService, ChatGoal};
pub use device_tools::DeviceProvider;
pub use executor::GeneralExecutor;
pub use net_tools::NetTools;
pub use substrate::{bind_default_substrate, BoundEnvironment};

/// An assembled assistant runtime: the [`Runtime`] to drive, the model-visible
/// tool list, and the environment metadata for the system prompt.
pub struct AssistantRuntime {
    /// The configured runtime (validator + policy + tiers + event log), whose
    /// tool executor is the [`GeneralExecutor`].
    pub runtime: Runtime,
    /// The model-visible tools (from `GeneralExecutor::all_tool_defs()`).
    pub tools: Vec<Value>,
    /// One-line environment description for the system prompt.
    pub description: String,
    /// Whether execution is isolated in a container.
    pub sandboxed: bool,
    /// Tools that require human approval before running under the standing tier
    /// (writes/shell on the local host without `--full-access`); empty when the
    /// tier auto-allows everything. Feed into `AssistantConfig::gated_tools`.
    pub gated_tools: Vec<String>,
    /// If the sandbox was requested but unavailable, why we fell back to local.
    pub fallback_notice: Option<String>,
}

/// A delegate that tries each inner executor in turn, using the `unknown tool`
/// convention to fall through — so several tool families (network, memory) share
/// one `GeneralExecutor` delegate slot.
struct ChainedDelegate(Vec<Arc<dyn ToolExecutor>>);

#[async_trait]
impl ToolExecutor for ChainedDelegate {
    async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
        for ex in &self.0 {
            match ex.execute(tool, params).await {
                Err(e) if e.starts_with("unknown tool") => continue,
                other => return other,
            }
        }
        Err(format!("unknown tool: '{tool}'"))
    }
}

/// Build a registry [`ToolSchema`] from a model-facing `{name, description,
/// parameters}` def, so any advertised tool can be registered for validation.
fn schema_from_def(def: &Value) -> ToolSchema {
    ToolSchema {
        name: def["name"].as_str().unwrap_or_default().to_string(),
        description: def["description"].as_str().unwrap_or_default().to_string(),
        parameters: def["parameters"].clone(),
        returns: None,
        idempotent: false,
        cache_ttl_secs: None,
        rate_limit: None,
    }
}

/// The names of advertised tools whose self-declared `"tier"` exceeds the
/// standing `tier` — these must be approval-gated (neo leak #3). A tool without
/// a `"tier"` field, or one at/below the standing tier, is not gated here.
fn tier_gated_tool_names(tools: &[Value], standing: PermissionTier) -> Vec<String> {
    tools
        .iter()
        .filter_map(|def| {
            let name = def.get("name").and_then(|v| v.as_str())?;
            let tier = PermissionTier::from_str_opt(def.get("tier").and_then(|v| v.as_str())?)?;
            (tier > standing).then(|| name.to_string())
        })
        .collect()
}

/// Default durable-memory path: `~/.car/memory/assistant.json`
/// (HOME, or USERPROFILE on Windows).
fn default_memory_path() -> PathBuf {
    let home = std::env::var_os("HOME")
        .or_else(|| std::env::var_os("USERPROFILE"))
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    home.join(".car").join("memory").join("assistant.json")
}

/// Assemble an [`AssistantRuntime`] from an engine and a bound environment.
///
/// Registers the model-visible tools so the validator allows them (agent_basics
/// builtins + `shell` + `http_request` + `web_search`), binds the
/// [`GeneralExecutor`] as the executor and the environment's substrate, and
/// attaches an optional event-log journal.
pub async fn build_assistant_runtime(
    engine: Arc<InferenceEngine>,
    env: BoundEnvironment,
    eventlog: Option<PathBuf>,
    device_provider: Option<Arc<dyn DeviceProvider>>,
) -> AssistantRuntime {
    // Mutations auto-allow unless the standing tier is ReadOnly (local host
    // without --full-access), in which case writes/shell need approval; clamp
    // file paths to root only off-sandbox.
    let mut gated_tools: Vec<String> = if matches!(env.tier, PermissionTier::ReadOnly) {
        ["write_file", "edit_file", "shell"]
            .iter()
            .map(|s| s.to_string())
            .collect()
    } else {
        Vec::new()
    };
    let clamp = !env.sandboxed;
    let description = env.description.clone();
    let sandboxed = env.sandboxed;
    let fallback_notice = env.fallback_notice.clone();

    // Delegate tools (host-side): network + durable memory. Both bypass the
    // substrate/path-clamp — network needs host egress, memory is CAR's graph.
    let net: Arc<dyn ToolExecutor> = Arc::new(NetTools::new());
    let mem: Arc<dyn ToolExecutor> = Arc::new(MemoryTools::open(default_memory_path()));
    // Media generation (image today) — host-side, backed by the inference
    // engine's local models. Advertises nothing when no image model is
    // available, so it never offers a tool it can't run. The capability a
    // text-only agent structurally cannot have.
    let media = Arc::new(media_tools::MediaTools::new(
        engine.clone(),
        env.root.clone(),
    ));
    // Parslee Studio media (music today) — host-side, via the Studio service on
    // CAR's existing Parslee bearer. Advertises nothing without a Parslee
    // session. Another capability a text-only agent structurally lacks.
    let studio = Arc::new(studio_tools::StudioMediaTools::new(env.root.clone()));
    // Vision (image understanding) — host-side, via Apple Vision / Tesseract.
    // The CONSUMER counterpart to the generators: read text from an image (OCR)
    // and classify what it depicts. Advertises nothing when no vision backend is
    // present. A text-only agent can neither make nor read an image.
    let vision = Arc::new(vision_tools::VisionTools::new(env.root.clone()));
    // macOS automation ("control the Mac", AppleScript/JXA) — host-side, CANNOT
    // be sandboxed. Self-declares tier:full_access, so the tier-based gating
    // below routes it through approval unless the session is --full-access. A
    // capability no sandboxed or text-only agent has.
    let automation = Arc::new(automation_tools::AutomationTools::new());
    let device_tools = device_provider
        .map(device_tools::DeviceTools::new)
        .map(Arc::new);
    let mut delegate_defs = net_tools::net_tool_defs();
    delegate_defs.extend(MemoryTools::tool_defs());
    delegate_defs.extend(media.tool_defs());
    delegate_defs.extend(studio.tool_defs());
    delegate_defs.extend(vision.tool_defs());
    delegate_defs.extend(automation.tool_defs());
    if device_tools.is_some() {
        delegate_defs.extend(device_tools::DeviceTools::tool_defs());
    }
    let media: Arc<dyn ToolExecutor> = media;
    let studio: Arc<dyn ToolExecutor> = studio;
    let vision: Arc<dyn ToolExecutor> = vision;
    let automation: Arc<dyn ToolExecutor> = automation;
    let mut delegates: Vec<Arc<dyn ToolExecutor>> =
        vec![net, mem, media, studio, vision, automation];
    if let Some(device_tools) = device_tools {
        delegates.push(device_tools);
    }
    let delegate: Arc<dyn ToolExecutor> = Arc::new(ChainedDelegate(delegates));

    let executor = GeneralExecutor::new(env.substrate.clone(), env.root.clone(), clamp)
        .with_delegate(delegate, delegate_defs);
    let tools = executor.all_tool_defs();
    // Tier-based approval gating (neo leak #3): any advertised tool that
    // self-declares a `"tier"` ABOVE the standing tier (e.g. a full_access
    // automation tool in a non-full-access session) must route through the
    // approval gate. Derived from the defs, so a new capability gates itself
    // without editing this function.
    gated_tools.extend(tier_gated_tool_names(&tools, env.tier));
    let executor: Arc<dyn ToolExecutor> = Arc::new(executor);

    let mut runtime = Runtime::new()
        .with_inference(engine)
        .with_executor(executor)
        .with_substrate(env.substrate.clone());
    if let Some(path) = eventlog {
        if let Some(parent) = path.parent() {
            let _ = std::fs::create_dir_all(parent);
        }
        runtime = runtime.with_event_log(EventLog::with_journal(path));
    }

    // OpenClaw-style personal assistants fail dangerously when persistent,
    // high-privilege context can flow straight into outbound tools. Install
    // CAR's verified information-flow gate by default: built-in labels mark
    // network tools as exfiltration sinks, and projects can refine source
    // confidentiality in `.car/tool-labels.json`.
    if let Err(e) = runtime
        .install_information_flow_gate(env.root.join(".car"))
        .await
    {
        tracing::warn!(
            error = %e,
            "assistant could not load project tool labels; falling back to built-in information-flow labels"
        );
        runtime
            .register_admission_gate(Arc::new(
                car_engine::InformationFlowGate::with_builtin_labels(),
            ))
            .await;
    }

    // Register the model-visible tools so the validator admits them. Execution
    // is owned by the GeneralExecutor above; these registrations are for
    // validation + schema listing. agent_basics covers the file/calculate
    // builtins; everything else advertised (shell, http_request, web_search,
    // remember, recall) is registered from its advertised def.
    runtime.register_agent_basics().await;
    let builtin_names: std::collections::HashSet<String> = car_engine::agent_basic_entries()
        .into_iter()
        .map(|e| e.schema.name)
        .collect();
    for def in &tools {
        let name = def["name"].as_str().unwrap_or_default();
        if name.is_empty() || builtin_names.contains(name) {
            continue;
        }
        runtime
            .register_tool_entry(ToolEntry::new(schema_from_def(def)).with_side_effects(true))
            .await;
    }

    AssistantRuntime {
        runtime,
        tools,
        description,
        sandboxed,
        gated_tools,
        fallback_notice,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use car_eventlog::EventKind;
    use car_ir::{Action, ActionProposal, ActionStatus, ActionType, FailureBehavior};
    use std::collections::HashMap;

    use serde_json::json;

    struct StaticDeviceProvider(Value);

    #[async_trait]
    impl DeviceProvider for StaticDeviceProvider {
        async fn devices(&self) -> Result<Value, String> {
            Ok(self.0.clone())
        }

        async fn notify_device(
            &self,
            device_id: Option<String>,
            title: String,
            body: String,
        ) -> Result<Value, String> {
            Ok(json!({
                "device_id": device_id,
                "title": title,
                "body": body
            }))
        }
    }

    fn test_engine(root: &std::path::Path) -> Arc<InferenceEngine> {
        let mut cfg = car_inference::InferenceConfig::default();
        cfg.models_dir = root.join("models");
        Arc::new(InferenceEngine::new(cfg))
    }

    fn test_env(root: &std::path::Path) -> BoundEnvironment {
        BoundEnvironment {
            substrate: Arc::new(car_engine::LocalSubstrate::new()),
            root: root.to_path_buf(),
            tier: PermissionTier::ReadOnly,
            description: "test local host".to_string(),
            sandboxed: false,
            fallback_notice: None,
        }
    }

    fn test_action(tool: &str) -> Action {
        Action {
            id: uuid::Uuid::new_v4().simple().to_string()[..12].to_string(),
            action_type: ActionType::ToolCall,
            tool: Some(tool.to_string()),
            parameters: HashMap::new(),
            preconditions: vec![],
            expected_effects: HashMap::new(),
            state_dependencies: vec![],
            read_set: vec![],
            write_set: vec![],
            assumptions: vec![],
            invocation_mode: Default::default(),
            idempotent: false,
            max_retries: 3,
            failure_behavior: FailureBehavior::Abort,
            timeout_ms: None,
            metadata: HashMap::new(),
        }
    }

    fn test_proposal(actions: Vec<Action>) -> ActionProposal {
        ActionProposal {
            id: "assistant-test-proposal".to_string(),
            source: "assistant-test".to_string(),
            actions,
            timestamp: chrono::Utc::now(),
            context: HashMap::new(),
        }
    }

    #[test]
    fn tier_gating_derives_from_self_declared_tier() {
        let tools = vec![
            json!({"name": "read_file"}), // no tier → never gated
            json!({"name": "generate_image", "tier": "sandbox_edit"}),
            json!({"name": "web_search", "tier": "full_access"}),
            json!({"name": "run_applescript", "tier": "full_access"}),
        ];

        // A ReadOnly session gates BOTH the sandbox_edit and full_access tools.
        let g = tier_gated_tool_names(&tools, PermissionTier::ReadOnly);
        assert!(g.contains(&"generate_image".to_string()));
        assert!(g.contains(&"web_search".to_string()));
        assert!(g.contains(&"run_applescript".to_string()));
        assert!(!g.contains(&"read_file".to_string()));

        // A SandboxEdit session gates only the full_access tool.
        let g = tier_gated_tool_names(&tools, PermissionTier::SandboxEdit);
        assert_eq!(
            g,
            vec!["web_search".to_string(), "run_applescript".to_string()]
        );

        // A FullAccess session gates nothing by tier.
        assert!(tier_gated_tool_names(&tools, PermissionTier::FullAccess).is_empty());
    }

    #[tokio::test]
    async fn assistant_runtime_installs_information_flow_gate_by_default() {
        let dir = tempfile::tempdir().unwrap();
        let engine = test_engine(dir.path());
        let env = test_env(dir.path());

        let rt = build_assistant_runtime(engine, env, None, None).await;
        assert_eq!(rt.runtime.admission_gate_count().await, 1);
    }

    #[tokio::test]
    async fn assistant_runtime_gates_external_and_persistent_sinks_by_default() {
        let dir = tempfile::tempdir().unwrap();
        let rt = build_assistant_runtime(test_engine(dir.path()), test_env(dir.path()), None, None)
            .await;

        assert!(rt.gated_tools.contains(&"http_request".to_string()));
        assert!(rt.gated_tools.contains(&"web_search".to_string()));
        assert!(rt.gated_tools.contains(&"remember".to_string()));
    }

    #[tokio::test]
    async fn assistant_runtime_can_see_linked_devices_when_provider_supplied() {
        let dir = tempfile::tempdir().unwrap();
        let provider: Arc<dyn DeviceProvider> = Arc::new(StaticDeviceProvider(json!([
            {
                "name": "Mia's iPhone",
                "platform": "ios",
                "status": "online",
                "capabilities": ["assistant.chat", "assistant.approvals"]
            }
        ])));
        let rt = build_assistant_runtime(
            test_engine(dir.path()),
            test_env(dir.path()),
            None,
            Some(provider),
        )
        .await;

        assert!(rt
            .tools
            .iter()
            .any(|def| def["name"].as_str() == Some("linked_devices")));
        assert!(rt
            .tools
            .iter()
            .any(|def| def["name"].as_str() == Some("notify_linked_device")));
        let result = rt
            .runtime
            .execute(&test_proposal(vec![test_action("linked_devices")]))
            .await;
        assert_eq!(result.results[0].status, ActionStatus::Succeeded);
        assert_eq!(
            result.results[0].output.as_ref().unwrap()[0]["platform"],
            "ios"
        );
    }

    #[tokio::test]
    async fn malformed_assistant_tool_labels_still_install_builtin_flow_gate() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::create_dir_all(dir.path().join(".car")).unwrap();
        std::fs::write(dir.path().join(".car/tool-labels.json"), "{not json").unwrap();
        let engine = test_engine(dir.path());
        let env = test_env(dir.path());

        let rt = build_assistant_runtime(engine, env, None, None).await;
        assert_eq!(rt.runtime.admission_gate_count().await, 1);
    }

    #[tokio::test]
    async fn assistant_runtime_rejects_confidential_data_to_web_search() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::create_dir_all(dir.path().join(".car")).unwrap();
        std::fs::write(
            dir.path().join(".car/tool-labels.json"),
            r#"{"labels":{"read_file":{"capability":"fs_read","confidentiality":"secret"}}}"#,
        )
        .unwrap();
        let rt = build_assistant_runtime(test_engine(dir.path()), test_env(dir.path()), None, None)
            .await;

        let mut read = test_action("read_file");
        read.expected_effects = [("file_data".to_string(), json!(true))].into();
        let mut search = test_action("web_search");
        search.state_dependencies = vec!["file_data".to_string()];

        let result = rt.runtime.execute(&test_proposal(vec![read, search])).await;

        assert!(result
            .results
            .iter()
            .all(|r| r.status == ActionStatus::Rejected));
        let log = rt.runtime.log.lock().await;
        assert!(log.events().iter().any(|e| {
            e.kind == EventKind::AdmissionGateDecision
                && e.data.get("gate").and_then(|v| v.as_str()) == Some("information_flow")
                && e.data.get("decision").and_then(|v| v.as_str()) == Some("reject")
        }));
    }

    #[tokio::test]
    async fn assistant_runtime_rejects_recalled_memory_to_web_search_by_default() {
        let dir = tempfile::tempdir().unwrap();
        let rt = build_assistant_runtime(test_engine(dir.path()), test_env(dir.path()), None, None)
            .await;

        let mut recall = test_action("recall");
        recall.expected_effects = [("memory_context".to_string(), json!(true))].into();
        let mut search = test_action("web_search");
        search.state_dependencies = vec!["memory_context".to_string()];

        let result = rt
            .runtime
            .execute(&test_proposal(vec![recall, search]))
            .await;

        assert!(result
            .results
            .iter()
            .all(|r| r.status == ActionStatus::Rejected));
        let log = rt.runtime.log.lock().await;
        assert!(log.events().iter().any(|e| {
            e.kind == EventKind::AdmissionGateDecision
                && e.data.get("gate").and_then(|v| v.as_str()) == Some("information_flow")
                && e.data.get("decision").and_then(|v| v.as_str()) == Some("reject")
        }));
    }
}
