@icon("./security-icon.svg")
@tool
extends Resource

## Used to store and load token's and to exchange them through the code.
## Try to avoid debugging this object cause it leaks your access and refresh tokens
## Hint never store the token value as string in your code to reduce the chance
## to leak the tokens always use the getter.

class_name OAuthToken

static var CRYPTO: Crypto = Crypto.new()

const APP_ACCESS_TOKEN: StringName = &"App Access Token"
const USER_ACCESS_TOKEN: StringName = &"User Access Token"

## Key for encryption purpose to save the tokens
@export var _crypto_key_provider: CryptoKeyProvider = preload("res://addons/twitcher/lib/oOuch/default_key_provider.tres")
## Unique identifier to store multiple tokens within one config file
@export var _identifier: String = "Auth-%s" % randi_range(0, 10000)
## Storage where the tokens should be saved encrypted (multiple secrets can be put in the same file see _identifier)
@export var _cache_path: String = SystemManager.get_program_path().path_join("auth_token.cache"):
	set(val):
		_cache_path = val
		_load_tokens()

## Returns if its an user access token or app accesstoken
var type: StringName
var _scopes: PackedStringArray = []
var _expire_date: int
var _config_file: ConfigFile = ConfigFile.new()

var _access_token: String = "":
	set(val):
		_access_token = val
		if val != "": authorized.emit()
var _refresh_token: String = ""


## Called when the token was resolved / accesstoken got refreshed
signal authorized

## Update the visual representation of the scopes (don't use it for actually changing scopes it wont work!)
# Client credential flow doesn't return used scopes and I wanted to give the use feedback over the scopes
func _update_scopes(scopes: Array[StringName]) -> void:
	for scope in scopes: _scopes.append(scope)
	_persist_tokens()
	emit_changed()


## Updates the data in the scopes and persist it
func update_values(access_token: String, refresh_token: String, expire_in: int, scopes: Array[String], token_type: StringName) -> void:
	_expire_date = roundi(Time.get_unix_time_from_system() + expire_in)
	_access_token = access_token
	_refresh_token = refresh_token
	_scopes = scopes
	type = token_type
	_persist_tokens()
	emit_changed()


## Persists the tokesn with the expire date
func _persist_tokens():
	var encrypted_access_token: PackedByteArray  = _crypto_key_provider.encrypt(_access_token.to_utf8_buffer())
	var encrypted_refresh_token: PackedByteArray = _crypto_key_provider.encrypt(_refresh_token.to_utf8_buffer())
	_config_file.load(_cache_path)
	_config_file.set_value(_identifier, "expire_date", _expire_date)
	_config_file.set_value(_identifier, "type", type)
	_config_file.set_value(_identifier, "access_token", Marshalls.raw_to_base64(encrypted_access_token))
	_config_file.set_value(_identifier, "refresh_token", Marshalls.raw_to_base64(encrypted_refresh_token))
	_config_file.set_value(_identifier, "scopes", ",".join(_scopes))
	var err: Error = _config_file.save(_cache_path)
	if err != OK: push_error("Couldn't save tokens cause of ", error_string(err))


## Loads the tokens and returns the information if the file got created
func _load_tokens() -> bool:
	var status: Error = _config_file.load(_cache_path)
	if status == OK && _config_file.has_section(_identifier):
		_expire_date = _config_file.get_value(_identifier, "expire_date", 0)
		var encrypted_access_token: PackedByteArray = Marshalls.base64_to_raw(_config_file.get_value(_identifier, "access_token"))
		var encrypted_refresh_token: PackedByteArray = Marshalls.base64_to_raw(_config_file.get_value(_identifier, "refresh_token"))
		_access_token = _crypto_key_provider.decrypt(encrypted_access_token).get_string_from_utf8()
		_refresh_token = _crypto_key_provider.decrypt(encrypted_refresh_token).get_string_from_utf8()
		type = _config_file.get_value(_identifier, "type", &"")
		_scopes = _config_file.get_value(_identifier, "scopes", "").split(",", false)
		emit_changed()
		return true
	return false


func remove_tokens() -> void:
	var status: Error = _config_file.load(_cache_path)
	if status == OK && _config_file.has_section(_identifier):
		_access_token = ""
		_refresh_token = ""
		type = &""
		_expire_date = 0
		_scopes.clear()

		_config_file.erase_section(_identifier)
		var err: Error = _config_file.save(_cache_path)
		if err != OK: push_error("Couldn't save tokens cause of ", error_string(err))
		emit_changed()
		print("%s got revoked" % _identifier)
	else:
		print("%s not found" % _identifier)


func get_refresh_token() -> String:
	return _refresh_token


func get_access_token() -> String:
	if not is_token_valid(): await authorized
	return _access_token


func get_scopes() -> PackedStringArray:
	return _scopes


## The unix timestamp when the token is expiring
func get_expiration() -> int:
	return _expire_date


func get_expiration_readable() -> String:
	if _expire_date == 0:
		return "Not available"
	return Time.get_datetime_string_from_unix_time(_expire_date, true)


func invalidate() -> void:
	_expire_date = 0
	_refresh_token = ""
	_access_token = ""
	_scopes = []
	emit_changed()


## Does this accesstoken has a refresh token
func has_refresh_token() -> bool:
	return _refresh_token != "" && _refresh_token != null


## Checks if the access token is still valid
func is_token_valid() -> bool:
	var current_time: float = Time.get_unix_time_from_system()
	return current_time < _expire_date


func _to_string() -> String:
	return "<%s#%s>" % [_identifier, get_instance_id()]


## Get all token names within a config file
static func get_identifiers(cache_file: String) -> PackedStringArray:
	var _config_file: ConfigFile = ConfigFile.new()
	var status: Error = _config_file.load(cache_file)
	if status != OK: return []
	return _config_file.get_sections()
