' Embodies the Authorization Device operations
sub AuthorizeDeviceTask() as object
    self = CreateObject ("roAssociativeArray")

    ' Public methods

    self.Init = sub () as object
    end sub

    ' Checks if params are allright and start get authorization
    ' @param {string} clientId - ID provided by OpenPass
    ' @param {string} baseURL - Base URL openpass domain to do authorization
    ' @return {object} authorizeDeviceData - Returns authorization object.
    self.AuthorizeDevice = sub (clientId, baseURL) as object
        if clientId = invalid or clientId.Len() = 0 then
            throw "clientId not provided"
        end if
        
        if baseURL = invalid or baseURL.Len() = 0 then
            throw "baseURL not provided"
        end if

        try
            authorizeDeviceData = m.RequestDeviceAuth(clientId, baseURL)
        catch e
            throw "Authorization error: " + e.message
        end try

        if authorizeDeviceData = invalid
            throw "authorizeDeviceData returned invalid"
        end if

        return authorizeDeviceData
    end sub


    ' Private methods

    ' Do operations to authorize device
    ' @param {string} clientId - ID provided by OpenPass
    ' @param {string} baseURL - Base URL openpass domain to do authorization
    ' @return {object} authorizationObject - Returns authorization object.
    self.requestDeviceAuth = sub (clientId, baseURL) as object
        ' Create a message port to monitor for the response
        port = createObject("roMessagePort")
        authorizeRequest = m.configureAuthorizationRequest(port, baseURL)
        params = m.configureAuthorizationURLParams(clientId)
        return m.executeAuthorizationRequest(authorizeRequest, params, port)
    end sub

    ' Execute a post request request usign parameters as string
    ' @param {roUrlTransfer} authorizeRequest - Object created to do the request itself
    ' @param {string} params - URL Params
    ' @param {roMessagePort} port - A Message Port is the place messages (events) are sent
    ' @return {object} authorizeDeviceData - Returns authorization object.
    self.executeAuthorizationRequest = sub (authorizeRequest, params, port) as object
        if authorizeRequest.AsyncPostFromString(params)
            timeout = 3000
            response = wait(timeout, port)
            if type(response) = "roUrlEvent"
                responseCode = response.GetResponseCode()
                
                if responseCode >= 400
                    throw "Error executing request. HTTP Code: " + responseCode.ToStr()
                end if

                responseBody = response.GetString()

                parsedResponse = ParseJson(responseBody)
                return parsedResponse
            end if
        end if
    end sub

    ' Configure authorization url parameters
    ' @param {string} clientId - ID provided by OpenPass
    ' @return {string} URL Params.
    self.configureAuthorizationURLParams = sub (clientId) as string
        return "client_id=" + clientId + "&scope=openid"
    end sub

    ' Configure parameters for roUtlTransfer object, e.g. headers, content type, type of request, etc
    ' @param {roMessagePort} port - A Message Port is the place messages (events) are sent
    ' @param {string} baseURL - Base URL openpass domain to do authorization
    ' @return {roUrlTransfer} searchRequest - Object created to do the request itself
    self.configureAuthorizationRequest = sub (port, baseURL) as object
        searchRequest = CreateObject("roUrlTransfer")
        searchRequest.SetRequest("POST")
        searchRequest.SetCertificatesFile("common:/certs/ca-bundle.crt")
        searchRequest.SetURL(baseURL + "/" + DefaultOpenPassURLS().authorize_device_path)

        ' Assign the message port to the request
        searchRequest.SetMessagePort(port)
        return searchRequest
    end sub

    ' @param {string} clientId - ID provided by OpenPass
    ' @param {object} parsedResponse - Response object retrieved from URL
    ' @return {object} buildAuthorizationObject - Object with client_id, device_code, user_code and verication uri.
    self.buildAuthorizationObject = sub (clientId, parsedResponse) as object
        return {
            client_id: clientId,
            device_code: parsedResponse.device_code,
            user_code: parsedResponse.user_code,
            verification_uri: parsedResponse.verification_uri,
            verification_uri_complete: parsedResponse.verification_uri_complete,
            interval: parsedResponse.interval,
            expires_in: parsedResponse.expires_in
        }
    end sub
    return self
end sub 
sub DefaultOpenPassURLS() as object
    BASE_URL = "https://auth.myopenpass.com"
    return {
        "base_url": BASE_URL,
        "device_token_path": "v1/api/device-token",
        "authorize_device_path": "v1/api/authorize-device",
        "jwks_path": ".well-known/jwks",
        "refresh_token_path": "v1/api/token"
    }
end sub 
' readJWT
' Read a JWT and returns the body
'
' @param jwtData, JWT parts in string
' @return AssocArray JWT Body
function DecodeIdTokenJwt(idTokenJwt) as object
    jwtDataSplit = idTokenJwt.split(".")

    if jwtDataSplit.count() < 3 then
        throw "Unable to decode JWT - Invalid JWT, Missing section"
    end if

    jwtHeader = CreateObject("roByteArray")
    jwtHeader.FromBase64String(base64UrlToBase64(jwtDataSplit[0]))
    header = parseJSON(jwtHeader.ToAsciiString())
    if header = Invalid then
        throw "Unable to decode JWT - Header is Invalid JSON"
    end if

    jwtBody = CreateObject("roByteArray")
    jwtBody.FromBase64String(base64UrlToBase64(jwtDataSplit[1]))
    body = parseJSON(jwtBody.ToAsciiString())
    if body = Invalid then
        throw "Unable to decode JWT - Body is Invalid JSON"
    end if  

    signature = base64UrlToBase64(jwtDataSplit[2])

    return {
        "header": header,
        "payload": body,
        "signature": signature
    }
end function

' base64UrlToBase64
' Convert base64url to base64
'
' @param base64url, base64URL to convert to base64
' @return Converted base64
function base64UrlToBase64 (base64url)
    base64 = base64Url.replace("-", "+").replace("_","/")
    length = base64.len() mod 4

    ' Add required padding, optional in base64url
    if length < 3 then
        base64 = base64 + "=="
    else if length < 4 then
        base64 = base64 + "="
    end if

    return base64
end function

' @param base64, base64 to convert to base64url
' @return Converted base64url
function base64ToBase64Url(base64)
    return base64.replace("+", "-").replace("/", "_").replace("=", "") ' + to -, / to _ and remove optional padding
end function

' ####### VALIDATION

' Check issuer, throw error if not as expected
' @param {string} issuer - An URL
sub CheckIssuer(issuer)
    if (issuer <> DefaultOpenPassURLS().base_url)
        throw "Different issuer. Issuer: " + issuer + " Expected issuer: " + DefaultOpenPassURLS().base_url
    end if
end sub

sub CheckAudience(aud, clientId)
    if aud <> clientId
        throw "Different Audience from Client Id."
    end if
end sub

' Get Known JWKs
sub GetKnownJWKs() as object
    port = createObject("roMessagePort")
    request = configureJWKRequest(port, DefaultOpenPassURLS().base_url)
    return executeJWKGetRequest(request, port).keys
end sub

' Check if key ids match any well known keys, return matched
' @param {string} kid - Key ID
' @param {object} keys - Well Known Keys
' @return {object} candidateKeys - Matched kid with well known keys
sub CheckForKID(kid, keys) as object
    candidateKeys = CreateObject("roList")

    for each key in keys        
        if kid = key.kid
            candidateKeys.AddTail(key)
        end if
    end for

    if candidateKeys.count() = 0
        throw "JWK not validated, no kid matched " + kid.ToStr() + "in known keys"
    end if
    
    return candidateKeys
end sub

' Verify if id token is valid, checks issuer, matched kids and validate algorithm
' @param {object} decodedIdTokenObj - Id Token decoded
' @param {string} encodedIdToken - Id Token encoded
' @param {string} clientId - Client Id
' @return {boolean} if valid returns true if not throws error
sub VerifyIdToken(decodedIdTokenObj, encodedIdToken, clientId) as boolean
    try
        keys = GetKnownJWKs()
    catch e
        throw "Unable to retrieve JWKs: " + e
    end try

    header = decodedIdTokenObj.header
    payload = decodedIdTokenObj.payload
    ' signature = decodedIdTokenObj.signature ' Unused for now

    CheckIssuer(payload.iss)
    CheckAudience(payload.aud, clientId)
    candidateKeys = CheckForKID(header.kid, keys)
    ' CheckJWK(header.alg, encodedIdToken, candidateKeys)

    return true
end sub

' Configure JWK request
' @param {roMessagePort} port - Message Port
' @param {string} baseURL - Base URL
' @return {object} Request
sub configureJWKRequest(port, baseURL) as object
    request = CreateObject("roUrlTransfer")
    request.SetRequest("GET")
    request.SetCertificatesFile("common:/certs/ca-bundle.crt")
    request.SetURL(baseURL + "/" + DefaultOpenPassURLS().jwks_path)

    ' Assign the message port to the request
    request.SetMessagePort(port)
    return request
end sub

' Get response from well known keys, throws error if response code >= 400
' @param {roUrlTransfer} request - Request object
' @param {roMessagePort} port - Message Port
' @return {object} Response
sub executeJWKGetRequest(request, port) as object
    if request.AsyncGetToString()
        timeout = 3000
        response = wait(timeout, port)
        if type(response) = "roUrlEvent"
            responseCode = response.GetResponseCode()

            if responseCode >= 400
                throw "Error executing request. HTTP Code: " + responseCode.ToStr()
            end if

            responseBody = response.GetString()

            return ParseJson(responseBody)
        end if
    end if
end sub 
' Embodies the token retrieval operations
sub DeviceTokenTask() as object
    self = CreateObject ("roAssociativeArray")
    self.shouldCancelPolling = false
    self.DEFAULT_SLOWDOWN_FACTOR_TIME = 5000

    ' Public methods

    self.Init = sub () as object
    end sub

    ' Checks if params are allright and start get token operation
    ' @param {string} clientId - Client ID provided by OpenPass
    ' @param {object} authorizeDeviceData - Provided by Authorize Device Step
    ' @param {object} baseURL - Base URL openpass domain to request a token
    ' @return {object} authorization - Returns authorization object.
    self.GetDeviceToken = sub (clientId, authorizeDeviceData, baseURL) as object
        m.CheckDeviceTokenParams(clientId, authorizeDeviceData, baseURL)
        return m.GetToken(clientId, authorizeDeviceData, baseURL)
    end sub

    ' Private methods

    ' Checks if params are valid
    ' @param {string} clientId - Client ID provided by OpenPass
    ' @param {object} authorizeDeviceData - Provided by Authorize Device Step
    ' @param {object} baseURL - Base URL openpass domain to request a token
    self.CheckDeviceTokenParams  = sub (clientId, authorizeDeviceData, baseURL)
        if clientId = invalid or clientId.Len() = 0 then
            throw "clientId not provided"
        end if
        if authorizeDeviceData = invalid or authorizeDeviceData.device_code = invalid or authorizeDeviceData.device_code.Len() = 0  then
            throw "deviceCode not provided"
        end if
        if baseURL = invalid or baseURL.Len() = 0 then
            throw "baseURL not provided"
        end if
    end sub

    ' Checks if token is expired
    ' @param {integer} tokenExpiresIn - Token expiration time
    ' @return {boolean} Is the token expired or not
    self.IsTokenExpired = sub (tokenExpiresIn) As boolean
        ' Is current time after expiration time?
        return tokenExpiresIn <= 0
    end sub

    ' Do operations to retrieve the token
    ' @param {string} clientId - Client ID provided by OpenPass
    ' @param {object} authorizeDeviceData - Provided by Authorize Device Step
    ' @param {object} baseURL - Base URL openpass domain to request a token
    ' @return {object} Response
    self.GetToken  = sub (clientId, authorizeDeviceData, baseURL) as object
        port = createObject("roMessagePort")
        authenticateRequest = m.configureAuthenticateRequest(baseURL, port)
        authenticateRequest.SetURL(baseURL + "/" + DefaultOpenPassURLS().device_token_path)
        params = m.configureAuthenticateURLParams(clientId, authorizeDeviceData.device_code)
        return m.executeAuthenticateRequest(authenticateRequest, params, port)
    end sub

    ' Do operations to retrieve the token using refresh token
    ' @param {string} clientId - Client ID provided by OpenPass
    ' @param {object} refreshToken - Provided by GetToken step
    ' @param {object} baseURL - Base URL openpass domain to request a token
    ' @return {object} Response
    self.RefreshToken = sub (clientId, refreshToken, baseURL) as object
        port = createObject("roMessagePort")
        request = m.configureAuthenticateRequest(baseURL, port)
        request.SetURL(baseURL + "/" + DefaultOpenPassURLS().refresh_token_path)
        params = m.configureRefreshTokenURLParams(clientId, refreshToken)
        return m.executeAuthenticateRequest(request, params, port)
    end sub

    ' Cancel Polling
    self.CancelPolling = sub ()
        m.shouldCancelPolling = true
    end sub

    ' Polling a url to retrieve device token
    ' @param {string} clientId - Client ID provided by OpenPass
    ' @param {object} authorizeDeviceData - Provided by Authorize Device Step
    ' @param {roAssociativeArray} additionalParams - Additional parameters provided by the user
    ' @return {object} deviceTokenData - The Token Object
    self.PollForDeviceToken = sub (clientId, authorizeDeviceData, additionalParams) as object
        baseURL = additionalParams.openpass_base_url
        pollingTime = authorizeDeviceData.interval * 1000
        deviceTokenData = invalid

        ' Keep trying until the request is fully completed
        while true
            if m.shouldCancelPolling
                return invalid
            end if

            try
                deviceTokenData = m.GetDeviceToken(clientId, authorizeDeviceData, baseURL)
            catch e
                throw "Not recoverable error: " + e.message
            end try

            if deviceTokenData.status = "ok"
                exit while
            else if deviceTokenData.status = "authorization_pending"
                sleep(pollingTime)
            else if deviceTokenData.status = "slow_down"
                ' Increase polling timeout by 5 seconds
                ' https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
                pollingTime += m.DEFAULT_SLOWDOWN_FACTOR_TIME
                sleep(pollingTime)
            else if deviceTokenData.status = "expired_token"
                throw "Your token is expired"
            else
                ' Shouldn't come here, just in case
                throw "Not recoverable error: " + FormatJson(deviceTokenData)
            end if
        end while

        return deviceTokenData.payload
    end sub

    ' Configure device token url parameters
    ' @param {string} clientId - Client ID provided by OpenPass
    ' @param {object} deviceCode - Provided by Authorize Device Step
    ' @return {string} URL Params.
    self.configureAuthenticateURLParams = sub (clientId, deviceCode) as string
        return "client_id=" + clientId + "&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=" + deviceCode
    end sub

    ' Configure device token url parameters
    ' @param {string} clientId - Client ID provided by OpenPass
    ' @param {object} refreshToken - Provided by GetToken Step
    ' @return {string} URL Params.
    self.configureRefreshTokenURLParams = sub (clientId, refreshToken) as string
        return "client_id=" + clientId + "&grant_type=refresh_token&refresh_token=" + refreshToken
    end sub

    ' Configure parameters for roUtlTransfer object, e.g. headers, content type, type of request, etc
    ' @param {object} baseURL - Base URL openpass domain to request a token
    ' @param {roMessagePort} port - A Message Port is the place messages (events) are sent
    ' @return {roUrlTransfer} searchRequest - Object created to do the request itself
    self.configureAuthenticateRequest = sub (baseURL, port) as object
        searchRequest = CreateObject("roUrlTransfer")
        searchRequest.SetRequest("POST")
        searchRequest.RetainBodyOnError(true)
        searchRequest.SetCertificatesFile("common:/certs/ca-bundle.crt")

        searchRequest.AddHeader("OpenPass-SDK-Name", "openpass-roku-sdk")
        try
            version = ReadAsciifile("pkg:/source/SDK_VERSION")
            searchRequest.AddHeader("OpenPass-SDK-Version", version)
        catch e
            ? "Could not retrieve SDK Version"
            searchRequest.AddHeader("OpenPass-SDK-Version", "Unknown")
        end try

        searchRequest.InitClientCertificates()
        searchRequest.SetMessagePort(port)
        return searchRequest
    end sub

    ' Execute a post request request usign parameters as string
    ' @param {roUrlTransfer} searchRequest - Object created to do the request itself
    ' @param {string} params - URL Params
    ' @param {roMessagePort} port - A Message Port is the place messages (events) are sent
    ' @return {roUrlTransfer} - Response
    self.executeAuthenticateRequest = sub (authenticateRequest, params, port) as object
        if authenticateRequest.AsyncPostFromString(params)
            response = wait(0, port)
            if type(response) = "roUrlEvent"
                responseCode = response.GetResponseCode()
                responseBody = response.GetString()
                tokenData = ParseJson(responseBody)

                if responseCode = 200
                    return { 
                        "status": "ok", 
                        "payload": tokenData
                    }
                else if tokenData.error = "authorization_pending"
                    return { 
                        "status": "authorization_pending", 
                        "payload": invalid
                    }
                else if tokenData.error = "slow_down"
                    return { 
                        "status": "slow_down", 
                        "payload": invalid 
                    }
                else if tokenData.error = "expired_token"
                    return { 
                        "status": "expired_token", 
                        "payload": invalid 
                    }
                end if
                throw "Response Code is: " + responseCode.ToStr()
            end if
        end if
    end sub
    return self
end sub 
' This class represents the primary way to interact with the SDK.
' Note: When the application is created the user should initialize it by calling Init(...).
'
' Public Methods: 
' - Init
' - SignIn
' - SignOut
' - CancelPolling
' - CurrentToken
' - CurrentState
sub OpenPassManager() as object

    self = CreateObject ("roAssociativeArray")
    self.clientId = invalid
    self.userCallback = invalid
    self.additionalParams = {}

    ' Represents the possible states the manager can be in.
    ' - 0: NotInitialized - The manager is created but not initialized
    ' - 1: Loading - The manager is loading the initial state or beginning the sign in process.
    ' - 2: SignedIn - The user has authenticated successfully and is considered to be signed-in.
    ' - 3: SignedOut - The user has not authenticated or has signed out.
    ' - 4: Polling - The sign-in process is in progress, waiting for the user to authenticate.
    ' - 5: Error - An error occurred. More information is included in the thrown exception or logs.
    self.states = {
        "NotInitialized": 0,
        "Loading": 1,
        "SignedIn": 2,
        "SignedOut": 3,
        "Polling": 4,
        "Error": 5
    }

    self.log = Logger()
    self.state = self.states.NotInitialized
    self.deviceTokenTaskObj = DeviceTokenTask()
    self.authorizeDeviceTaskObj = AuthorizeDeviceTask()

    ' Initializes the OpenPassManager.
    ' 
    ' @param {string} clientId - The applications client ID provided by OpenPass.
    ' @param {roAssociativeArray} userStateHandlerCallbackParam - A user defined method which is used to signify a state change in the sign-in flow. The method should take two parameters:
    '   - _state {integer} - The current state of the manager. This can be one of the states in the states enum.
    '   _ _data {object} - A map of data that can be used to provide more information about the state change. Possible values:
    '     * message {string} - An info message that can be used to provide more information about the state change.
    '     * error_message {string} - An error message that describes the error state.
    '     * warning_message {string} - A warning message that describes the warning state.
    '     * device_token_success {object} - A map of data that can be used to provide more information about the device token success.
    '       - decoded_id_token - The decoded ID token, which contains the user's identity details (including their email address).
    '       - id_token - The raw ID token string (JWT) which contains the user's identity details (including their email address).
    '       - access_token - The access token string, which can be used to do Bearer authorization against APIs.
    '       - expires_in - When the access token expires in seconds.
    '       - refresh_token - The refresh token string, which can be used to get a new access token.
    '       - refresh_token_expires_in - When the refresh token expires in seconds.
    '       - token_type - The token type, which is always "Bearer".
    '     * authorize_device_success {object} - The authorize device data returned from the OpenPass API. This is used to start the polling process:
    '       - client_id {string}: The applications client ID provided by OpenPass.
    '       - device_code {string}: The device code to be used by the device for polling the device-token endpoint to retrieve the access token.
    '       - user_code {string}: A user-friendly code that is typically displayed on the device for the subject to register or reject the device registration.
    '       - verification_uri {string}: The verification URI where the user can navigate to authorize the device by entering the user code.
    '       - verification_uri_complete {string}: The verification URI that contains the user code as an HTTP query parameter typically displayed on the device as part of the QR code.
    '       - interval {integer}: The number of seconds that the device should wait between polling requests to the device-token endpoint.
    '       - expires_in {interger}: The duration in seconds indicating the maximum time the device should poll for the registration result and the amount of time the user has to complete the device registration.
    ' @param {roAssociativeArray} optionalParams - A map of options to configure the manager:
    '   - openpass_base_url {string} - The base URL of the OpenPass API. Default: "https://auth.myopenpass.com"
    '   - enable_logging {boolean} - Enable logging to a file. Default: false
    '   - enable_testing {boolean} - Enable testing mode. Default: false
    self.Init = sub (clientId, userStateHandlerCallbackParam = {}, optionalParams = {})
        m.clientId = clientId
        m.additionalParams = m.ParseOptionalParams(optionalParams)
        m.userCallback = userStateHandlerCallbackParam

        ' Writes to file to tmp, destroyed after reboot
        m.log.Init("OpenPassManagerLogger", WriteToFileLogOperation, m.additionalParams.enable_logging)

        ' Checks if userCallback is invalid
        if (userStateHandlerCallbackParam = invalid)
            m.state = m.states.Error
            m.log.error("Init() - User Callback empty")
            throw "Init() - User Callback invalid"
        end if

        ' Checks if userCallback is not a function
        if type(userStateHandlerCallbackParam) <> "Function" and type(userStateHandlerCallbackParam) <> "roFunction"
            m.state = m.states.Error
            m.log.error("Init() - User Callback not a function")
            throw "Init() - User Callback not a function"
        end if

        if clientId = invalid or clientId.Len() = 0 then
            m.state = m.states.Error
            m.log.error("Init() - clientId not provided")
            m.userCallback(m.state, { "message": "clientId not provided" })
            return
        end if

        m.state = m.states.Loading
        m.userCallback(m.state, { "message": "Initialized" })
        m.log.info("Init() finished. State: " + m.state.ToStr() + " ClientId: " + clientId + " additional params: " + FormatJson(m.additionalParams))
    end sub

    ' Check if OpenPassManager was initialized. Throws error if not initialized
    self.CheckInitialization = sub ()
        if m.clientId = invalid or m.clientId.Len() = 0 or m.userCallback = invalid
            m.state = m.states.Error
            m.log.error("CheckIfInitialized() - Not initialized, need to call Init(clientId, userStateHandlerCallbackParam, ...) first")
            throw "CheckIfInitialized() - Not initialized, need to call Init(clientId, userStateHandlerCallbackParam, ...) first"
        end if
    end sub

    ' Signs out the current user. This will delete any authentication data stored in the registry.
    self.SignOut = sub ()
        m.CheckInitialization()
        try
            DeleteAuthSesssionData()
            m.state = m.states.SignedOut
            m.userCallback(m.state, { "message": "SignedOut" })
            m.log.info("SignOut() finished. State: " + m.state.ToStr())

        catch e
            ' Used for internal unit testing
            if m.additionalParams.enable_testing
                throw e
            end if

            m.log.error("SignOut() - An unexpected error occurred: " + e.message)
        end try
    end sub
    
    ' Retrieves the current authentication state as a string.
    ' @return {string} - Current state string, see State enum documentation for all possible values.
    self.CurrentState = sub () as string
        for each item in m.states.Items()
            if m.state = item.value
                return item.key
            end if
        end for
    end sub
    
    ' Retrieves the OpenPass tokens and metadata for the currently signed-in user.
    ' @return {roAssociativeArray} - A map with the currently signed-in users OpenPass tokens and metadata:
    '   - decoded_id_token - The decoded ID token, which contains the user's identity details (including their email address).
    '   - id_token - The raw ID token string (JWT) which contains the user's identity details (including their email address).
    '   - access_token - The access token string, which can be used to do Bearer authorization against APIs.
    '   - expires_in - When the access token expires in seconds.
    '   - refresh_token - The refresh token string, which can be used to get a new access token.
    '   - refresh_token_expires_in - When the refresh token expires in seconds.
    '   - token_type - The token type, which is always "Bearer".
    self.CurrentToken = sub () as object
        return GetAuthSessionData()
    end sub

    ' @param {object} authorizationData - AssocArray JWT Body
    self.SetSignedIn = sub (authorizationData, decodedIdToken) 
        authorizationData["decoded_id_token"] = decodedIdToken
        SetAuthSessionData(authorizationData)
        m.log.info("SetSignedIn() finished. authorizationData: " + FormatJson(authorizationData))
    end sub

    ' Retrieves the current token state (if it's signedin or not) based on the object stored in registry (like a cookie)
    ' @param {object} sessionDeviceTokenData - AssocArray JWT Body Object
    ' @return {integer} Current state
    self.GetStateByRegistry = sub (sessionDeviceTokenData) as integer
        if sessionDeviceTokenData <> invalid
            return m.states.SignedIn
        else 
            return m.states.SignedOut
        end if
    end sub

    ' This will abort the current active sign-in flow by canceling the polling for device token process.
    self.CancelPolling = sub ()
        m.CheckInitialization()
        try
            m.deviceTokenTaskObj.CancelPolling()
            m.state = m.states.SignedOut ' Is this state correct?
            m.userCallback(m.state, { "message": "Polling canceled" })
        catch e
            ' Used for internal unit testing
            if m.additionalParams.enable_testing
                throw e
            end if

            m.log.error("CancelPolling() - State" + m.state.ToStr() + "An unexpected error occurred: " + e.message)
        end try
    end sub

    ' Refresh Token
    self.RefreshToken = sub ()
        authSession = GetAuthSessionData()
        if m.deviceTokenTaskObj.IsTokenExpired(authSession.refresh_token_expires_in)
            m.userCallback(m.states.Error, { "message": "Refresh Token is expired" })
            return
        end if
        deviceTokenData = m.deviceTokenTaskObj.RefreshToken(m.clientId, authSession.refresh_token, m.additionalParams.openpass_base_url)
        decodedIdToken = m.ValidateTokenStep(deviceTokenData.payload)
        if m.state = m.states.Error
            return
        end if
        m.SetSignedIn(deviceTokenData.payload, decodedIdToken) 
        m.state = m.states.SignedIn
        sessionData = FormatSessionData(deviceTokenData.payload)
        m.userCallback(m.state, { "device_token_success":  sessionData})
        m.log.info("RefreshToken() - Finished. State: " + m.state.ToStr() + " SessionData: " + FormatJson(sessionData))
    end sub

    ' Loading initial signin configurations
    self.SignInLoadingStep = sub ()
        m.state = m.states.Loading
        m.userCallback(m.state, { "message": "Loading" })
    end sub

    ' Sign in Authorization
    ' @return {object} authorizeDeviceData - Authorization Object
    self.SignInAuthorizeDeviceStep = sub () as object
        try
            authorizeDeviceData = m.authorizeDeviceTaskObj.AuthorizeDevice(m.clientId, m.additionalParams.openpass_base_url)
            m.log.info("SignIn() - Authorize Device finished. authorizeDeviceData: " + FormatJson(authorizeDeviceData))
            return authorizeDeviceData
        catch e
            m.state = m.states.Error
            m.userCallback(m.state, { "error_message": "Unauthorized: " + e.message })
            m.log.error("SignIn() - Authorize Device Step - State: " + m.state.ToStr() + " Unauthorized. Error message: " + e.message)
            return invalid
        end try
    end sub

    ' Sign in New Token
    ' @param {object} authorizeDeviceData - Authorization Object
    ' @return {object} deviceTokenData - New token Object
    self.SignInNewTokenStep = sub (authorizeDeviceData as object) as object
        m.state = m.states.Polling
        ' This would be best placed in SignInAuthorizeDeviceStep, but we need to change the state first
        m.userCallback(m.state, { "authorize_device_success": authorizeDeviceData }) 
        m.log.info("SignIn() - New Token - Starting Polling. clientId: " + m.clientId + " device code: " + authorizeDeviceData.device_code + " additionalParams: " + FormatJson(m.additionalParams))
        
        try
            deviceTokenData = m.deviceTokenTaskObj.PollForDeviceToken(m.clientId, authorizeDeviceData, m.additionalParams)
        catch e
            m.state = m.states.Error
            m.userCallback(m.state, { "error_message": e.message })
            return invalid
        end try

        m.log.info("SignIn() - New Token - Finished Polling. Starting to decode Id Token: " + deviceTokenData.id_token)
        return deviceTokenData
    end sub

    ' Validate Token
    ' @param {object} deviceTokenData - New token Object
    self.ValidateTokenStep = sub (deviceTokenData as object) as object
        try
            decodedIdTokenObj = DecodeIdTokenJwt(deviceTokenData.id_token)
            decodedIdToken = decodedIdTokenObj.payload
            VerifyIdToken(decodedIdTokenObj, deviceTokenData.id_token, m.clientId)

            formattedDecodedIdToken = FormatJson(decodedIdToken)
            m.log.info("SignIn() - New Token - Id token decoded successfully. decodedIdToken: " + formattedDecodedIdToken)
            return formattedDecodedIdToken
        catch e
            m.state = m.states.Error
            m.userCallback(m.state, { "error_message": "Token Validation failed - " + e.message })
            m.log.error("SignIn() - Token Validation failed - State: " + m.state.ToStr() + " . Error Message: " + e.message)
        end try
    end sub

    ' Starts the sign-in process for the current user.
    self.SignIn = sub ()
        m.CheckInitialization()
        try
            m.SignInLoadingStep()

            authorizeDeviceData = m.SignInAuthorizeDeviceStep()
            if m.state = m.states.Error
                return
            end if

            deviceTokenData = m.SignInNewTokenStep(authorizeDeviceData)
            if m.state = m.states.Error
                return
            end if

            decodedIdToken = m.ValidateTokenStep(deviceTokenData)
            if m.state = m.states.Error
                return
            end if
            
            m.SetSignedIn(deviceTokenData, decodedIdToken) ' Set token in registry
            m.state = m.states.SignedIn

            sessionData = FormatSessionData(deviceTokenData)
            m.userCallback(m.state, { "device_token_success":  sessionData})
            m.log.info("SignIn() - Finished. State: " + m.state.ToStr() + " SessionData: " + FormatJson(sessionData))
        catch e
            ' Used for internal unit testing
            if m.additionalParams.enable_testing
                throw e
            end if

            m.state = m.states.Error
            m.userCallback(m.state, { "error_message": "Unexpected error: " + e.message })
            m.log.error("SignIn() - An unexpected error occurred: " + e.message)
        end try
    end sub

    ' Check if parameters are empty, if they are use default values
    self.ParseOptionalParams = sub (params) as object
        if params.openpass_base_url = invalid
            params.openpass_base_url = DefaultOpenPassURLS().base_url
        else
            urlPattern = CreateObject("roRegex", "[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)", "i")

            if(not urlPattern.IsMatch(params.openpass_base_url))
                throw("openpass_base_url is not a valid URL")
            end if
        end if

        if params.enable_logging = invalid
            params.enable_logging = false
        else
            if(getInterface(params.enable_logging, "ifBoolean") = invalid)
                throw("enable_logging is not a boolean")
            end if
        end if

        ' Used for internal unit testing
        if params.enable_testing = invalid
            params.enable_testing = false
        else
            if(getInterface(params.enable_testing, "ifBoolean") = invalid)
                throw("enable_testing is not a boolean")
            end if
        end if

        return params
    end sub
    return self
end sub  
' Get token `cookie`
sub GetAuthSessionData() As Dynamic
    sec = CreateObject("roRegistrySection", "Authentication")

    ' ToDo: Improve this, include others?
    if sec.Exists("DeviceRefreshToken") and sec.Exists("DeviceTokenType") and sec.Exists("DeviceRefreshTokenExpiresIn")
        deviceTokenType = sec.Read("DeviceTokenType")

        deviceRefreshToken = sec.Read("DeviceRefreshToken")
        refreshExpirationDateTimeInSeconds = sec.Read("DeviceRefreshTokenExpiresIn")

        deviceAccessToken = sec.Read("DeviceAccessToken")
        accessExpirationDateTimeInSeconds = sec.Read("DeviceAccessTokenExpiresIn")

        deviceIDToken = sec.Read("DeviceIDToken")

        nowDateTime = CreateObject("roDateTime")
        refresh_expires_in = refreshExpirationDateTimeInSeconds.ToInt() - nowDateTime.AsSeconds()
        access_expires_in = accessExpirationDateTimeInSeconds.ToInt() - nowDateTime.AsSeconds()
        decoded_id_token =  sec.Read("DeviceDecodedIdToken")

        deviceTokenData = {
            access_token: deviceAccessToken,
            expires_in: access_expires_in,
            id_token: deviceIDToken,
            refresh_token: deviceRefreshToken,
            refresh_token_expires_in: refresh_expires_in,
            token_type: deviceTokenType,
            decoded_id_token: ParseJson(decoded_id_token)
        }

        return deviceTokenData
    endif
    return invalid
end sub

' Create token `cookie`
sub SetAuthSessionData(deviceTokenData) As Void
    ' Set expiration Date
    nowDateTime = CreateObject("roDateTime")
    refreshTokenExpirationDateTimeEpoch = nowDateTime.AsSeconds() + deviceTokenData.refresh_token_expires_in
    accessTokenExpirationDateTimeEpoch = nowDateTime.AsSeconds() + deviceTokenData.expires_in
    
    sec = CreateObject("roRegistrySection", "Authentication")
    sec.Write("DeviceRefreshToken", deviceTokenData.refresh_token)
    sec.Write("DeviceRefreshTokenExpiresIn", refreshTokenExpirationDateTimeEpoch.ToStr())
    sec.Write("DeviceTokenType", deviceTokenData.token_type)

    sec.Write("DeviceAccessToken", deviceTokenData.access_token)
    sec.Write("DeviceAccessTokenExpiresIn", accessTokenExpirationDateTimeEpoch.ToStr())
    sec.Write("DeviceIDToken", deviceTokenData.id_token)
    sec.Write("DeviceDecodedIdToken", deviceTokenData.decoded_id_token)
    sec.Flush()
end sub

sub DeleteAuthSesssionData() As void
    sec = CreateObject("roRegistrySection", "Authentication")
    sec.Delete("DeviceRefreshToken")
    sec.Delete("DeviceRefreshTokenExpiresIn")
    sec.Delete("DeviceTokenType")
    sec.Delete("DeviceAccessToken")
    sec.Delete("DeviceAccessTokenExpiresIn")
    sec.Delete("DeviceIDToken")
    sec.Delete("DeviceDecodedIdToken")
    ' ToDo: Check if flush is necessary
    sec.Flush()
end sub

sub FormatSessionData(deviceTokenData) as object
    expires_in = CreateObject("roInt")
    expires_in.SetInt(deviceTokenData.expires_in)

    refresh_token_expires_in = CreateObject("roInt")
    refresh_token_expires_in.SetInt(deviceTokenData.refresh_token_expires_in)

    sessionData = {
        access_token: deviceTokenData.access_token,
        expires_in: expires_in,
        id_token: deviceTokenData.id_token,
        refresh_token: deviceTokenData.refresh_token,
        refresh_token_expires_in: refresh_token_expires_in,
        token_type: deviceTokenData.token_type,
        decoded_id_token: ParseJson(deviceTokenData.decoded_id_token)
    }

    return sessionData
end sub 
sub PrintLogOperation(category as String, level as String, message as String, args = invalid) as Void
    ? "->>>>>>> ";category;" # ";message;

    if args = invalid then
        ? ""
    else
        ? " -->>>>> ";args
    end if
end sub

' Options to write files, currently using tmp
' tmp -  Contents are not retained when application exits 
' cachefs - Data is evicted when more space is required for another Channel
' registry - Data size is limited
sub WriteToFileLogOperation(category as String, level as String, message as String, args = invalid)
    try
        fs = CreateObject("roFileSystem")
        if not fs.Exists("tmp:/logs")
            CreateDirectory("tmp:/logs")
        end if

        ba=CreateObject("roByteArray")
       
    
        ba.FromAsciiString("->>>>>>> " + " [" + level.ToStr() + "] " + category + " # " + message + chr(10)) ' chr(10) jumps line with LF
        ba.AppendFile("tmp:/logs/log.txt")
        if args <> invalid then
            ba.FromAsciiString(" -->>>>> " + args)
            ba.AppendFile("tmp:/logs/log.txt")
        end if
    catch e
        print "Could not write to log file: " + e.message
    end try
end sub

sub PrintFileLog(path="tmp:/logs/log.txt")
    fs = CreateObject("roFileSystem")
    if fs.Exists(path)
        ' Another Option
        ' ba2=CreateObject("roByteArray")
        ' ba2.ReadFile("tmp:/ByteArrayTestFile")
        ' print ba2.ToAsciiString()
        print ReadAsciifile(path)
    else
        print "Log file dont exist"
    end if
end sub 

sub Logger() as object
    self = CreateObject ("roAssociativeArray")
    self.category = invalid
    self.log = invalid
    self.loggingEnabled = invalid
    self.levels = {
        "1": "DEBUG",
        "2": "INFO",
        "3": "WARN",
        "4": "ERROR",
        "5": "CRITICAL"
    }

    self.Init = sub (_category = "Log" as String, _logOperation = PrintLogOperation as Function, _loggingEnabled = false as Boolean)
        m.category = _category
        m.log = _logOperation
        m.loggingEnabled = _loggingEnabled
    end sub

    self.Debug = sub(message as String, args = invalid) as Void
        if m.loggingEnabled = true
            m.log(m.category, m.levels["1"], message, args )
        end if
    end sub

    self.Info = sub(message as String, args = invalid) as Void
        if m.loggingEnabled = true
            m.log(m.category, m.levels["2"], message, args )
        end if
    end sub

    self.Warn = sub(message as String, args = invalid) as Void
        if m.loggingEnabled = true
            m.log(m.category, m.levels["3"], message, args )
        end if
    end sub

    self.Error = sub(message as String, args = invalid) as Void
        if m.loggingEnabled = true
            m.log(m.category, m.levels["4"], message, args )
        end if
    end sub

    self.Critical = sub(message as String, args = invalid) as Void
        if m.loggingEnabled = true
            m.log(m.category, m.levels["5"], message, args )
        end if
    end sub
    
    return self
end sub 
