const { Unit } = require('libflitter')
const fs = require('fs')
const radius = require('radius')
const uuid = require('uuid').v4

/**
 * Unit that provides a CoreID IAM-integrated RADIUS server.
 */
class RadiusUnit extends Unit {
    static get services() {
        return [...super.services, 'configs', 'output', 'models']
    }

    async go(app) {
        const config = this.getConfig()

        // Overwrite radius-server's global config object with the user-provided values
        const radiusConfig = require('radius-server/config')
        for ( const key in config ) {
            if ( !Object.hasOwnProperty.apply(config, [key]) ) continue
            radiusConfig[key] = config[key]
        }

        const { Authentication } = require('radius-server/dist/auth')
        const { UDPServer } = require('radius-server/dist/server/UDPServer')
        const { RadiusService } = require('radius-server/dist/radius/RadiusService')

        this.output.info('Starting RADIUS server...')

        // Create the authenticator instance
        const AuthMechanismus = require(`radius-server/dist/auth/${config.authentication}`)[config.authentication]
        const auth = new AuthMechanismus(config.authenticationOptions)

        const authentication = new Authentication(auth)
        this.server = new UDPServer(config.port)
        this.radiusService = new RadiusService(config.secret, authentication)

        // Define the server handler
        this.server.on('message', async (msg, rinfo) => {
            this.output.debug('Incoming RADIUS message...')
            const response = await this.handleMessage(msg)

            if ( response ) {
                this.output.debug('Sending response...')
                this.server.sendToClient(
                    response.data,
                    rinfo.port,
                    rinfo.address,
                    (err, _bytes) => {
                        if ( err ) {
                            this.output.error(`Error sending response to:`)
                            this.output.error(rinfo)
                        }
                    },
                    response.expectAcknowledgment
                )
            }
        })

        // Start the radius server
        await this.server.start()
        this.output.success('Started RADIUS server!')
    }

    async cleanup(app) {
        if ( this.server ) {
            // radius-server doesn't expose a "close" method explicitly, which is annoying
            // instead, reach in and close the internal UDP socket
            this.server.server.close()
        }
    }

    /**
     * Handle an incoming radius request message.
     * @param msg
     * @returns {Promise<{expectAcknowledgment: boolean, data: *}|undefined>}
     */
    async handleMessage(msg) {
        const RadiusClient = this.models.get('radius:Client')
        const clients = await RadiusClient.find({ active: true })

        // Try the secrets for all active clients.
        // If we can successfully decode the packet with a client's secret, then we know
        // that the message came from that client.
        let authenticatedClient
        let packet
        for ( const client of clients ) {
            try {
                packet = radius.decode({ packet: msg, secret: client.secret })
                authenticatedClient = client
                break
            } catch (e) {}
        }

        if (packet.code !== 'Access-Request') {
            console.error('unknown packet type: ', packet.code)
            return
        }

        // Include the RADIUS Client ID in the username so we can parse it out in the controller
        // This allows us to check IAM access in the controller
        packet.attributes['User-Name'] = `${packet.attributes['User-Name']}@${authenticatedClient.id}`
        this.output.info(`RADIUS auth attempt: ${packet.attributes['User-Name']}`)

        const response = await this.radiusService.packetHandler.handlePacket(packet)

        // still no response, we are done here
        if (!response || !response.code) {
            return
        }

        // all fine, return radius encoded response
        return {
            data: radius.encode_response({
                packet,
                code: response.code,
                secret: authenticatedClient.secret,  // use the client's secret to encode the response
                attributes: response.attributes,
            }),

            // if message is accept or reject, we conside this as final message
            // this means we do not expect a reponse from the client again (acknowledgement for package)
            expectAcknowledgment: response.code === 'Access-Challenge',
        };
    }

    getConfig() {
        const baseUrl = this.configs.get('app.url')

        const config = {
            port: this.configs.get('radius.port', 1812),
            secret: uuid(),  // this is never used - client-specific secrets are injected instead
            certificate: {
                cert: fs.readFileSync(this.configs.get('radius.cert_file.public')),
                key: [
                    {
                        pem: fs.readFileSync(this.configs.get('radius.cert_file.private')),
                    },
                ],
            },
            authentication: 'HTTPAuth',
            authenticationOptions: {
                url: `${baseUrl}api/v1/radius/attempt`,
            },
        }

        const passphrase = this.configs.get('saml.cert_file.passphrase')
        if ( passphrase ) {
            config.certificate.key[0].passphrase = passphrase
        }

        return config
    }
}

module.exports = exports = RadiusUnit
