extends Node
class_name No_Twitch

## Handler for a Connection to a Single Twitch Account
##
## Used for getting authentication and handling websocket connections.

signal token_received(token : String)

signal twitch_error(error : String)

signal invalid_auth()

signal chat_received(chat_dict)
signal chat_received_rich(chat_dict)

@export var client_id := "qyjg1mtby1ycs5scm1pvctos7yvyc1"
@export var redirect_uri := "http://localhost"
## Port that the redirect_uri will head to on your local system.  Defaults to 80 for most cases.
## Linux tends to prefer 8000 or possibly 1338
@export var redirect_port := "8000"

var auth_url := "https://id.twitch.tv/oauth2/authorize?response_type=token&"
var auth_scopes : Array[String] = ["chat:read", "chat:edit", "channel:manage:redemptions", "bits:read", "channel:read:goals", "channel:manage:polls", "channel:read:hype_train", "channel:manage:predictions", "moderator:read:followers", "user:read:chat", "channel:read:subscriptions", "channel:moderate", "channel:read:charity", "moderator:read:shield_mode", "moderator:manage:shoutouts"]

var auth_server : TCPServer

const chat_socket_class = preload("res://addons/no_twitch/chat_socket.gd")
const eventsub_socket_class = preload("res://addons/no_twitch/eventsub_socket.gd")
## [Websocket_Client] used for receiving all data from Twitch pertaining to chat (Ex. IRC Chat Messages)
var chat_socket : Websocket_Client
## [Websocket_Client] handles all the data from Twitch pertaining to EventSub (Ex. General Alerts)
var eventsub_socket : Websocket_Client

var state := make_state()
var token : String

## The User data of the account that the authorized [member token] is connected to.
var user_info : Dictionary

var responses : Array

func _ready():

	token_received.connect(cache_user_data.unbind(1))


## Function that handles Caching the data of the Account tied to the Connections Token
func cache_user_data():
	
	if token.is_empty():
		
		return
		
	
	var resp = request_user_info()
	resp.response_received.connect(func(data):
		
		# Error handling here.
		if data.has("error") and not data.error.is_empty(): # If we have an error.
			
			invalid_auth.emit()
			
			# Return out.
			return
			
		
		user_info = data.data
		print("User Info Cached")
		
		)



## Handles the basic Twitch Authentication process to request and then later receive a Token
## (using [method check_auth_peer]). Returns the authentication URL.
func authenticate_with_twitch(client_id = client_id, scopes : Array[String] = auth_scopes) -> String:
	auth_server = TCPServer.new()
	auth_scopes = scopes
	var url := create_auth_url()
	auth_server.listen(int(redirect_port))
	return url


## Sets up the chat connection.  Joining 1 room with [param token] as it's "PASS" specifying what
## account it's on.  And the optional [param default_chat] specifying the default room to join.
## While [param nick] specifies the "nickname" (Not the username on Twitch)
func setup_chat_connection(default_chat : String = "", token : String = token, request_twitch_info = true, nick = "terribletwitch"):

	chat_socket = chat_socket_class.new(self)

	# Connects to the Twitch IRC server.
	chat_socket.connect_to_chat(token, request_twitch_info)
	await chat_socket.socket_open

	if !default_chat.is_empty():

		chat_socket.join_chat(default_chat)




## Joins the given [param channel] over IRC.  Essentially just sending JOIN #[param channel]
func join_channel(channel : String):

	chat_socket.join_chat(channel)


## Leaves or "PART"s the given [param channel] over IRC. (Sends PART #[param channel])
func leave_channel(channel : String):

	chat_socket.leave_chat(channel)


func send_chat(msg : String, channel : String = ""):

	chat_socket.send_chat(msg, channel)


## Sets up an EventSub connection to allow subscribing to EventSub events.  Ex. Alerts, Channel Point
## Redemptions etc.
func setup_eventsub_connection(events : Array[EventSub_Subscription] = [], timeout_duration : int = 20):

	eventsub_socket = eventsub_socket_class.new(self, timeout_duration)
	var ret = await eventsub_socket.connect_to_eventsub(events)
	return ret


## Adds a new subscription to a given EventSub Event using a [No_Twitch.EventSub_Subscription]
## to handle the data.
func add_eventsub_subscription(sub_type : EventSub_Subscription):

	if eventsub_socket == null:

		return await setup_eventsub_connection([sub_type])


	if sub_type.session_id == "":

		sub_type.session_id = eventsub_socket.session_id


	return twitch_request("https://api.twitch.tv/helix/eventsub/subscriptions", HTTPClient.METHOD_POST, str(sub_type.return_request_dictionary()))


func subscribe_to_channel_points(user_id : String):

	if !eventsub_socket.socket_open:

		push_error("No_Twitch Error: No EventSub Connection, please use setup_eventsub_connection first")
		return


	return await add_eventsub_subscription(EventSub_Subscription.new("channel.channel_points_custom_reward_redemption.add", {"broadcaster_user_id" : user_id}, eventsub_socket.session_id))


func _process(delta):

	if auth_server and auth_server.is_listening() and auth_server.is_connection_available():

		check_auth_peer(auth_server.take_connection())


	if chat_socket:

		chat_socket.poll_socket()


	if eventsub_socket:

		eventsub_socket.poll_socket()



## Utility function for creating a Twitch Authentication URL with the given
## [param scopes], Twitch Client ID ([param id]) and [param redirect_uri].
## [param id] defaults to [member client_id] and both [param redirect] and
## [param redirect_port]
func create_auth_url(port := redirect_port, id : String = client_id, redirect : String = redirect_uri) -> String:

	var str_scopes : String

	for all in auth_scopes:

		str_scopes += " " + all
	str_scopes = str_scopes.strip_edges()

	var full_redirect_uri := redirect
	if !port.is_empty():
		full_redirect_uri += ":" + port

	var url := auth_url + "client_id=" + id + "&redirect_uri=" + full_redirect_uri + "&scope=" + str_scopes + "&state=" + str(state)

	return url

## Utility Function for making a generic HTTP Request to Twitch
func twitch_request(url : String, method : HTTPClient.Method = HTTPClient.METHOD_GET, body := "") -> HTTPResponse:

	var headers : Array = ["Authorization: Bearer " + token, "Client-Id: " + client_id]

#	Adds the Content type to the headers if we're actually sending some data along
	if method != HTTPClient.METHOD_GET:

		headers.append("Content-Type: application/json")


	var http := HTTPRequest.new()

	var resp = HTTPResponse.new(http, responses)
	add_child(http)
	http.request(url, headers, method, body)
	
	# Generic error signal handling to ensure the signal is emitted.
	resp.response_received.connect(func(data):
		# If there's no error return.
		if !data.has("error") or data.error.is_empty():
			
			return
		
		twitch_error.emit(data.error, data.message)
		
		)
	
	return resp


## Wrapper function around [method twitch_request] to handle doing a Twitch "Get Users" request.
func request_user_info(users : Array[String] = []):

	var req_url := "https://api.twitch.tv/helix/users"

	if users.is_empty():

		return twitch_request(req_url)


	var user_list : String = "?login=" + users[0]
	for all in users:

		user_list += "&login=" + all


	return twitch_request(req_url + user_list)


## Wrapper function around [method twitch_request] to grab information about a set of channels based
## off an Array of Channel IDs Ex. stream title, current category, current tags, etc.
func request_channel_info(channels : Array[String] = []):

	var req_url := "https://api.twitch.tv/helix/channels?broadcaster_id="

	if channels.is_empty():

		if user_info.is_empty():

			return


		channels.append(user_info.id)


	var id_string := channels[0]
	for all in channels:

		id_string += "&broadcaster_id=" + all


	return twitch_request(req_url + id_string)


## Utility function for creating a "state" used for different requests for some extra security as a
## semi password.
func make_state(len : int = 16) -> String:

	var crypto = Crypto.new()
	var state = crypto.generate_random_bytes(len).hex_encode()

	return state


func check_auth_peer(peer : StreamPeerTCP):

	var info = peer.get_utf8_string(peer.get_available_bytes())
	printraw(info)

	var root := redirect_uri
	if !redirect_port.is_empty():
		root += ":" + redirect_port
	root += "/"

	var script = "<script>fetch('" + root + "' + window.location.hash.substr(1))</script>"

	peer.put_data(str("HTTP/1.1 200\n\n" + script).to_utf8_buffer())


	var resp_state = info.split("&state=")

#	Ensures that the received state is correct.
	if !resp_state.size() > 1 or resp_state[0] == state:

		return


	var token = info.split("access_token=")[1].split("&scope=")[0].strip_edges()
	printraw("Token: ", token, "\n")
	self.token = token
	token_received.emit(token)


func check_chat_socket(dict, rich = false):

	if rich:

		chat_received_rich.emit(dict)
		return

	chat_received.emit(dict)


## "Promise" Class used for relaying the info received from the supplied [HTTPRequest] node.
class HTTPResponse:

	signal response_received

	## The infuriatingly needed data variable.  Because you can't await AND get signal data.
	var inf_data

	func _init(http : HTTPRequest, storage : Array):

		storage.append(self)
		http.request_completed.connect(request_complete.bind(http, storage))


	func request_complete(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray, http, storage):

		var info : Dictionary
		var str = body.get_string_from_utf8()
		if !body.is_empty() and !JSON.parse_string(str) == null:
			
			info = JSON.parse_string(body.get_string_from_utf8())
			

		info["result"] = result
		info["code"] = response_code
		info["headers"] = headers

		if info.has("error"):
			
			emit_response(info, http, storage)
			return
			
		
		if info.data is Array and info.data.size() == 1:
			info.data = info.data[0]
		# Revisit later cause Twitch weirdness
		#info["data"] = info.get("data", [[]])[0]

		print("Response Received")
		emit_response(info, http, storage)
	
	func emit_response(info, http, storage):
		
		inf_data = info
		response_received.emit(info)
		
		http.queue_free()
		storage.erase(self)
		



## Data handler class for making it easier to send the data used for EventSub Subscriptions over
## [member eventsub_socket]
class EventSub_Subscription:

	## Specifies the type of subscription this is representing (Ex. "Channel Update" is channel.update)
	var subscription_type : String
	## Used for setting the "version" the subscription that we're currently using.
	var version : String
	## Stores the "condition", AKA the condition for being able to subscribe to this Event.
	## (Ex. The User ID of the channel you're listening to)
	var condition : Dictionary
	## Holds the "session" ID for this EventSub Subscription.  Which is held in
	## [member eventsub_socket.session_id]
	var session_id : String
	## Stores the subscription ID returned from the EventSub Subscription API Request
	var sub_id

	var method = "websocket"

	func _init(sub_type : String, cond : Dictionary, sess_id : String = "", vers : String = "1"):

		subscription_type = sub_type
		version = vers
		condition = cond
		session_id = sess_id


	## Returns back the information pertaining to the Subscription in the format needed for twitch.
	func return_request_dictionary():

		var dict = {"type" : subscription_type, "version" : version, "condition" : condition, "transport" : {"method" : method, "session_id" : session_id}}

		return dict