//! nanny — Rust SDK and CLI.
//!
//! # Usage
//!
//! ```toml
//! [dependencies]
//! nanny = "0.1"
//! ```
//!
//! ```rust,ignore
//! use nanny::{tool, rule, agent, PolicyContext};
//!
//! #[tool(cost = 10)]
//! fn search_web(query: &str) -> String { ... }
//!
//! #[rule("no_spiral")]
//! fn check_spiral(ctx: &PolicyContext) -> bool { ... }
//!
//! #[agent("researcher")]
//! fn run_research(topic: &str) { ... }
//! ```

// ── User-facing re-exports ────────────────────────────────────────────────────

/// The type passed to `#[nanny::rule]` functions.
pub use nanny_core::policy::PolicyContext;

/// All possible reasons nanny stopped execution.
pub use nanny_core::agent::state::StopReason;

/// Declare a function as a governed nanny tool.
/// See [`tool`](nanny_macros::tool) for full documentation.
pub use nanny_macros::tool;

/// Register a function as a named enforcement rule.
/// See [`rule`](nanny_macros::rule) for full documentation.
pub use nanny_macros::rule;

/// Activate a named limits set for the duration of a function.
/// See [`agent`](nanny_macros::agent) for full documentation.
pub use nanny_macros::agent;

// ── Built-in bridge tools ─────────────────────────────────────────────────────

/// Fetch a URL via nanny's built-in `http_get` bridge tool.
///
/// The request is executed by the nanny bridge — the calling process never
/// opens a network connection directly. Nanny enforces the allowlist, charges
/// cost (10 units on success), and applies the step limit before making the
/// request.
///
/// # Passthrough mode
///
/// When running outside `nanny run` (no bridge active), returns
/// `Err("bridge unavailable")`. This keeps behaviour predictable: the tool
/// always goes through nanny when a bridge is present.
///
/// # Example
///
/// ```rust,ignore
/// use nanny::PolicyContext;
/// use rig::tool::Tool;
///
/// impl Tool for MyFetchTool {
///     async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
///         tokio::task::spawn_blocking(move || {
///             nanny::http_get(args.url).map_err(MyError)
///         })
///         .await
///         .map_err(|e| MyError(e.to_string()))?
///     }
/// }
/// ```
pub fn http_get(url: String) -> Result<String, String> {
    // Evaluate local rules first — same as #[nanny::tool] does.
    // This ensures #[nanny::rule] functions (e.g. "no_loop") fire for
    // built-in bridge tools, not just developer-defined #[nanny::tool] functions.
    let mut args = std::collections::HashMap::new();
    args.insert("url".to_string(), url.clone());
    if let Some(rule_name) = runtime::evaluate_local_rules("http_get", args) {
        runtime::report_stop("RuleDenied");
        eprintln!("nanny: stopped — RuleDenied: {rule_name}");
        std::process::exit(1);
    }

    let args_json = serde_json::json!({"url": url}).to_string();
    runtime::call_bridge_tool("http_get", &args_json)
}

// ── Private runtime — for generated code only ─────────────────────────────────
//
// Everything below this line is used exclusively by code generated by
// nanny-macros. It is not a public API. Names and signatures may change
// in any release without notice.

#[doc(hidden)]
pub mod __private {
    // Re-export inventory so generated code can call
    // `::nanny::__private::inventory::submit! { ... }` without requiring
    // users to add inventory as a direct dependency.
    pub use ::inventory;

    pub use super::runtime::{
        agent_enter, agent_exit, call_bridge_tool, call_tool, evaluate_local_rules, is_active,
        report_stop, Rule, ToolVerdict,
    };
}

mod runtime {
    use nanny_core::policy::PolicyContext;
    use std::collections::HashMap;
    use std::sync::{Mutex, OnceLock};
    use std::time::Instant;

    // ── Rule registry ─────────────────────────────────────────────────────────

    /// A user-defined enforcement rule, registered via `#[nanny::rule("name")]`.
    pub struct Rule {
        pub name: &'static str,
        pub func: fn(&PolicyContext) -> bool,
    }

    inventory::collect!(Rule);

    // ── Client-side state ──────────────────────────────────────────────────────

    struct ClientState {
        start: Instant,
    }

    impl ClientState {
        fn new() -> Self {
            Self { start: Instant::now() }
        }
    }

    fn client_state() -> &'static Mutex<ClientState> {
        static STATE: OnceLock<Mutex<ClientState>> = OnceLock::new();
        STATE.get_or_init(|| Mutex::new(ClientState::new()))
    }

    // ── Bridge detection ──────────────────────────────────────────────────────

    /// Returns `true` if the bridge is active.
    pub fn is_active() -> bool {
        bridge_socket_path().is_some() || bridge_tcp_port().is_some()
    }

    #[cfg(unix)]
    fn bridge_socket_path() -> Option<std::path::PathBuf> {
        std::env::var("NANNY_BRIDGE_SOCKET").ok().map(std::path::PathBuf::from)
    }

    #[cfg(not(unix))]
    fn bridge_socket_path() -> Option<std::path::PathBuf> {
        None
    }

    fn bridge_tcp_port() -> Option<u16> {
        std::env::var("NANNY_BRIDGE_PORT").ok()?.parse().ok()
    }

    fn session_token() -> String {
        std::env::var("NANNY_SESSION_TOKEN").unwrap_or_default()
    }

    // ── HTTP transport ────────────────────────────────────────────────────────

    struct BridgeResponse {
        status: u16,
        body:   String,
    }

    fn http_get(path: &str) -> Option<BridgeResponse> {
        let token = session_token();
        let req = format!(
            "GET {path} HTTP/1.1\r\n\
             Host: localhost\r\n\
             X-Nanny-Session-Token: {token}\r\n\
             Connection: close\r\n\
             \r\n"
        );

        #[cfg(unix)]
        if let Some(sock) = bridge_socket_path() {
            use std::io::{Read, Write};
            use std::os::unix::net::UnixStream;
            let mut stream = UnixStream::connect(&sock).ok()?;
            stream.write_all(req.as_bytes()).ok()?;
            let mut raw = String::new();
            stream.read_to_string(&mut raw).ok()?;
            return parse_http_response(&raw);
        }

        if let Some(port) = bridge_tcp_port() {
            use std::io::{Read, Write};
            use std::net::TcpStream;
            let mut stream = TcpStream::connect(("127.0.0.1", port)).ok()?;
            stream.write_all(req.as_bytes()).ok()?;
            let mut raw = String::new();
            stream.read_to_string(&mut raw).ok()?;
            return parse_http_response(&raw);
        }

        None
    }

    fn http_post(path: &str, body: &str) -> Option<BridgeResponse> {
        let token = session_token();
        let req = format!(
            "POST {path} HTTP/1.1\r\n\
             Host: localhost\r\n\
             X-Nanny-Session-Token: {token}\r\n\
             Content-Type: application/json\r\n\
             Content-Length: {len}\r\n\
             Connection: close\r\n\
             \r\n\
             {body}",
            len = body.len()
        );

        #[cfg(unix)]
        if let Some(sock) = bridge_socket_path() {
            use std::io::{Read, Write};
            use std::os::unix::net::UnixStream;
            let mut stream = UnixStream::connect(&sock).ok()?;
            stream.write_all(req.as_bytes()).ok()?;
            let mut raw = String::new();
            stream.read_to_string(&mut raw).ok()?;
            return parse_http_response(&raw);
        }

        if let Some(port) = bridge_tcp_port() {
            use std::io::{Read, Write};
            use std::net::TcpStream;
            let mut stream = TcpStream::connect(("127.0.0.1", port)).ok()?;
            stream.write_all(req.as_bytes()).ok()?;
            let mut raw = String::new();
            stream.read_to_string(&mut raw).ok()?;
            return parse_http_response(&raw);
        }

        None
    }

    fn parse_http_response(raw: &str) -> Option<BridgeResponse> {
        let (headers, body) = raw.split_once("\r\n\r\n")?;
        let status_line = headers.lines().next()?;
        let status: u16 = status_line.split_whitespace().nth(1)?.parse().ok()?;
        Some(BridgeResponse { status, body: body.to_string() })
    }

    // ── Rule evaluation ───────────────────────────────────────────────────────

    /// Evaluate all locally-registered rules for `tool_name`.
    /// Returns the name of the first denying rule, or `None` if all allowed.
    ///
    /// Fetches `tool_call_counts` and `tool_call_history` from the bridge
    /// `/status` endpoint — `BridgeState` is the single source of truth.
    pub fn evaluate_local_rules(
        tool_name: &str,
        args: HashMap<String, String>,
    ) -> Option<&'static str> {
        let elapsed_ms = client_state().lock().unwrap().start.elapsed().as_millis() as u64;

        // Fetch tracked counts from the bridge (authoritative state).
        let (tool_call_counts, tool_call_history) = fetch_bridge_counts();

        let ctx = PolicyContext {
            requested_tool:    Some(tool_name.to_string()),
            tool_call_counts,
            tool_call_history,
            last_tool_args:    args,
            elapsed_ms,
            ..PolicyContext::default()
        };

        for rule in inventory::iter::<Rule> {
            if !(rule.func)(&ctx) {
                return Some(rule.name);
            }
        }
        None
    }

    /// Fetch tool_call_counts and tool_call_history from the bridge /status endpoint.
    /// Returns empty defaults if the bridge is unreachable or returns unexpected data.
    fn fetch_bridge_counts() -> (HashMap<String, u32>, Vec<String>) {
        let resp = match http_get("/status") {
            Some(r) if r.status == 200 => r,
            _ => return (HashMap::new(), Vec::new()),
        };
        let v: serde_json::Value = match serde_json::from_str(&resp.body) {
            Ok(v) => v,
            Err(_) => return (HashMap::new(), Vec::new()),
        };
        let counts = v.get("tool_call_counts")
            .and_then(|c| serde_json::from_value(c.clone()).ok())
            .unwrap_or_default();
        let history = v.get("tool_call_history")
            .and_then(|h| serde_json::from_value(h.clone()).ok())
            .unwrap_or_default();
        (counts, history)
    }

    // ── Tool call ─────────────────────────────────────────────────────────────

    /// What the bridge decided about a tool call.
    #[derive(Debug)]
    pub enum ToolVerdict {
        /// Allowed — run the original function body.
        Run,
        /// Denied or stopped — panic with this message.
        Stop(String),
    }

    /// POST /tool/call to the bridge.
    pub fn call_tool(tool_name: &str, cost: u64) -> ToolVerdict {
        let body = format!(r#"{{"tool":"{tool_name}","cost":{cost}}}"#);
        match http_post("/tool/call", &body) {
            Some(resp) if resp.status == 200 && resp.body.contains("\"allowed\"") => {
                ToolVerdict::Run
            }
            Some(resp) if resp.status == 200 => {
                let reason = extract_str(&resp.body, "rule_name")
                    .map(|r| format!("RuleDenied: {r}"))
                    .or_else(|| extract_str(&resp.body, "tool_name").map(|t| format!("ToolDenied: {t}")))
                    .or_else(|| extract_str(&resp.body, "reason"))
                    .unwrap_or_else(|| "ToolDenied".to_string());
                ToolVerdict::Stop(reason)
            }
            Some(resp) if resp.status == 410 => {
                let reason = extract_str(&resp.body, "reason")
                    .unwrap_or_else(|| "ExecutionStopped".to_string());
                ToolVerdict::Stop(reason)
            }
            _ => {
                // If the bridge is unreachable while we are in a governed run,
                // the enforcement guarantee is broken — fail closed rather than
                // silently allowing the tool call to proceed ungoverned.
                // If we are not in a governed run (passthrough mode, no env vars
                // set), is_active() returns false and we run normally.
                if is_active() {
                    ToolVerdict::Stop("BridgeUnavailable".to_string())
                } else {
                    ToolVerdict::Run
                }
            }
        }
    }

    /// POST /tool/call for a bridge-side tool (e.g. `http_get`).
    ///
    /// Unlike `call_tool` (which is for user-defined tools), this forwards
    /// `args_json` to the bridge and returns the tool's actual output content
    /// on success. The bridge executes the tool itself — the child process
    /// never runs any local logic for it.
    ///
    /// Returns `Err(reason)` if the call is denied, stopped, or the bridge
    /// is unavailable.
    pub fn call_bridge_tool(tool_name: &str, args_json: &str) -> Result<String, String> {
        let body = format!(r#"{{"tool":"{tool_name}","args":{args_json}}}"#);
        match http_post("/tool/call", &body) {
            Some(resp) if resp.status == 200 => {
                // Use serde_json so the result field is safely decoded even
                // when it contains HTML with escaped quotes or special chars.
                let v: serde_json::Value = serde_json::from_str(&resp.body)
                    .map_err(|e| e.to_string())?;
                if v["status"] == "allowed" {
                    Ok(v["result"].as_str().unwrap_or("").to_string())
                } else {
                    let reason = v["rule_name"].as_str()
                        .map(|r| format!("RuleDenied: {r}"))
                        .or_else(|| v["tool_name"].as_str().map(|t| format!("ToolDenied: {t}")))
                        .or_else(|| v["reason"].as_str().map(str::to_string))
                        .unwrap_or_else(|| "Denied".to_string());
                    Err(reason)
                }
            }
            Some(resp) if resp.status == 410 => {
                let reason = serde_json::from_str::<serde_json::Value>(&resp.body)
                    .ok()
                    .and_then(|v| v["reason"].as_str().map(str::to_string))
                    .unwrap_or_else(|| "ExecutionStopped".to_string());
                Err(reason)
            }
            Some(resp) if resp.status == 500 => {
                // Tool execution failed bridge-side (e.g. network error in http_get).
                let message = serde_json::from_str::<serde_json::Value>(&resp.body)
                    .ok()
                    .and_then(|v| v["message"].as_str().map(str::to_string))
                    .unwrap_or_else(|| "tool execution failed".to_string());
                Err(format!("ToolFailed: {message}"))
            }
            _ => Err("bridge unavailable".to_string()),
        }
    }

    /// POST /stop to the bridge — report a stop reason before calling exit(1).
    ///
    /// The bridge records this reason so the CLI emits it in `ExecutionStopped`
    /// instead of falling back to `ProcessCrashed`. Silently ignored if the
    /// bridge is unreachable.
    pub fn report_stop(reason: &str) {
        let body = serde_json::json!({"reason": reason}).to_string();
        let _ = http_post("/stop", &body);
    }

    // ── Agent enter / exit ────────────────────────────────────────────────────

    /// POST /agent/enter — switch to a named limits set.
    pub fn agent_enter(name: &str) {
        let body = serde_json::json!({"name": name}).to_string();
        if let Some(resp) = http_post("/agent/enter", &body) {
            if resp.status == 404 {
                panic!("nanny: agent limits set '{name}' not found in nanny.toml");
            }
        }
    }

    /// POST /agent/exit — revert to global limits.
    pub fn agent_exit() {
        http_post("/agent/exit", "{}");
    }

    // ── JSON helpers ──────────────────────────────────────────────────────────

    fn extract_str(json: &str, key: &str) -> Option<String> {
        let needle = format!("\"{key}\":");
        let after = json.find(&needle).map(|i| json[i + needle.len()..].trim_start())?;
        let inner = after.strip_prefix('"')?;
        let end = inner.find('"')?;
        Some(inner[..end].to_string())
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

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

    #[test]
    fn inactive_when_no_env_vars() {
        // SAFETY: std::env::remove_var is unsound when other threads are
        // reading the environment concurrently (deprecated in Rust 1.85).
        // This test runs in a single-threaded harness with no other env
        // readers, so the mutation is safe here. Do not copy this pattern
        // into multi-threaded production code.
        unsafe {
            std::env::remove_var("NANNY_BRIDGE_SOCKET");
            std::env::remove_var("NANNY_BRIDGE_PORT");
        }
        assert!(!is_active());
    }

    #[test]
    fn no_rules_registered_allows_all() {
        assert!(evaluate_local_rules("any_tool", ::std::collections::HashMap::new()).is_none());
    }
}
