/**
 * Custom Interceptor: Access Token Authorization (SFA / Open Finance Chile)
 *
 * Valida o Access Token na borda do gateway para as APIs regulatórias do
 * Sistema de Finanzas Abiertas (SFA), conforme perfil de segurança da CMF
 * (NCG 514 / NCG 569, FAPI 2.0, RFC 9396 RAR, RFC 8705 mTLS-bound tokens).
 *
 * Adaptado do interceptor "Access Token Authorization" do Open Finance Brasil.
 * Diferenças em relação ao Brasil:
 *   - NÃO existe openbanking_intent_id nem escopos dinâmicos (consent:/enrollment:).
 *     O consentimento é granularizado via authorization_details (RAR / RFC 9396).
 *   - NÃO existe multi-brand: cada participante opera seu próprio Authorization Server.
 *   - NÃO existe client_credentials: apenas authorization_code e refresh_token.
 *   - Token é JWT assinado em PS256, validado localmente via JWKS e confirmado
 *     remotamente via introspecção RFC 7662 (/introspect) sobre mTLS.
 *   - Envelope de erro segue o padrão ResponseError (RFC 7807).
 *
 * Execution Point: Request
 *
 * Dependências (devem rodar ANTES deste interceptor):
 *   - certificate-client-metadata (SFA): calcula o thumbprint SHA-256 do certificado
 *     mTLS e injeta o header 'x-client-cert-thumbprint'. Usado para validar o binding
 *     holder-of-key contra a claim `cnf.x5t#S256` do token (RFC 8705).
 *
 * IMPORTANTE: este interceptor roda no gateway de um RESOURCE SERVER (APIs de dados),
 * não no Authorization Server. Portanto NENHUM endereço do AS é derivado do request nem
 * do token — todos os endpoints do AS (JWKS, introspecção) vêm de variáveis de ambiente
 * confiáveis, e o `iss` do token é apenas VALIDADO contra uma allow-list (allowed-issuers).
 *
 * Variáveis de AMBIENTE (environmentVariables):
 *   - environmentName            : ambiente de execução (development, sandbox, production...)
 *   - customerId                 : identificador do customer na plataforma
 *   - allowed-issuers            : lista (CSV) de issuers (`iss`) permitidos no token
 *   - jwks-uri                   : URL completa do JWKS do AS (obrigatória)
 *   - token-validation-endpoint  : URL do endpoint INTERNO de validação do AS
 *                                  (SfaTokenValidationResource): {as}/realms/{realm}/sfa/token-validation.
 *                                  Não é o /introspect padrão (sem mTLS, sem restrição de aud
 *                                  nem política de client-auth); modelo do ext/token/validation do BR.
 *   - token-validation-key       : segredo compartilhado do endpoint (realm attr
 *                                  sfa.token_validation_key); enviado no header X-SFA-Validation-Key.
 *   - resource-server-org-id     : org-id deste detentor; exigido no `aud` do token.
 *   - auth-server-call-timeout   : timeout (ms) das chamadas ao Authorization Server (default 1000)
 *
 * Variáveis de CONTEXTO (contextVariables):
 *   - expected-authorization-details-type : lista (CSV/space) de tipos RAR esperados
 *                                  para o recurso chamado (ex.: 'accounts', 'payments').
 *                                  Se ausente, a validação de type do RAR é ignorada.
 *   - use-timeout-on-instrospection-api-call : 'true'/'false' (default: usar timeout)
 *
 * @version 1.0.0
 * @author adelino.avelino@sensedia.com
 */

const VERSION = '1.0.0'

const DEFAULT_AUTH_SERVER_TIMEOUT = 1000
const CLOCK_SKEW_SECONDS = 30
const EXPECTED_TOKEN_ALG = 'PS256'

const CustomHeaderNames = {
    CLIENT_CERT: 'x-forwarded-client-cert',
    CLIENT_CERT_THUMBPRINT: 'x-client-cert-thumbprint',
    GRANT_ID: 'grantId',
    X_GRANT_ID: 'x-grant-id',
    CUSTOMER_ID: 'customerId',
    ENVIRONMENT_ID: 'environmentId',
    FAPI_INTERACTION_ID: 'x-fapi-interaction-id',
    // Segredo compartilhado para o endpoint interno de validação de token do AS
    // (SfaTokenValidationResource). Evita mTLS/client-auth nessa chamada interna.
    VALIDATION_KEY: 'X-SFA-Validation-Key'
}

const ContextVariablesNames = {
    EXPECTED_AUTHORIZATION_DETAILS_TYPE: 'expected-authorization-details-type',
    USE_TIMEOUT_ON_INTROSPECTION_API_CALL: 'use-timeout-on-instrospection-api-call',
    TOKEN_SCOPE: 'token-scope'
}

const EnvironmentVariablesNames = {
    ALLOWED_ISSUERS: 'allowed-issuers',
    AUTH_SERVER_CALL_TIMEOUT: 'auth-server-call-timeout',
    CUSTOMER_ID: 'customerId',
    ENVIRONMENT_ID: 'environmentName',
    JWKS_URI: 'jwks-uri',
    // URL do endpoint INTERNO de validação de token do AS
    // (SfaTokenValidationResource): {as}/realms/{realm}/sfa/token-validation.
    // Não é o /introspect padrão — este é interno (sem mTLS, sem restrição de
    // aud nem política de client-auth FAPI), no modelo do ext/token/validation do BR.
    TOKEN_VALIDATION_ENDPOINT: 'token-validation-endpoint',
    // Segredo compartilhado exigido pelo endpoint interno (realm attr
    // sfa.token_validation_key). Enviado no header X-SFA-Validation-Key.
    TOKEN_VALIDATION_KEY: 'token-validation-key',
    // org-id do PRÓPRIO resource server (detentor). Usado no check de `aud`:
    // este RS só aceita tokens cujo aud o inclua (SfaAudienceProtocolMapper).
    RESOURCE_SERVER_ORG_ID: 'resource-server-org-id'
}

const ERROR_TYPE_BASE = 'https://api.sfa-chile.cl/errors/'

// Códigos alinhados ao enum ProblemDetails.code do SFA (grant_mgmt.yaml).
const Errors = {
    ATV_001: {
        status: 500,
        code: 'SFA_ERR_INTERNAL',
        title: 'Internal Server Error',
        typeUri: ERROR_TYPE_BASE + 'internal-error',
        detail: 'Ocurrió un error al validar la configuración del gateway'
    },
    ATV_002: {
        status: 401,
        code: 'SFA_ERR_UNAUTHORIZED',
        title: 'Unauthorized',
        typeUri: ERROR_TYPE_BASE + 'unauthorized',
        detail: 'Encabezado de autenticación ausente/inválido o token inválido'
    },
    ATV_003: {
        status: 403,
        code: 'SFA_ERR_FORBIDDEN',
        title: 'Forbidden',
        typeUri: ERROR_TYPE_BASE + 'forbidden',
        detail: 'El token tiene un alcance incorrecto o se violó una política de seguridad'
    },
    ATV_004: {
        status: 500,
        code: 'SFA_ERR_INTERNAL',
        title: 'Internal Server Error',
        typeUri: ERROR_TYPE_BASE + 'internal-error',
        detail: 'Ocurrió un error en el gateway de la API o en el microservicio'
    }
}

const ValidationHelper = {
    isEmpty: function (parameter) {
        return (parameter === undefined || parameter === null || parameter === '' || parameter === 'undefined' || parameter === 'null')
    },
    assertIsNotEmpty: function (value, errorObj) {
        if (this.isEmpty(value)) {
            throw new CustomError(errorObj)
        }
    },
    isSecondsInFutureWithSkew: function (seconds) {
        const nowSeconds = new Date().getTime() / 1000
        return Number(seconds) > (nowSeconds - CLOCK_SKEW_SECONDS)
    }
}

function CustomError(error) {
    this.status = error.status
    this.code = error.code
    this.title = error.title
    this.typeUri = error.typeUri
    this.detail = error.detail
    this.description = error.description
}

function buildError(baseError, description) {
    return {
        status: baseError.status,
        code: baseError.code,
        title: baseError.title,
        typeUri: baseError.typeUri,
        detail: baseError.detail,
        description: description
    }
}

const EnvironmentsHelper = {
    getEnvironment: function (envName, required) {
        const envValue = $call.environmentVariables.get(String(envName))
        if (required !== false) {
            ValidationHelper.assertIsNotEmpty(envValue, buildError(Errors.ATV_004,
                'No se encontró la variable de ambiente "' + envName + '"'))
        }
        return ValidationHelper.isEmpty(envValue) ? null : String(envValue)
    }
}

const Base64Helper = {
    // Decodifica base64url para byte[] (java) — usado em partes do JWT e componentes da JWK.
    decodeUrlToBytes: function (encoded) {
        return java.util.Base64.getUrlDecoder().decode(new java.lang.String(String(encoded)))
    },
    decodeUrlToString: function (encoded) {
        return String(new java.lang.String(this.decodeUrlToBytes(encoded), 'UTF-8'))
    }
}

const JWTHelper = {
    _splitToken: function (jwt) {
        const parts = String(jwt).split('.')
        if (parts.length !== 3) {
            throw new CustomError(buildError(Errors.ATV_002, 'La estructura del JWT no es válida (se esperaban 3 segmentos)'))
        }
        return parts
    },
    getHeader: function (jwt) {
        const parts = this._splitToken(jwt)
        try {
            return JSON.parse(Base64Helper.decodeUrlToString(parts[0]))
        } catch (e) {
            throw new CustomError(buildError(Errors.ATV_002, 'No fue posible decodificar el header del JWT'))
        }
    },
    getClaims: function (jwt) {
        const parts = this._splitToken(jwt)
        let claims
        try {
            claims = JSON.parse(Base64Helper.decodeUrlToString(parts[1]))
        } catch (e) {
            claims = null
        }
        if (!claims) {
            throw new CustomError(buildError(Errors.ATV_002, 'No fue posible decodificar el payload del JWT'))
        }
        return claims
    },
    getSigningInputBytes: function (jwt) {
        const parts = this._splitToken(jwt)
        const signingInput = parts[0] + '.' + parts[1]
        return new java.lang.String(signingInput).getBytes('US-ASCII')
    },
    getSignatureBytes: function (jwt) {
        const parts = this._splitToken(jwt)
        return Base64Helper.decodeUrlToBytes(parts[2])
    }
}

const JwksHelper = {
    _getJwksUri: function () {
        // Endereço fixo e confiável do JWKS do AS — nunca derivado do request nem do token.
        return EnvironmentsHelper.getEnvironment(EnvironmentVariablesNames.JWKS_URI)
    },
    _fetchJwks: function (jwksUri, timeoutMs) {
        $call.tracer.trace('AVISO: Inicio de la llamada al JWKS en ' + jwksUri)
        const headers = { 'Accept': 'application/json' }
        const response = timeoutMs
            ? $http.get(jwksUri, headers, new java.lang.Integer(Number(timeoutMs / 1000)))
            : $http.get(jwksUri, headers)
        $call.tracer.trace('AVISO: Fin de la llamada al JWKS en ' + jwksUri)

        if (response.status != 200) {
            throw new CustomError(buildError(Errors.ATV_004,
                'La llamada al JWKS retornó status ' + response.status))
        }
        let jwks
        try {
            jwks = JSON.parse(response.responseText)
        } catch (e) {
            throw new CustomError(buildError(Errors.ATV_004, 'El JWKS retornado no es un JSON válido'))
        }
        return (jwks && jwks.keys) ? jwks.keys : []
    },
    _findKeyByKid: function (keys, kid) {
        for (var i = 0; i < keys.length; i++) {
            if (String(keys[i].kid) === String(kid)) {
                return keys[i]
            }
        }
        return null
    },
    _buildRsaPublicKey: function (jwk) {
        const modulus = new java.math.BigInteger(1, Base64Helper.decodeUrlToBytes(jwk.n))
        const exponent = new java.math.BigInteger(1, Base64Helper.decodeUrlToBytes(jwk.e))
        const spec = new java.security.spec.RSAPublicKeySpec(modulus, exponent)
        return java.security.KeyFactory.getInstance('RSA').generatePublic(spec)
    },
    _verifyPS256: function (publicKey, signingInputBytes, signatureBytes) {
        const signature = java.security.Signature.getInstance('RSASSA-PSS')
        const pssSpec = new java.security.spec.PSSParameterSpec(
            'SHA-256', 'MGF1', java.security.spec.MGF1ParameterSpec.SHA256, 32, 1)
        signature.setParameter(pssSpec)
        signature.initVerify(publicKey)
        signature.update(signingInputBytes)
        return signature.verify(signatureBytes)
    },
    // Valida a assinatura PS256 do token localmente usando a JWKS publicada pelo AS.
    verifySignature: function (accessToken, timeoutMs) {
        const header = JWTHelper.getHeader(accessToken)
        $console.debug('DEBUG: token header', header)

        if (String(header.alg) !== EXPECTED_TOKEN_ALG) {
            throw new CustomError(buildError(Errors.ATV_002,
                'El algoritmo de firma del token es "' + header.alg + '", se esperaba "' + EXPECTED_TOKEN_ALG + '"'))
        }

        const jwksUri = this._getJwksUri()
        const keys = this._fetchJwks(jwksUri, timeoutMs)

        const jwk = this._findKeyByKid(keys, header.kid)
        ValidationHelper.assertIsNotEmpty(jwk, buildError(Errors.ATV_002,
            'No se encontró la clave (kid="' + header.kid + '") en el JWKS del Authorization Server'))

        var isValid
        try {
            const publicKey = this._buildRsaPublicKey(jwk)
            isValid = this._verifyPS256(publicKey, JWTHelper.getSigningInputBytes(accessToken), JWTHelper.getSignatureBytes(accessToken))
        } catch (e) {
            throw new CustomError(buildError(Errors.ATV_002,
                'Error al verificar la firma del token: ' + e.message))
        }

        if (!isValid) {
            throw new CustomError(buildError(Errors.ATV_002, 'La firma del token es inválida'))
        }
    }
}

const TokenHelper = {
    _getClaim: function (claims, claim, required) {
        const value = claims[claim]
        if (required !== false) {
            ValidationHelper.assertIsNotEmpty(value, buildError(Errors.ATV_002, 'No se encontró la claim "' + claim + '" en el token'))
        }
        return value
    },
    getAuthorizationHeader: function () {
        const authorization = $request.getHeader('Authorization')
        ValidationHelper.assertIsNotEmpty(authorization, buildError(Errors.ATV_002,
            'El header Authorization no fue encontrado en la solicitud'))
        return authorization
    },
    getTokenFromAuthorizationHeader: function (authorization) {
        const parts = String(authorization).split(' ')
        if (parts.length != 2) {
            throw new CustomError(buildError(Errors.ATV_002, 'El header Authorization no tiene el formato correcto'))
        }
        if (parts[0].toLowerCase() != 'bearer') {
            throw new CustomError(buildError(Errors.ATV_002, 'El header Authorization no está con el prefijo Bearer'))
        }
        return parts[1]
    },
    getIssuer: function (claims) {
        return String(this._getClaim(claims, 'iss'))
    },
    getExpiration: function (claims) {
        return this._getClaim(claims, 'exp')
    },
    getScope: function (claims) {
        const scope = this._getClaim(claims, 'scope', false)
        if (ValidationHelper.isEmpty(scope)) {
            return ''
        }
        return Array.isArray(scope) ? String(scope.join(' ')) : String(scope)
    },
    getAuthorizationDetails: function (claims) {
        return claims['authorization_details']
    },
    // Este resource server só aceita tokens cujo `aud` o inclua (org-id do detentor).
    // É a contraparte da introspecção: o mesmo org-id que autoriza a introspecção
    // (estar no aud) precisa ser exigido no aud do token servido aqui.
    assertAudienceContains: function (claims, expectedAud) {
        const aud = claims['aud']
        const list = Array.isArray(aud) ? aud.map(String) : (aud ? [String(aud)] : [])
        if (list.indexOf(String(expectedAud)) === -1) {
            throw new CustomError(buildError(Errors.ATV_003,
                'El token no fue emitido para este resource server (aud no contiene "' + expectedAud + '")'))
        }
    },
    // Thumbprint do certificado ao qual o token está vinculado (RFC 8705).
    getCnfThumbprint: function (claims) {
        const cnf = claims['cnf']
        const thumbprint = cnf ? cnf['x5t#S256'] : null
        ValidationHelper.assertIsNotEmpty(thumbprint, buildError(Errors.ATV_002,
            'El token no contiene la confirmación de certificado cnf["x5t#S256"] (RFC 8705)'))
        return String(thumbprint)
    },
    assertTokenIsNotExpired: function (expSeconds) {
        if (!ValidationHelper.isSecondsInFutureWithSkew(expSeconds)) {
            throw new CustomError(buildError(Errors.ATV_002, 'El token está expirado'))
        }
    },
    // Binding holder-of-key: o thumbprint da claim cnf.x5t#S256 deve coincidir com
    // o thumbprint do certificado mTLS efetivamente apresentado na conexão.
    assertTokenBoundToCertificate: function (claims, certificateThumbprint) {
        const tokenThumbprint = this.getCnfThumbprint(claims)
        if (String(tokenThumbprint) != String(certificateThumbprint)) {
            throw new CustomError(buildError(Errors.ATV_002,
                'El token no está vinculado al certificado mTLS presentado (cnf.x5t#S256 no coincide)'))
        }
    }
}

const AuthorizationDetailsHelper = {
    _getExpectedTypes: function () {
        const raw = $call.contextVariables.get(ContextVariablesNames.EXPECTED_AUTHORIZATION_DETAILS_TYPE)
        if (ValidationHelper.isEmpty(raw)) {
            return null
        }
        return String(raw).split(/[\s,]+/).filter(function (t) { return !ValidationHelper.isEmpty(t) })
    },
    // Valida a presença de authorization_details (RAR) e, se configurado, o(s) type(s) esperado(s).
    validate: function (authorizationDetails) {
        const expectedTypes = this._getExpectedTypes()

        if (!authorizationDetails || !Array.isArray(authorizationDetails) || authorizationDetails.length === 0) {
            throw new CustomError(buildError(Errors.ATV_003,
                'El token no contiene authorization_details (RAR / RFC 9396)'))
        }

        if (ValidationHelper.isEmpty(expectedTypes)) {
            $call.tracer.trace('AVISO: context variable "' + ContextVariablesNames.EXPECTED_AUTHORIZATION_DETAILS_TYPE +
                '" no configurada, ignorando validación de type del authorization_details.')
            return
        }

        const presentTypes = authorizationDetails.map(function (detail) { return String(detail && detail.type) })
        $console.debug('DEBUG: authorization_details types', presentTypes)

        const hasExpectedType = expectedTypes.some(function (expected) {
            return presentTypes.indexOf(String(expected)) !== -1
        })

        if (!hasExpectedType) {
            throw new CustomError(buildError(Errors.ATV_003,
                'El authorization_details del token no contiene ninguno de los types esperados para el recurso: "' +
                expectedTypes.join(', ') + '". Types presentes: "' + presentTypes.join(', ') + '"'))
        }
    }
}

function getCertificateThumbprint() {
    const thumbprint = $call.request.getHeader(CustomHeaderNames.CLIENT_CERT_THUMBPRINT)
    ValidationHelper.assertIsNotEmpty(thumbprint, buildError(Errors.ATV_002,
        'El header "x-client-cert-thumbprint" no fue agregado por el interceptor certificate-client-metadata'))
    return String(thumbprint)
}

function assertIssuerIsAllowed(issuer) {
    // O issuer aceito é configurado por ambiente (allow-list). O `iss` do token nunca é
    // usado para construir URLs — só é aceito se estiver na lista confiável.
    const allowedIssuers = EnvironmentsHelper.getEnvironment(EnvironmentVariablesNames.ALLOWED_ISSUERS)
        .split(',').map(function (value) { return String(value).trim() })
    $console.debug('DEBUG: allowedIssuers', allowedIssuers)

    if (allowedIssuers.indexOf(String(issuer)) === -1) {
        throw new CustomError(buildError(Errors.ATV_002,
            'El issuer "' + issuer + '" no está en la lista de issuers permitidos'))
    }
}

function getAuthServerTimeout() {
    const useTimeout = $call.contextVariables.get(ContextVariablesNames.USE_TIMEOUT_ON_INTROSPECTION_API_CALL)
    // Mantém compatibilidade: se a context variable não existir, o timeout é aplicado.
    const shouldUseTimeout = !useTimeout || String(useTimeout) == 'true'
    if (!shouldUseTimeout) {
        $call.tracer.trace('AVISO: Timeout deshabilitado en las llamadas al Authorization Server.')
        return null
    }
    let timeout = $call.environmentVariables.get(EnvironmentVariablesNames.AUTH_SERVER_CALL_TIMEOUT)
    if (ValidationHelper.isEmpty(timeout)) {
        $call.tracer.trace('AVISO: variable "' + EnvironmentVariablesNames.AUTH_SERVER_CALL_TIMEOUT +
            '" no encontrada, usando default de ' + DEFAULT_AUTH_SERVER_TIMEOUT + ' ms')
        timeout = DEFAULT_AUTH_SERVER_TIMEOUT
    }
    return Number(timeout)
}

function getIntrospectionUri() {
    // Endereço fixo e confiável do endpoint INTERNO de validação de token do AS
    // (SfaTokenValidationResource) — nunca derivado do request nem do token.
    return EnvironmentsHelper.getEnvironment(EnvironmentVariablesNames.TOKEN_VALIDATION_ENDPOINT)
}

// Validação de estado do token no endpoint INTERNO do AS (SfaTokenValidationResource,
// no modelo do ext/token/validation do BR). É uma chamada interna do resource server:
//   - SEM mTLS e SEM client_id: o endpoint não é o /introspect padrão, então não exige
//     que o chamador seja um client FAPI no `aud` do token;
//   - autenticada por um segredo compartilhado no header X-SFA-Validation-Key
//     (realm attr sfa.token_validation_key), já que a chamada é interna ao manager;
//   - o AS valida assinatura + exp + estado do grant (revogação) e devolve {active,...}.
function callTokenIntrospect(accessToken, environmentId, customerId, timeoutMs) {
    const introspectionUri = getIntrospectionUri()

    const headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
    headers[CustomHeaderNames.ENVIRONMENT_ID] = environmentId
    headers[CustomHeaderNames.CUSTOMER_ID] = customerId
    const validationKey = EnvironmentsHelper.getEnvironment(EnvironmentVariablesNames.TOKEN_VALIDATION_KEY, false)
    if (!ValidationHelper.isEmpty(validationKey)) {
        headers[CustomHeaderNames.VALIDATION_KEY] = validationKey
    }

    const body = new com.sensedia.interceptor.externaljar.rest.UrlEncodedForm()
    body.addParam('token', accessToken)

    $call.tracer.trace('AVISO: Inicio de la introspección en ' + introspectionUri)
    const response = timeoutMs
        ? $http.post(introspectionUri, headers, body, new java.lang.Integer(Number(timeoutMs / 1000)))
        : $http.post(introspectionUri, headers, body)
    $call.tracer.trace('AVISO: Fin de la introspección en ' + introspectionUri)

    return response
}

function validateIntrospectResponse(response) {
    if (response.status != 200) {
        throw new CustomError(buildError(Errors.ATV_002,
            'La introspección retornó status ' + response.status + ' en la validación del token'))
    }
    let introspection
    try {
        introspection = JSON.parse(response.responseText)
    } catch (e) {
        throw new CustomError(buildError(Errors.ATV_004, 'La respuesta de la introspección no es un JSON válido'))
    }
    $console.debug('DEBUG: introspection', introspection)

    if (introspection.active !== true) {
        throw new CustomError(buildError(Errors.ATV_002, 'El token no está activo (introspection.active=false)'))
    }
    return introspection
}

function getInteractionId() {
    return $call.request.getHeader(CustomHeaderNames.FAPI_INTERACTION_ID) || uuidv4()
}

function sendHttpError(error, interactionId) {
    const errorResponseBody = {
        type: String(error.typeUri),
        title: String(error.title),
        status: Number(error.status),
        code: String(error.code),
        detail: String(error.detail),
        instance: String($call.request.getRequestedUrl().getPath())
    }
    errorResponseBody[CustomHeaderNames.FAPI_INTERACTION_ID] = String(interactionId)

    $call.stopFlow = true
    $call.decision.setAccept(false)
    $call.response = new com.sensedia.interceptor.externaljar.dto.ApiResponse()
    $call.response.setStatus(Number(error.status))
    $call.response.getBody().setString(JSON.stringify(errorResponseBody), 'utf-8')
    $call.response.setHeader('Content-Type', 'application/json')
    $call.response.setHeader(CustomHeaderNames.FAPI_INTERACTION_ID, String(interactionId))

    $console.debug('DEBUG: errorResponseBody', errorResponseBody)
}

function uuidv4() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8)
        return v.toString(16)
    })
}

/*
 * Main
 */
function main() {
    $call.tracer.trace('SFA Custom Javascript Interceptor: Name [Access Token Authorization] version [' + VERSION + ']')

    try {
        const environmentId = EnvironmentsHelper.getEnvironment(EnvironmentVariablesNames.ENVIRONMENT_ID)
        const customerId = EnvironmentsHelper.getEnvironment(EnvironmentVariablesNames.CUSTOMER_ID)

        const authorization = TokenHelper.getAuthorizationHeader()
        const accessToken = TokenHelper.getTokenFromAuthorizationHeader(authorization)

        const claims = JWTHelper.getClaims(accessToken)
        $console.debug('DEBUG: claims', claims)

        // 1) Issuer allow-list (endereços do AS vêm de env, nunca do token)
        const issuer = TokenHelper.getIssuer(claims)
        assertIssuerIsAllowed(issuer)

        // 1b) Audiência: este resource server (org-id do detentor) deve estar no `aud`.
        //     É o mesmo org-id usado como client_id na introspecção (precisa estar no aud).
        const resourceServerOrgId = EnvironmentsHelper.getEnvironment(EnvironmentVariablesNames.RESOURCE_SERVER_ORG_ID)
        TokenHelper.assertAudienceContains(claims, resourceServerOrgId)

        const timeoutMs = getAuthServerTimeout()

        // 2) Validação local: assinatura PS256 via JWKS
        JwksHelper.verifySignature(accessToken, timeoutMs)

        // 3) Expiração (com clock skew)
        TokenHelper.assertTokenIsNotExpired(TokenHelper.getExpiration(claims))

        // 4) Scope disponível para downstream (não é o mecanismo de consentimento no SFA)
        const scope = TokenHelper.getScope(claims)
        $call.contextVariables.put(ContextVariablesNames.TOKEN_SCOPE, scope)

        // 5) Consentimento via authorization_details (RAR / RFC 9396)
        AuthorizationDetailsHelper.validate(TokenHelper.getAuthorizationDetails(claims))

        // 6) Binding token <-> certificado mTLS (holder-of-key): cnf.x5t#S256 do token
        //    deve casar com o thumbprint do cert injetado pelo certificate-client-metadata.
        const certificateThumbprint = getCertificateThumbprint()
        TokenHelper.assertTokenBoundToCertificate(claims, certificateThumbprint)

        // 7) Validação remota: introspecção RFC 7662 (revogação/estado do token)
        const introspectionResponse = callTokenIntrospect(accessToken, environmentId, customerId, timeoutMs)
        const introspection = validateIntrospectResponse(introspectionResponse)

        // 8) Propaga o grant_id (se presente) para os serviços downstream
        const grantId = introspection.grant_id || claims['grant_id']
        if (!ValidationHelper.isEmpty(grantId)) {
            $call.request.setHeader(CustomHeaderNames.GRANT_ID, String(grantId))
            $call.request.setHeader(CustomHeaderNames.X_GRANT_ID, String(grantId))   
        }
    } catch (err) {
        const interactionId = getInteractionId()
        if (err instanceof CustomError) {
            $call.tracer.trace('CustomError: ' + JSON.stringify(err))
            sendHttpError(err, interactionId)
        } else {
            $call.tracer.trace('Exception message => ' + err.message + ' <= in line => ' + err.stack)
            sendHttpError(buildError(Errors.ATV_004, 'Error inesperado en el gateway de la API.'), interactionId)
        }
    }
}

main()
