// r8r: rust port of n8n.io
// Initial author: Michael Cochran <mcochran@cochranblock.org>
// Built for: Tanner Woodrum, intended cofounder
// License: Unlicense (public domain dedication, see UNLICENSE)
//
// `lm_studio` — chat against a local LM Studio server (OpenAI-compatible
// /v1/chat/completions). Default base_url = http://localhost:1234.
// No API key needed.

use async_trait::async_trait;
use crate::core::error::NodeError;
use crate::core::types::*;
use crate::engine::{ExecuteCtx, Node, NodeFactory};
use serde_json::{json, Value};
use std::sync::Arc;

pub struct LmStudioNode;

static META: NodeMetadata = NodeMetadata {
    name: "lm_studio",
    display_name: "LM Studio",
    version: &[1],
    group: &["ai"],
    description: "Chat against a local LM Studio server (OpenAI-compatible) — no API key",
    inputs: 1,
    outputs: 1,
    properties: &[],
    credentials: &[],
};

#[async_trait]
impl Node for LmStudioNode {
    fn metadata(&self) -> &'static NodeMetadata { &META }

    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        let base_url = ctx.param_opt_str("base_url")
            .unwrap_or_else(|| "http://localhost:1234".into());
        let base_url = base_url.trim_end_matches('/').to_string();
        let model = ctx.param_opt_str("model").unwrap_or_else(|| "local-model".into());
        let prompt = ctx.param_str("prompt")?;
        let system = ctx.param_opt_str("system");
        let temperature = ctx.node_def.parameters.get("temperature")
            .and_then(|v| v.as_f64()).unwrap_or(0.7);

        let mut messages = Vec::new();
        if let Some(sys) = system {
            messages.push(json!({ "role": "system", "content": sys }));
        }
        messages.push(json!({ "role": "user", "content": prompt }));

        let resp = reqwest::Client::new()
            .post(format!("{}/v1/chat/completions", base_url))
            .json(&json!({ "model": model, "messages": messages, "temperature": temperature }))
            .send().await.map_err(|e| NodeError::Http(format!("lm_studio: {}", e)))?;
        let status = resp.status();
        let parsed: Value = resp.json().await.unwrap_or(Value::Null);
        if !status.is_success() {
            return Err(NodeError::Http(format!("lm_studio {}: {}", status, parsed)));
        }
        let content = parsed.get("choices").and_then(|c| c.get(0))
            .and_then(|c| c.get("message")).and_then(|m| m.get("content"))
            .and_then(|v| v.as_str()).unwrap_or("").to_string();
        Ok(vec![vec![NodeItem {
            json: json!({ "content": content, "raw": parsed }),
            ..Default::default()
        }]])
    }
}

inventory::submit! { NodeFactory::new("lm_studio", || Arc::new(LmStudioNode)) }
