/**
 * Custom Interceptor: Certificate Client Metadata (SFA / Open Finance Chile)
 *
 * Extrai os metadados do certificado de transporte mTLS (X.509) apresentado pelo
 * cliente e os propaga para os interceptors seguintes e para o backend.
 *
 * Responsabilidade principal: calcular o thumbprint SHA-256 do certificado
 * (x5t#S256, RFC 8705) e injetá-lo no header 'x-client-cert-thumbprint', usado
 * pelo access-token-authorization para validar o binding holder-of-key contra a
 * claim `cnf.x5t#S256` do token (sender-constrained token do SFA).
 *
 * Diferenças em relação ao Open Finance Brasil:
 *   - O binding NÃO é feito por client_id embutido no certificado. No SFA o
 *     client_id é opaco (emitido pelo AS no DCR) e o vínculo token↔cert é o
 *     thumbprint (cnf.x5t#S256). Por isso aqui calculamos e propagamos o thumbprint.
 *   - A identidade do participante (CN, O, organizationIdentifier) é propagada
 *     best-effort — o perfil exato do DN depende da CA aprovada pela CMF.
 *
 * Execution Point: Request (deve rodar ANTES do access-token-authorization).
 *
 * Requer que o terminador mTLS/gateway repasse o certificado do cliente no header
 * 'x-forwarded-client-cert' (XFCC) com o elemento `Cert` (PEM URL-encoded).
 *
 * Headers produzidos:
 *   - x-client-cert-thumbprint : SHA-256 do DER do cert em base64url (sem padding) = x5t#S256
 *   - x-organisation-name      : atributo O do Subject DN (se presente)
 *   - x-participant-id         : atributo CN do Subject DN (se presente)
 *   - x-organisation-id        : organizationIdentifier / OID 2.5.4.97 (se presente)
 *
 * @version 1.0.0
 * @author adelino.avelino@sensedia.com
 */


const VERSION = '1.0.0'

const ORGANISATION_IDENTIFIER_OID = '2.5.4.97'
const OID_PREFIX = 'OID.'

const CustomHeaderNames = {
    CLIENT_CERT: 'x-forwarded-client-cert',
    CLIENT_CERT_THUMBPRINT: 'x-client-cert-thumbprint',
    ORGANISATION_NAME: 'x-organisation-name',
    PARTICIPANT_ID: 'x-participant-id',
    ORGANISATION_ID: 'x-organisation-id',
    FAPI_INTERACTION_ID: 'x-fapi-interaction-id'
}

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: 'Certificado de cliente ausente o invá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)
        }
    }
}

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 XfccHelper = {
    // Extrai o PEM do certificado do cliente a partir do header XFCC (Envoy):
    // "Cert=\"<PEM URL-encoded>\";Chain=...;Subject=..." — valores podem vir
    // URL-encoded e entre aspas.
    _decodeValue: function (rawValue) {
        var value = String(rawValue)
        if (/%[0-9A-Fa-f]{2}/.test(value)) {
            value = decodeURIComponent(value.replace(/\+/g, '%20'))
        } else {
            value = value.replace(/\+/g, ' ')
        }
        return value.replace(/^"|"$/g, '')
    },
    getClientCertPem: function () {
        const certHeader = $call.request.getHeader(CustomHeaderNames.CLIENT_CERT)
        ValidationHelper.assertIsNotEmpty(certHeader, buildError(Errors.UNAUTHORIZED,
            'No se encontró el header "' + CustomHeaderNames.CLIENT_CERT + '" con el certificado del cliente'))

        var pem = null
        String(certHeader).split(';').forEach(function (item) {
            const index = item.indexOf('=')
            if (index === -1) {
                return
            }
            const key = item.substring(0, index)
            if (String(key).trim().toLowerCase() === 'cert') {
                pem = XfccHelper._decodeValue(item.substring(index + 1))
            }
        })

        ValidationHelper.assertIsNotEmpty(pem, buildError(Errors.UNAUTHORIZED,
            'El header XFCC no contiene el elemento "Cert" con el certificado del cliente en PEM'))
        return pem
    }
}

const CertificateHelper = {
    parse: function (pem) {
        try {
            const certFactory = java.security.cert.CertificateFactory.getInstance('X.509')
            const bytes = new java.lang.String(String(pem)).getBytes('UTF-8')
            return certFactory.generateCertificate(new java.io.ByteArrayInputStream(bytes))
        } catch (e) {
            throw new CustomError(buildError(Errors.UNAUTHORIZED, 'No fue posible parsear el certificado del cliente: ' + e.message))
        }
    },
    // Thumbprint SHA-256 do DER do certificado em base64url sem padding (= x5t#S256).
    getThumbprintX5tS256: function (cert) {
        const digest = java.security.MessageDigest.getInstance('SHA-256').digest(cert.getEncoded())
        return String(java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(digest))
    },
    // Mapeia os RDNs do Subject DN (RFC 2253) em um objeto { tipo: valor }.
    getSubjectAttributes: function (cert) {
        const subjectDn = String(cert.getSubjectX500Principal().getName())
        $console.debug('DEBUG: subjectDn', subjectDn)

        const attributes = {}
        const ldapName = new javax.naming.ldap.LdapName(subjectDn)
        const rdns = ldapName.getRdns()
        for (var i = 0; i < rdns.size(); i++) {
            var rdn = rdns.get(i)
            var type = String(rdn.getType()).trim()
            if (type.indexOf(OID_PREFIX) === 0) {
                type = type.substring(OID_PREFIX.length)
            }
            attributes[type] = String(rdn.getValue())
        }
        $console.debug('DEBUG: subjectAttributes', attributes)
        return attributes
    }
}

function propagateIdentity(attributes) {
    if (!ValidationHelper.isEmpty(attributes['O'])) {
        $call.request.setHeader(CustomHeaderNames.ORGANISATION_NAME, attributes['O'])
    }
    if (!ValidationHelper.isEmpty(attributes['CN'])) {
        $call.request.setHeader(CustomHeaderNames.PARTICIPANT_ID, attributes['CN'])
    }
    const organisationId = attributes['organizationIdentifier'] || attributes[ORGANISATION_IDENTIFIER_OID]
    if (!ValidationHelper.isEmpty(organisationId)) {
        $call.request.setHeader(CustomHeaderNames.ORGANISATION_ID, organisationId)
    }
}

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 [Certificate Client Metadata] version [' + VERSION + ']')

    try {
        const pem = XfccHelper.getClientCertPem()
        const cert = CertificateHelper.parse(pem)

        const thumbprint = CertificateHelper.getThumbprintX5tS256(cert)
        $console.debug('DEBUG: x5t#S256', thumbprint)
        $call.request.setHeader(CustomHeaderNames.CLIENT_CERT_THUMBPRINT, thumbprint)

        const attributes = CertificateHelper.getSubjectAttributes(cert)
        propagateIdentity(attributes)
    } 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()
