use std::env;
use std::sync::LazyLock;
use futures_util::StreamExt;
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast::Sender;
use uuid::Uuid;
use crate::model::event::{ChatEvent, ChatMsg};
use crate::error::{ApiResult, AppError};


#[derive(Clone)]
pub struct LlmService;


static LMSTUDIO_URL: LazyLock<Option<String>> = LazyLock::new(|| env::var("LMSTUDIO_URL").ok());
static LMSTUDIO_MODEL: LazyLock<Option<String>> = LazyLock::new(|| env::var("LMSTUDIO_MODEL").ok());

impl LlmService {

	pub fn new() -> Self {
		Self {}
	}

	pub fn enabled(&self) -> bool {
		LMSTUDIO_URL.is_some()
	}

	pub async fn query(&self, message: &ChatMsg, context: &[ChatMsg], sender: Sender<ChatEvent>) -> ApiResult<ChatMsg> {
		let Some(url) = LMSTUDIO_URL.clone() else {
			return Err(AppError::internal("AI not enabled!"))
		};

		let reply_id = Uuid::new_v4();
		let timestamp = chrono::Utc::now();
		let mut reply = ChatMsg {
			id: reply_id,
			display_name: Some("llm".into()),
			user_id: 0,
			text: String::new(),
			timestamp,
		};
		let _ = sender.send(ChatEvent::SendMessage(reply.clone()));

		let mut messages: Vec<Message> = Vec::new();
		let system_prompt = format!(
			"You are a helpful assistant in a group chat. \
			 You are talking to '{}'. \
			 Keep responses concise and conversational.",
			message.display_name.as_deref().unwrap_or("unknown"),
		);
		messages.push(Message { role: "system".into(), content: system_prompt });

		for msg in context {
			let role = if msg.user_id == 0 {
				"assistant"  // your LLM user_id convention
			} else {
				"user"
			};
			messages.push(Message {
				role: role.into(),
				content: format!(
					"{}: {}",
					msg.display_name.as_deref().unwrap_or("unknown"),
					msg.text
				),
			});
		}

		messages.push(Message {
			role: "user".into(),
			content: format!(
				"{}: {}",
				message.display_name.as_deref().unwrap_or("unknown"),
				message.text.trim_start_matches("/ask ")  // strip the command prefix
			),
		});

		let Ok(resp) = reqwest::Client::new()
			.post(url)
			.json(&LlmRequest {
				think: false,
				model: LMSTUDIO_MODEL
					.clone()
					.unwrap_or_else(|| "gpt-oss-20b".into()),
				messages,
				stream: true,
			})
			.send()
			.await
		else {
			tracing::warn!("Failed to reach LLM");
			let _ = sender.send(ChatEvent::MessageAppendContent {
				id: reply_id,
				content: String::from("I'm not available right now. Please try again later.")
			});
			return Err(AppError::internal("Failed to reach LLM"));
		};


		let mut full_text = String::new();
		let mut buffer = String::new();
		let mut stream = resp.bytes_stream();

		while let Some(chunk) = stream.next().await {
			let chunk = chunk.map_err(|_| AppError::internal("Stream error"))?;
			buffer.push_str(&String::from_utf8_lossy(&chunk));

			while let Some(pos) = buffer.find('\n') {
				let line = buffer[..pos].trim().to_string();
				buffer = buffer[pos + 1..].to_string();

				if line == "data: [DONE]" {
					break;
				}

				if let Some(json) = line.strip_prefix("data: ")
					&& let Ok(parsed) = serde_json::from_str::<StreamingResponse>(json)
					&& let Some(content) = parsed.choices[0].delta.content.as_ref() {
						full_text.push_str(content);
						let _ = sender.send(ChatEvent::MessageAppendContent {
							id: reply_id,
							content: content.clone(),
						});
					}
			}
		}

		reply.text = full_text;

		Ok(reply)
	}
}



#[derive(Serialize)]
struct LlmRequest {
	model: String,
	messages: Vec<Message>,
	stream: bool,
	think: bool,
}

#[derive(Deserialize)]
struct StreamingResponse {
	choices: Vec<StreamingChoice>,
}

#[derive(Deserialize)]
struct StreamingChoice {
	delta: Delta,
	#[serde(default)]
	finish_reason: Option<String>,
}

#[derive(Deserialize)]
struct Delta {
	#[serde(default)]
	content: Option<String>,
}
#[derive(Serialize, Deserialize)]
struct Message {
	role: String, // "user" or "assistant"
	content: String,
}


