@tool
extends Node
## HTTP client for OpenAI-compatible LLM APIs with SSE streaming.

const GolemSettings = preload("golem_settings.gd")

signal stream_chunk(text: String)
signal stream_done(full_text: String)
signal stream_error(message: String)
signal tool_response_received(data: Dictionary)

var _http: HTTPRequest
var _busy := false

# Streaming state
var _client: HTTPClient
var _streaming := false
var _stream_buffer := ""
var _full_response := ""
var _stream_headers: PackedStringArray
var _stream_body := ""
var _stream_path := ""
var _stream_state := 0  # 0=idle, 1=connecting, 2=requesting, 3=body


func _ready() -> void:
	_http = HTTPRequest.new()
	_http.name = "HTTP"
	_http.timeout = 120.0
	add_child(_http)
	set_process(false)


func _process(_delta: float) -> void:
	if _streaming:
		_poll_stream()


func is_busy() -> bool:
	return _busy


# ------------------------------------------------------------------
# Non-streaming completion (kept for compatibility)
# ------------------------------------------------------------------

func chat_completion(messages: Array) -> Dictionary:
	if _busy:
		return {"error": true, "message": "A request is already in progress"}

	_busy = true

	var api_key := GolemSettings.get_api_key()
	var base_url := GolemSettings.get_base_url()
	var model := GolemSettings.get_model()
	var temperature := GolemSettings.get_temperature()
	var provider := GolemSettings.get_provider()

	if base_url.is_empty():
		_busy = false
		return {"error": true, "message": "No API base URL configured"}

	var url := base_url.rstrip("/") + "/chat/completions"

	var body := {
		"model": model,
		"temperature": temperature,
		"messages": messages,
	}

	var headers: PackedStringArray = [
		"Content-Type: application/json",
	]

	if not api_key.is_empty():
		headers.append("Authorization: Bearer " + api_key)

	if provider == "openrouter":
		headers.append("HTTP-Referer: https://github.com/Haidy-ID/golem-mcp")
		headers.append("X-Title: Golem")

	var json_body := JSON.stringify(body)
	var err := _http.request(url, headers, HTTPClient.METHOD_POST, json_body)
	if err != OK:
		_busy = false
		return {"error": true, "message": "HTTP request failed (error %d)" % err}

	var result = await _http.request_completed
	_busy = false

	var result_code: int = result[0]
	var http_code: int = result[1]
	var response_body: PackedByteArray = result[3]

	if result_code != HTTPRequest.RESULT_SUCCESS:
		var msg := "Request failed"
		match result_code:
			HTTPRequest.RESULT_CANT_CONNECT:
				msg = "Cannot connect to %s" % base_url
			HTTPRequest.RESULT_CANT_RESOLVE:
				msg = "Cannot resolve host"
			HTTPRequest.RESULT_CONNECTION_ERROR:
				msg = "Connection error"
			HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR:
				msg = "TLS handshake error"
			HTTPRequest.RESULT_TIMEOUT:
				msg = "Request timed out (120s)"
		return {"error": true, "message": msg}

	var response_text := response_body.get_string_from_utf8()
	var json := JSON.new()
	if json.parse(response_text) != OK:
		return {"error": true, "message": "Invalid JSON response: " + response_text.left(200)}

	var data: Variant = json.data
	if not data is Dictionary:
		return {"error": true, "message": "Unexpected response format"}

	if http_code != 200:
		var error_msg := "API error %d" % http_code
		if data.has("error"):
			var err_data: Variant = data["error"]
			if err_data is Dictionary:
				error_msg = err_data.get("message", error_msg)
			elif err_data is String:
				error_msg = err_data
		return {"error": true, "message": error_msg}

	if data.has("choices") and data["choices"] is Array and data["choices"].size() > 0:
		var choice: Dictionary = data["choices"][0]
		var message: Variant = choice.get("message", {})
		if message is Dictionary:
			return {
				"error": false,
				"content": message.get("content", ""),
				"role": message.get("role", "assistant"),
				"usage": data.get("usage", {}),
			}

	return {"error": true, "message": "No choices in response"}


# ------------------------------------------------------------------
# Non-streaming completion with tool calling
# ------------------------------------------------------------------

func chat_completion_with_tools(messages: Array, tools: Array) -> Dictionary:
	## Send a chat completion request with tool definitions (non-streaming).
	## Returns the full response dict including potential tool_calls.
	if _busy:
		return {"error": true, "message": "A request is already in progress"}

	_busy = true

	var api_key := GolemSettings.get_api_key()
	var base_url := GolemSettings.get_base_url()
	var model := GolemSettings.get_model()
	var temperature := GolemSettings.get_temperature()
	var provider := GolemSettings.get_provider()

	if base_url.is_empty():
		_busy = false
		return {"error": true, "message": "No API base URL configured"}

	var url := base_url.rstrip("/") + "/chat/completions"

	var body: Dictionary = {
		"model": model,
		"temperature": temperature,
		"messages": messages,
		"tools": tools,
	}

	var headers: PackedStringArray = [
		"Content-Type: application/json",
	]

	if not api_key.is_empty():
		headers.append("Authorization: Bearer " + api_key)

	if provider == "openrouter":
		headers.append("HTTP-Referer: https://github.com/Haidy-ID/golem-mcp")
		headers.append("X-Title: Golem")

	var json_body := JSON.stringify(body)
	var err := _http.request(url, headers, HTTPClient.METHOD_POST, json_body)
	if err != OK:
		_busy = false
		return {"error": true, "message": "HTTP request failed (error %d)" % err}

	var result = await _http.request_completed
	_busy = false

	var result_code: int = result[0]
	var http_code: int = result[1]
	var response_body: PackedByteArray = result[3]

	if result_code != HTTPRequest.RESULT_SUCCESS:
		var msg := "Request failed"
		match result_code:
			HTTPRequest.RESULT_CANT_CONNECT:
				msg = "Cannot connect to %s" % base_url
			HTTPRequest.RESULT_CANT_RESOLVE:
				msg = "Cannot resolve host"
			HTTPRequest.RESULT_CONNECTION_ERROR:
				msg = "Connection error"
			HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR:
				msg = "TLS handshake error"
			HTTPRequest.RESULT_TIMEOUT:
				msg = "Request timed out (120s)"
		return {"error": true, "message": msg}

	var response_text := response_body.get_string_from_utf8()
	var json := JSON.new()
	if json.parse(response_text) != OK:
		return {"error": true, "message": "Invalid JSON response: " + response_text.left(200)}

	var data: Variant = json.data
	if not data is Dictionary:
		return {"error": true, "message": "Unexpected response format"}

	if http_code != 200:
		var error_msg := "API error %d" % http_code
		if data.has("error"):
			var err_data: Variant = data["error"]
			if err_data is Dictionary:
				error_msg = err_data.get("message", error_msg)
			elif err_data is String:
				error_msg = err_data
		return {"error": true, "message": error_msg}

	if data.has("choices") and data["choices"] is Array and data["choices"].size() > 0:
		var choice: Dictionary = data["choices"][0]
		var message: Variant = choice.get("message", {})
		if message is Dictionary:
			var resp: Dictionary = {
				"error": false,
				"content": message.get("content", ""),
				"role": message.get("role", "assistant"),
				"usage": data.get("usage", {}),
				"finish_reason": choice.get("finish_reason", ""),
			}
			if message.has("tool_calls") and message["tool_calls"] is Array:
				resp["tool_calls"] = message["tool_calls"]
			return resp

	return {"error": true, "message": "No choices in response"}


# ------------------------------------------------------------------
# Streaming completion (SSE via HTTPClient)
# ------------------------------------------------------------------

func start_stream(messages: Array) -> void:
	if _busy:
		stream_error.emit("A request is already in progress")
		return

	_busy = true
	_full_response = ""
	_stream_buffer = ""

	var api_key := GolemSettings.get_api_key()
	var base_url := GolemSettings.get_base_url()
	var model := GolemSettings.get_model()
	var temperature := GolemSettings.get_temperature()
	var provider := GolemSettings.get_provider()

	if base_url.is_empty():
		_busy = false
		stream_error.emit("No API base URL configured")
		return

	var url_parts: Dictionary = _parse_url(base_url.rstrip("/") + "/chat/completions")
	_stream_path = url_parts.path

	var body := {
		"model": model,
		"temperature": temperature,
		"messages": messages,
		"stream": true,
	}

	_stream_headers = PackedStringArray([
		"Content-Type: application/json",
		"Accept: text/event-stream",
	])

	if not api_key.is_empty():
		_stream_headers.append("Authorization: Bearer " + api_key)

	if provider == "openrouter":
		_stream_headers.append("HTTP-Referer: https://github.com/Haidy-ID/golem-mcp")
		_stream_headers.append("X-Title: Golem")

	_stream_body = JSON.stringify(body)

	_client = HTTPClient.new()
	var tls: TLSOptions = TLSOptions.client() if url_parts.is_https else null
	var connect_err: int = _client.connect_to_host(url_parts.host, url_parts.port, tls)
	if connect_err != OK:
		_busy = false
		_client = null
		stream_error.emit("Cannot connect to %s" % base_url)
		return

	_stream_state = 1
	_streaming = true
	set_process(true)


func stop_stream() -> void:
	if _streaming:
		_streaming = false
		_stream_state = 0
		_busy = false
		if _client:
			_client.close()
			_client = null
		set_process(false)


func _poll_stream() -> void:
	if not _client:
		_stop_stream_error("Connection lost")
		return

	_client.poll()
	var status: int = _client.get_status()

	match _stream_state:
		1:  # Connecting
			if status == HTTPClient.STATUS_CONNECTED:
				var req_err: int = _client.request_raw(
					HTTPClient.METHOD_POST,
					_stream_path,
					_stream_headers,
					_stream_body.to_utf8_buffer()
				)
				if req_err != OK:
					_stop_stream_error("Request send failed")
					return
				_stream_state = 2
			elif status == HTTPClient.STATUS_CANT_CONNECT or status == HTTPClient.STATUS_CONNECTION_ERROR:
				_stop_stream_error("Connection failed")
			elif status == HTTPClient.STATUS_CANT_RESOLVE:
				_stop_stream_error("Cannot resolve host")

		2:  # Waiting for response headers
			if _client.has_response():
				var response_code: int = _client.get_response_code()
				if response_code != 200:
					_stop_stream_error("API error %d" % response_code)
					return
				_stream_state = 3

		3:  # Reading streaming body
			if status == HTTPClient.STATUS_BODY:
				var chunk := _client.read_response_body_chunk()
				if chunk.size() > 0:
					_process_sse_data(chunk.get_string_from_utf8())
			elif status != HTTPClient.STATUS_REQUESTING and status != HTTPClient.STATUS_CONNECTED:
				_finish_stream()


func _process_sse_data(raw: String) -> void:
	_stream_buffer += raw

	while true:
		var newline_pos: int = _stream_buffer.find("\n")
		if newline_pos == -1:
			break

		var line: String = _stream_buffer.substr(0, newline_pos).strip_edges()
		_stream_buffer = _stream_buffer.substr(newline_pos + 1)

		if line.is_empty():
			continue

		if not line.begins_with("data: "):
			continue

		var data_str: String = line.substr(6)

		if data_str == "[DONE]":
			_finish_stream()
			return

		var json := JSON.new()
		if json.parse(data_str) != OK:
			continue

		var parsed: Variant = json.data
		if not parsed is Dictionary:
			continue

		var choices: Variant = parsed.get("choices", [])
		if not choices is Array or choices.size() == 0:
			continue

		var first_choice: Variant = choices[0]
		if not first_choice is Dictionary:
			continue

		var delta: Variant = first_choice.get("delta", {})
		if delta is Dictionary and delta.has("content"):
			var content: String = str(delta["content"])
			if not content.is_empty():
				_full_response += content
				stream_chunk.emit(content)


func _finish_stream() -> void:
	_streaming = false
	_stream_state = 0
	_busy = false
	var response := _full_response
	if _client:
		_client.close()
		_client = null
	set_process(false)
	stream_done.emit(response)


func _stop_stream_error(msg: String) -> void:
	_streaming = false
	_stream_state = 0
	_busy = false
	if _client:
		_client.close()
		_client = null
	set_process(false)
	stream_error.emit(msg)


static func _parse_url(url: String) -> Dictionary:
	var is_https := url.begins_with("https://")
	var stripped := url.replace("https://", "").replace("http://", "")
	var slash_pos: int = stripped.find("/")
	var host_port: String
	var path: String
	if slash_pos == -1:
		host_port = stripped
		path = "/"
	else:
		host_port = stripped.substr(0, slash_pos)
		path = stripped.substr(slash_pos)

	var port: int = 443 if is_https else 80
	var colon_pos: int = host_port.find(":")
	var host: String
	if colon_pos != -1:
		host = host_port.substr(0, colon_pos)
		port = host_port.substr(colon_pos + 1).to_int()
	else:
		host = host_port

	return {"host": host, "port": port, "path": path, "is_https": is_https}
