// https://tools.ietf.org/html/rfc3748#section-4.1

import * as NodeCache from 'node-cache';
import { RadiusPacket } from 'radius';
import debug from 'debug';
import { EAPTTLS } from './eapMethods/EAPTTLS';
import { makeid } from '../../helpers';
import {
	IPacketHandler,
	IPacketHandlerResult,
	PacketResponseCode
} from '../../types/PacketHandler';
import { IAuthentication } from '../../types/Authentication';
import { IEAPMethod } from '../../types/EAPMethod';

const log = debug('radius:eap');

export class EAPPacketHandler implements IPacketHandler {
	private eapMethods: IEAPMethod[] = [];

	// 	private eapConnectionStates: { [key: string]: { validMethods: IEAPMethod[] } } = {};
	private eapConnectionStates = new NodeCache({ useClones: false, stdTTL: 3600 }); // max for one hour

	constructor(authentication: IAuthentication) {
		this.eapMethods.push(new EAPTTLS(authentication));
	}

	/**
	 *
	 * @param data
	 * @param msgType 1 = identity, 21 = EAP-TTLS, 2 = notification, 4 = md5-challenge, 3 = NAK
	 */
	private async buildEAPResponse(
		identifier: number,
		msgType: number,
		data?: Buffer
	): Promise<IPacketHandlerResult> {
		/** build a package according to this:
			0                   1                   2                   3
			0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
		 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 |     Code      |  Identifier   |            Length             |
		 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 |     Type      |  Type-Data ...
		 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
		 */
		const buffer = Buffer.from([
			1, // request
			identifier,
			0, // length (1/2)
			0, //  length (2/2)
			msgType // 1 = identity, 21 = EAP-TTLS, 2 = notificaiton, 4 = md5-challenge, 3 = NAK
		]);

		const resBuffer = data ? Buffer.concat([buffer, data]) : buffer;
		// set EAP length header
		resBuffer.writeUInt16BE(resBuffer.byteLength, 2);

		return {
			code: PacketResponseCode.AccessChallenge,
			attributes: [['EAP-Message', buffer]]
		};
	}

	private decodeEAPHeader(msg: Buffer) {
		/**
		 * parse msg according to this:
		  0                   1                   2                   3
			0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
		 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 |     Code      |  Identifier   |            Length             |
		 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 |     Type      |  Type-Data ...
		 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
		 */

		/*
		code:
        1       Request
        2       Response
        3       Success
        4       Failure
         */
		const code = msg.slice(0, 1).readUInt8(0);
		/* identifier is a number */
		const identifier = msg.slice(1, 2).readUInt8(0);
		const length = msg.slice(2, 4).readInt16BE(0);
		/* EAP type */
		const type = msg.slice(4, 5).readUInt8(0);
		const data = msg.slice(5);

		return {
			code,
			identifier,
			length,
			type,
			data
		};
	}

	async handlePacket(
		attributes: { [key: string]: Buffer },
		orgRadiusPacket: RadiusPacket
	): Promise<IPacketHandlerResult> {
		if (!attributes['EAP-Message']) {
			// not an EAP message
			return {};
		}

		const stateID = (attributes.State && attributes.State.toString()) || makeid(16);

		if (!this.eapConnectionStates.get(stateID)) {
			this.eapConnectionStates.set(stateID, {
				validMethods: this.eapMethods // on init all registered eap methods are valid, we kick them out in case we get a NAK response
			});
		}

		// EAP MESSAGE
		const msg = attributes['EAP-Message'];

		const { code, type, identifier, data } = this.decodeEAPHeader(msg);

		const currentState = this.eapConnectionStates.get(stateID) as { validMethods: IEAPMethod[] };

		switch (code) {
			case 1: // for request
			case 2: // for response
				switch (type) {
					case 1: // identifiy
						log('>>>>>>>>>>>> REQUEST FROM CLIENT: IDENTIFY', {});
						// start identify
						if (currentState.validMethods.length > 0) {
							return currentState.validMethods[0].identify(identifier, stateID);
						}

						return this.buildEAPResponse(identifier, 3);
					case 2: // notification
						log('>>>>>>>>>>>> REQUEST FROM CLIENT: notification', {});
						console.info('notification');
						break;
					case 4: // md5-challenge
						log('>>>>>>>>>>>> REQUEST FROM CLIENT: md5-challenge', {});

						console.info('md5-challenge');
						break;
					case 254: // expanded type
						console.error('not implemented type', type);
						break;
					case 3: // nak
						if (data) {
							// if there is data, each data octect reprsents a eap method the clients supports,
							// kick out all unsupported ones
							const supportedEAPMethods: number[] = [];
							for (const supportedMethod of data) {
								supportedEAPMethods.push(supportedMethod);
							}

							this.eapConnectionStates.set(stateID, {
								...currentState,
								validMethods: currentState.validMethods.filter(method => {
									return supportedEAPMethods.includes(method.getEAPType()); // kick it out?
								})
							});
						}
					// continue with responding a NAK and add rest of supported methods
					// eslint-disable-next-line no-fallthrough
					default: {
						const eapMethod = currentState.validMethods.find(method => {
							return type === method.getEAPType();
						});

						if (eapMethod) {
							return eapMethod.handleMessage(identifier, stateID, msg, orgRadiusPacket);
						}

						// we do not support this auth type, ask for something we support
						const serverSupportedMethods = currentState.validMethods.map(
							method => method.getEAPType
						);

						console.error('unsupported type', type, `requesting: ${serverSupportedMethods}`);

						return this.buildEAPResponse(identifier, 3, Buffer.from(serverSupportedMethods));
					}
				}
				break;
			case 3:
				log('Client Auth Success');
				break;
			case 4:
				log('Client Auth FAILURE');
				break;
			default:
		}
		// silently ignore;
		return {};
	}
}
