@tool
class_name APIClient
extends Node


signal response_received(response_body)
signal request_failed(error)

const RESULT_CANT_CONNECT = "Failed to connect"
const RESULT_CANT_RESOLVE = "Failed to resolve"
const RESULT_CONNECTION_ERROR = "Connection error"
const RESULT_TIMEOUT = "Connection timeout"
const RESULT_SERVER_ERROR = "Server error"
const REQUEST_FAILED = "Error in the request"
const REQUEST_TIMEOUT = "Request timed out on the client side"
const URL_NOT_SET = "URL parameter is not set"
const PARSE_FAILED = "Parsing failed"

## Location of the API endpoint.
@export var api_url : String
## HTTP request method to use. Typically GET or POST.
@export var request_method : HTTPClient.Method = HTTPClient.METHOD_POST
@export_group("Advanced")
## Location of an API key file, if authorization is required by the endpoint.
@export_file("*.txt") var api_key_file : String
## Time in seconds before the request fails due to timeout.
@export var request_timeout : float = 0.0
@export var _send_request_action : bool = false :
	set(value):
		if value and Engine.is_editor_hint():
			request()
# For Godot 4.4
# @export_tool_button("Send Request") var _send_request_action = request


@onready var _http_request : HTTPRequest = $HTTPRequest
@onready var _timeout_timer : Timer= $TimeoutTimer

## State flag for whether the connection has timed out on the client-side.
var timed_out : bool = false

func get_http_request() -> HTTPRequest:
	return _http_request

func get_api_key() -> String:
	if api_key_file.is_empty(): 
		return ""
	var file := FileAccess.open(api_key_file, FileAccess.READ)
	var error := FileAccess.get_open_error()
	if error != OK:
		push_error("API Key reading error: %d" % error)
		return ""
	var content = file.get_as_text()
	file.close()
	return content

func get_api_url() -> String:
	return api_url

func get_api_method() -> int:
	return request_method

func mock_empty_body() -> String:
	var form : Dictionary = {}
	return JSON.stringify(form)

func mock_request(body : String):
	await(get_tree().create_timer(10.0).timeout)
	_on_request_completed(HTTPRequest.RESULT_SUCCESS, "200", [], body)

func request(body : String = "", request_headers : Array = []) -> void:
	var local_http_request : HTTPRequest = get_http_request()
	var key : String = get_api_key()
	var url : String = get_api_url()
	var method : int = get_api_method()
	if url.is_empty():
		request_failed.emit(URL_NOT_SET)
		push_error(URL_NOT_SET)
		return
	request_headers.append("Content-Type: application/json")
	if key:
		request_headers.append("x-api-key: %s" % key)
	if request_timeout > 0.0:
		local_http_request.timeout = request_timeout
	var error = local_http_request.request(url, request_headers, method, body)
	if error != OK:
		request_failed.emit(REQUEST_FAILED)
		push_error("HTTP Request error: %d" % error)
		return
	if request_timeout > 0.0:
		_timeout_timer.start(request_timeout + 1.0)

func request_raw(data : PackedByteArray = [], request_headers : Array = []) -> void:
	var local_http_request : HTTPRequest = get_http_request()
	var key : String = get_api_key()
	var url : String = get_api_url()
	var method : int = get_api_method()
	if url.is_empty():
		request_failed.emit(URL_NOT_SET)
		push_error(URL_NOT_SET)
		return
	request_headers.append("Content-Type: application/json")
	if key:
		request_headers.append("x-api-key: %s" % key)
	if request_timeout > 0.0:
		local_http_request.timeout = request_timeout
	var error = local_http_request.request_raw(url, request_headers, method, data)
	if error != OK:
		request_failed.emit(REQUEST_FAILED)
		push_error("HTTP Request error: %d" % error)
		return
	if request_timeout > 0.0:
		_timeout_timer.start(request_timeout + 1.0)

func _on_request_completed(result, response_code, headers, body) -> void:
	# If already timed out on client-side, then return.
	if timed_out: return
	_timeout_timer.stop()
	if result == HTTPRequest.RESULT_SUCCESS:
		var body_string : String
		if body is PackedByteArray:
			body_string = body.get_string_from_utf8()
		elif body is String:
			body_string = body
		var json := JSON.new()
		var error = json.parse(body_string)
		if error != OK:
			request_failed.emit(PARSE_FAILED)
			push_error("Parse error: %d" % error)
			return
		var parsed_data = json.data
		response_received.emit(json.data)
	else:
		var error_message : String
		match(result):
			HTTPRequest.RESULT_CANT_CONNECT:
				error_message = RESULT_CANT_CONNECT
			HTTPRequest.RESULT_CANT_RESOLVE:
				error_message = RESULT_CANT_RESOLVE
			HTTPRequest.RESULT_CONNECTION_ERROR:
				error_message = RESULT_CONNECTION_ERROR
			HTTPRequest.RESULT_TIMEOUT:
				error_message = RESULT_TIMEOUT
			_:
				error_message = RESULT_SERVER_ERROR
		request_failed.emit(error_message)
		push_error("HTTP Result error: %d" % result)

func _on_http_request_request_completed(result, response_code, headers, body) -> void:
	_on_request_completed(result, response_code, headers, body)

func _on_timeout_timer_timeout() -> void:
	timed_out = true
	request_failed.emit(REQUEST_TIMEOUT)
	push_warning(REQUEST_TIMEOUT)
