/**
 * Custom Interceptor: Of Consent Validation (SFA / Open Finance Chile)
 *
 * Valida, na borda do gateway de recurso, se o consentimento concedido autoriza
 * a operação solicitada. No SFA o consentimento é expresso via authorization_details
 * (RAR / RFC 9396) carregado no próprio access token — não há um "consent engine"
 * separado nem permissions[]/resourceGroups/approvers como no Open Finance Brasil.
 *
 * Modelo token-based: lê authorization_details do JWT (já autenticado pelo
 * interceptor access-token-authorization, que roda antes) e exige que exista uma
 * concessão com o `type` do recurso e cujas `actions` cubram as ações requeridas
 * pelo endpoint. Revogação/expiração já são cobertas pela introspecção (RFC 7662)
 * e pelo `exp`, validados no access-token-authorization.
 *
 * Execution Point: Request
 *
 * Dependências (devem rodar ANTES deste interceptor):
 *   - access-token-authorization (SFA): autentica o token (assinatura PS256, exp,
 *     introspecção, binding mTLS) e propaga o header 'grantId'.
 *
 * Variáveis de CONTEXTO (contextVariables) — configuradas por API/operação:
 *   - context-authorization-type  : (obrigatória) o `type` RAR exigido pelo recurso.
 *                                   Ex.: 'Accounts', 'Payments', 'CreditCards'.
 *   - context-required-actions    : (opcional) lista (CSV/space) de `actions` que
 *                                   devem TODAS estar presentes na concessão do type.
 *                                   Ex.: 'ReadAccounts,ReadAccountsBalance'. Se ausente,
 *                                   valida apenas a presença do `type`.
 *   - forward-consent-context     : (opcional) 'true' para propagar downstream um header
 *                                   'x-consent-context' (base64) com { sub, grantId, type,
 *                                   grantedActions }. Default: não propaga.
 *
 * @version 1.0.0
 * @author adelino.avelino@sensedia.com
 */

const VERSION = '1.0.0'

const CustomHeaderNames = {
    FAPI_INTERACTION_ID: 'x-fapi-interaction-id',
    GRANT_ID: 'grantId',
    CONSENT_CONTEXT: 'x-consent-context'
}

const ContextVariablesNames = {
    AUTHORIZATION_TYPE: 'context-authorization-type',
    REQUIRED_ACTIONS: 'context-required-actions',
    FORWARD_CONSENT_CONTEXT: 'forward-consent-context'
}

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

const Errors = {
    UNAUTHORIZED: {
        status: 401,
        code: 'SFA_ERR_UNAUTHORIZED',
        title: 'Unauthorized',
        typeUri: ERROR_TYPE_BASE + 'unauthorized',
        detail: 'Encabezado de autenticación ausente o token inválido'
    },
    FORBIDDEN: {
        status: 403,
        code: 'SFA_ERR_FORBIDDEN',
        title: 'Forbidden',
        typeUri: ERROR_TYPE_BASE + 'forbidden',
        detail: 'El consentimiento no autoriza el acceso a este recurso'
    },
    INVALID_GRANT: {
        status: 403,
        code: 'SFA_ERR_INVALID_GRANT',
        title: 'Forbidden',
        typeUri: ERROR_TYPE_BASE + 'invalid-grant',
        detail: 'El token no contiene un consentimiento (authorization_details) válido'
    },
    INTERNAL: {
        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'
    }
}

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)
        }
    },
    toList: function (raw) {
        if (this.isEmpty(raw)) {
            return []
        }
        return String(raw).split(/[\s,]+/).filter(function (item) {
            return !ValidationHelper.isEmpty(item)
        })
    }
}

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 Base64Helper = {
    decodeUrlToString: function (encoded) {
        const bytes = java.util.Base64.getUrlDecoder().decode(new java.lang.String(String(encoded)))
        return String(new java.lang.String(bytes, 'UTF-8'))
    },
    encodeStringToUrl: function (value) {
        const bytes = new java.lang.String(String(value)).getBytes('UTF-8')
        return String(java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(bytes))
    }
}

const TokenHelper = {
    // O token já foi autenticado pelo access-token-authorization; aqui apenas
    // decodificamos o payload para ler o consentimento (authorization_details).
    getClaims: function () {
        const authorization = $request.getHeader('Authorization')
        ValidationHelper.assertIsNotEmpty(authorization, buildError(Errors.UNAUTHORIZED,
            'El header Authorization no fue encontrado en la solicitud'))

        const parts = String(authorization).split(' ')
        if (parts.length != 2 || parts[0].toLowerCase() != 'bearer') {
            throw new CustomError(buildError(Errors.UNAUTHORIZED, 'El header Authorization no tiene el formato Bearer esperado'))
        }

        const tokenParts = String(parts[1]).split('.')
        if (tokenParts.length !== 3) {
            throw new CustomError(buildError(Errors.UNAUTHORIZED, 'La estructura del JWT no es válida'))
        }

        var claims
        try {
            claims = JSON.parse(Base64Helper.decodeUrlToString(tokenParts[1]))
        } catch (e) {
            claims = null
        }
        ValidationHelper.assertIsNotEmpty(claims, buildError(Errors.UNAUTHORIZED, 'No fue posible decodificar el payload del JWT'))
        return claims
    },
    getAuthorizationDetails: function (claims) {
        return claims['authorization_details']
    },
    getSub: function (claims) {
        return claims['sub']
    },
    getGrantId: function (claims) {
        return $request.getHeader(CustomHeaderNames.GRANT_ID) || claims['grant_id']
    }
}

const ConsentHelper = {
    getRequiredType: function () {
        const type = $call.contextVariables.get(ContextVariablesNames.AUTHORIZATION_TYPE)
        ValidationHelper.assertIsNotEmpty(type, buildError(Errors.INTERNAL,
            'La context variable "' + ContextVariablesNames.AUTHORIZATION_TYPE + '" no está configurada para este recurso'))
        return String(type)
    },
    getRequiredActions: function () {
        return ValidationHelper.toList($call.contextVariables.get(ContextVariablesNames.REQUIRED_ACTIONS))
    },
    // Retorna as entries RAR cujo type corresponde ao exigido pelo recurso.
    _matchByType: function (authorizationDetails, requiredType) {
        return authorizationDetails.filter(function (detail) {
            return detail && String(detail.type) === String(requiredType)
        })
    },
    // Une as actions concedidas em todas as entries do type.
    _collectActions: function (matchedDetails) {
        const actions = []
        matchedDetails.forEach(function (detail) {
            const detailActions = Array.isArray(detail.actions) ? detail.actions : []
            detailActions.forEach(function (action) {
                const value = String(action)
                if (actions.indexOf(value) === -1) {
                    actions.push(value)
                }
            })
        })
        return actions
    },
    // Enforcement principal: presença do type + cobertura das actions exigidas.
    validate: function (authorizationDetails, requiredType, requiredActions) {
        if (!authorizationDetails || !Array.isArray(authorizationDetails) || authorizationDetails.length === 0) {
            throw new CustomError(buildError(Errors.INVALID_GRANT,
                'El token no contiene authorization_details (RAR / RFC 9396)'))
        }

        const matchedDetails = this._matchByType(authorizationDetails, requiredType)
        if (matchedDetails.length === 0) {
            const presentTypes = authorizationDetails.map(function (detail) { return String(detail && detail.type) })
            throw new CustomError(buildError(Errors.FORBIDDEN,
                'El consentimiento no autoriza el type "' + requiredType + '" requerido por el recurso. Types presentes: "' + presentTypes.join(', ') + '"'))
        }

        const grantedActions = this._collectActions(matchedDetails)
        $console.debug('DEBUG: grantedActions para type "' + requiredType + '"', grantedActions)

        const missingActions = requiredActions.filter(function (required) {
            return grantedActions.indexOf(String(required)) === -1
        })

        if (missingActions.length > 0) {
            throw new CustomError(buildError(Errors.FORBIDDEN,
                'El consentimiento no autoriza las actions requeridas para el recurso: "' + missingActions.join(', ') +
                '". Actions concedidas para "' + requiredType + '": "' + grantedActions.join(', ') + '"'))
        }

        return grantedActions
    }
}

function forwardConsentContext(claims, requiredType, grantedActions) {
    const shouldForward = $call.contextVariables.get(ContextVariablesNames.FORWARD_CONSENT_CONTEXT)
    if (String(shouldForward) !== 'true') {
        return
    }
    // Coerção p/ tipos JS nativos ANTES do JSON.stringify: getGrantId vem de
    // $request.getHeader (java.lang.String) e grantedActions pode ser lista Java.
    var grantedActionsJs = []
    for (var i = 0; i < grantedActions.length; i++) {
        grantedActionsJs.push(String(grantedActions[i]))
    }
    var consentContext = {
        sub: String(TokenHelper.getSub(claims)),
        grantId: String(TokenHelper.getGrantId(claims)),
        type: String(requiredType),
        grantedActions: grantedActionsJs
    }
    $request.setHeader(CustomHeaderNames.CONSENT_CONTEXT, Base64Helper.encodeStringToUrl(JSON.stringify(consentContext)))
    $console.debug('DEBUG: consentContext propagado', consentContext)
}

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 [Of Consent Validation] version [' + VERSION + ']')

    try {
        const requiredType = ConsentHelper.getRequiredType()
        const requiredActions = ConsentHelper.getRequiredActions()
        $console.debug('DEBUG: requiredType', requiredType)
        $console.debug('DEBUG: requiredActions', requiredActions)

        const claims = TokenHelper.getClaims()
        const authorizationDetails = TokenHelper.getAuthorizationDetails(claims)

        const grantedActions = ConsentHelper.validate(authorizationDetails, requiredType, requiredActions)

        forwardConsentContext(claims, requiredType, grantedActions)
    } 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.INTERNAL, 'Error inesperado en el gateway de la API.'), interactionId)
        }
    }
}

main()
