import config from "config";
import axios from "axios";
import async from "async";
import cors from "cors";
import cookieParser from "cookie-parser";
import bodyParser from "body-parser";
import express from "express";
import oauth from "oauth";
import http from "http";
import CoreClass from "../core";

const { OAuth2 } = oauth;

let AppModule;
let MailModule;
let CacheModule;
let DBModule;
let ActivitiesModule;
let PlaylistsModule;
let UtilsModule;

class _AppModule extends CoreClass {
	// eslint-disable-next-line require-jsdoc
	constructor() {
		super("app");

		AppModule = this;
	}

	/**
	 * Initialises the app module
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	initialize() {
		return new Promise(resolve => {
			MailModule = this.moduleManager.modules.mail;
			CacheModule = this.moduleManager.modules.cache;
			DBModule = this.moduleManager.modules.db;
			ActivitiesModule = this.moduleManager.modules.activities;
			PlaylistsModule = this.moduleManager.modules.playlists;
			UtilsModule = this.moduleManager.modules.utils;

			const app = (this.app = express());
			const SIDname = config.get("cookie");
			this.server = http.createServer(app).listen(config.get("port"));

			app.use(cookieParser());

			app.use(bodyParser.json());
			app.use(bodyParser.urlencoded({ extended: true }));

			let userModel;
			DBModule.runJob("GET_MODEL", { modelName: "user" })
				.then(model => {
					userModel = model;
				})
				.catch(console.error);

			const appUrl = `${config.get("url.secure") ? "https" : "http"}://${config.get("url.host")}`;

			const corsOptions = JSON.parse(JSON.stringify(config.get("cors")));
			corsOptions.origin.push(appUrl);
			corsOptions.credentials = true;

			app.use(cors(corsOptions));
			app.options("*", cors(corsOptions));

			/**
			 * @param {object} res - response object from Express
			 * @param {string} err - custom error message
			 */
			function redirectOnErr(res, err) {
				res.redirect(`${appUrl}?err=${encodeURIComponent(err)}`);
			}

			if (config.get("apis.github.enabled")) {
				const oauth2 = new OAuth2(
					config.get("apis.github.client"),
					config.get("apis.github.secret"),
					"https://github.com/",
					"login/oauth/authorize",
					"login/oauth/access_token",
					null
				);

				const redirectUri =
					config.get("apis.github.redirect_uri").length > 0
						? config.get("apis.github.redirect_uri")
						: `${appUrl}/backend/auth/github/authorize/callback`;

				app.get("/auth/github/authorize", async (req, res) => {
					if (this.getStatus() !== "READY") {
						this.log(
							"INFO",
							"APP_REJECTED_GITHUB_AUTHORIZE",
							`A user tried to use github authorize, but the APP module is currently not ready.`
						);
						return redirectOnErr(res, "Something went wrong on our end. Please try again later.");
					}

					const params = [
						`client_id=${config.get("apis.github.client")}`,
						`redirect_uri=${redirectUri}`,
						`scope=user:email`
					].join("&");
					return res.redirect(`https://github.com/login/oauth/authorize?${params}`);
				});

				app.get("/auth/github/link", async (req, res) => {
					if (this.getStatus() !== "READY") {
						this.log(
							"INFO",
							"APP_REJECTED_GITHUB_AUTHORIZE",
							`A user tried to use github authorize, but the APP module is currently not ready.`
						);
						return redirectOnErr(res, "Something went wrong on our end. Please try again later.");
					}

					const params = [
						`client_id=${config.get("apis.github.client")}`,
						`redirect_uri=${redirectUri}`,
						`scope=user:email`,
						`state=${req.cookies[SIDname]}`
					].join("&");
					return res.redirect(`https://github.com/login/oauth/authorize?${params}`);
				});

				app.get("/auth/github/authorize/callback", async (req, res) => {
					if (this.getStatus() !== "READY") {
						this.log(
							"INFO",
							"APP_REJECTED_GITHUB_AUTHORIZE",
							`A user tried to use github authorize, but the APP module is currently not ready.`
						);

						return redirectOnErr(res, "Something went wrong on our end. Please try again later.");
					}

					const { code } = req.query;
					let accessToken;
					let body;
					let address;

					const { state } = req.query;

					const verificationToken = await UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 64 });

					return async.waterfall(
						[
							next => {
								if (req.query.error) return next(req.query.error_description);
								return next();
							},

							next => {
								oauth2.getOAuthAccessToken(code, { redirect_uri: redirectUri }, next);
							},

							(_accessToken, refreshToken, results, next) => {
								if (results.error) return next(results.error_description);

								accessToken = _accessToken;

								const options = {
									headers: {
										"User-Agent": "request",
										Authorization: `token ${accessToken}`
									}
								};

								return axios
									.get("https://api.github.com/user", options)
									.then(github => next(null, github))
									.catch(err => next(err));
							},

							(github, next) => {
								if (github.status !== 200) return next(github.data.message);

								if (state) {
									return async.waterfall(
										[
											next => {
												CacheModule.runJob("HGET", {
													table: "sessions",
													key: state
												})
													.then(session => next(null, session))
													.catch(next);
											},

											(session, next) => {
												if (!session) return next("Invalid session.");
												return userModel.findOne({ _id: session.userId }, next);
											},

											(user, next) => {
												if (!user) return next("User not found.");
												if (user.services.github && user.services.github.id)
													return next("Account already has GitHub linked.");

												return userModel.updateOne(
													{ _id: user._id },
													{
														$set: {
															"services.github": {
																id: github.data.id,
																access_token: accessToken
															}
														}
													},
													{ runValidators: true },
													err => {
														if (err) return next(err);
														return next(null, user, github.data);
													}
												);
											},

											user => {
												CacheModule.runJob("PUB", {
													channel: "user.linkGithub",
													value: user._id
												});

												CacheModule.runJob("PUB", {
													channel: "user.updated",
													value: { userId: user._id }
												});

												res.redirect(`${appUrl}/settings?tab=security`);
											}
										],
										next
									);
								}

								if (!github.data.id) return next("Something went wrong, no id.");

								return userModel.findOne({ "services.github.id": github.data.id }, (err, user) => {
									next(err, user, github.data);
								});
							},

							(user, _body, next) => {
								body = _body;

								if (user) {
									user.services.github.access_token = accessToken;
									return user.save(() => next(true, user._id));
								}

								return userModel.findOne(
									{
										username: new RegExp(`^${body.login}$`, "i")
									},
									(err, user) => next(err, user)
								);
							},

							(user, next) => {
								if (user) return next(`An account with that username already exists.`);

								return axios
									.get("https://api.github.com/user/emails", {
										headers: {
											"User-Agent": "request",
											Authorization: `token ${accessToken}`
										}
									})
									.then(res => next(null, res.data))
									.catch(err => next(err));
							},

							(body, next) => {
								if (!Array.isArray(body)) return next(body.message);

								body.forEach(email => {
									if (email.primary) address = email.email.toLowerCase();
								});

								return userModel.findOne({ "email.address": address }, next);
							},

							(user, next) => {
								UtilsModule.runJob("GENERATE_RANDOM_STRING", {
									length: 12
								}).then(_id => next(null, user, _id));
							},

							(user, _id, next) => {
								if (user) {
									if (Object.keys(JSON.parse(user.services.github)).length === 0)
										return next(
											`An account with that email address exists, but is not linked to GitHub.`
										);
									return next(`An account with that email address already exists.`);
								}

								return next(null, {
									_id,
									username: body.login,
									name: body.name,
									location: body.location,
									bio: body.bio,
									email: {
										address,
										verificationToken
									},
									services: {
										github: {
											id: body.id,
											access_token: accessToken
										}
									}
								});
							},

							// generate the url for gravatar avatar
							(user, next) => {
								UtilsModule.runJob("CREATE_GRAVATAR", {
									email: user.email.address
								}).then(url => {
									user.avatar = { type: "gravatar", url };
									next(null, user);
								});
							},

							// save the new user to the database
							(user, next) => {
								userModel.create(user, next);
							},

							(user, next) => {
								MailModule.runJob("GET_SCHEMA", {
									schemaName: "verifyEmail"
								}).then(verifyEmailSchema => {
									verifyEmailSchema(address, body.login, user.email.verificationToken, err => {
										next(err, user._id);
									});
								});
							},

							// create a liked songs playlist for the new user
							(userId, next) => {
								PlaylistsModule.runJob("CREATE_USER_PLAYLIST", {
									userId,
									displayName: "Liked Songs",
									type: "user-liked"
								})
									.then(likedSongsPlaylist => {
										next(null, likedSongsPlaylist, userId);
									})
									.catch(err => next(err));
							},

							// create a disliked songs playlist for the new user
							(likedSongsPlaylist, userId, next) => {
								PlaylistsModule.runJob("CREATE_USER_PLAYLIST", {
									userId,
									displayName: "Disliked Songs",
									type: "user-disliked"
								})
									.then(dislikedSongsPlaylist => {
										next(
											null,
											{
												likedSongsPlaylist,
												dislikedSongsPlaylist
											},
											userId
										);
									})
									.catch(err => next(err));
							},

							// associate liked + disliked songs playlist to the user object
							({ likedSongsPlaylist, dislikedSongsPlaylist }, userId, next) => {
								userModel.updateOne(
									{ _id: userId },
									{
										$set: {
											likedSongsPlaylist,
											dislikedSongsPlaylist
										}
									},
									{ runValidators: true },
									err => {
										if (err) return next(err);
										return next(null, userId);
									}
								);
							},

							// add the activity of account creation
							(userId, next) => {
								ActivitiesModule.runJob("ADD_ACTIVITY", {
									userId,
									type: "user__joined",
									payload: { message: "Welcome to Musare!" }
								});

								next(null, userId);
							}
						],
						async (err, userId) => {
							if (err && err !== true) {
								err = await UtilsModule.runJob("GET_ERROR", {
									error: err
								});

								this.log(
									"ERROR",
									"AUTH_GITHUB_AUTHORIZE_CALLBACK",
									`Failed to authorize with GitHub. "${err}"`
								);

								return redirectOnErr(res, err);
							}

							const sessionId = await UtilsModule.runJob("GUID", {});
							const sessionSchema = await CacheModule.runJob("GET_SCHEMA", {
								schemaName: "session"
							});

							return CacheModule.runJob("HSET", {
								table: "sessions",
								key: sessionId,
								value: sessionSchema(sessionId, userId)
							})
								.then(() => {
									const date = new Date();
									date.setTime(new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000);

									res.cookie(SIDname, sessionId, {
										expires: date,
										secure: config.get("url.secure"),
										path: "/",
										domain: config.get("url.host")
									});

									this.log(
										"INFO",
										"AUTH_GITHUB_AUTHORIZE_CALLBACK",
										`User "${userId}" successfully authorized with GitHub.`
									);

									res.redirect(appUrl);
								})
								.catch(err => redirectOnErr(res, err.message));
						}
					);
				});
			}

			app.get("/auth/verify_email", async (req, res) => {
				if (this.getStatus() !== "READY") {
					this.log(
						"INFO",
						"APP_REJECTED_VERIFY_EMAIL",
						`A user tried to use verify email, but the APP module is currently not ready.`
					);
					return redirectOnErr(res, "Something went wrong on our end. Please try again later.");
				}

				const { code } = req.query;

				return async.waterfall(
					[
						next => {
							if (!code) return next("Invalid code.");
							return next();
						},

						next => {
							userModel.findOne({ "email.verificationToken": code }, next);
						},

						(user, next) => {
							if (!user) return next("User not found.");
							if (user.email.verified) return next("This email is already verified.");

							return userModel.updateOne(
								{ "email.verificationToken": code },
								{
									$set: { "email.verified": true },
									$unset: { "email.verificationToken": "" }
								},
								{ runValidators: true },
								next
							);
						}
					],
					err => {
						if (err) {
							let error = "An error occurred.";

							if (typeof err === "string") error = err;
							else if (err.message) error = err.message;

							this.log("ERROR", "VERIFY_EMAIL", `Verifying email failed. "${error}"`);

							return res.json({
								status: "error",
								message: error
							});
						}

						this.log("INFO", "VERIFY_EMAIL", `Successfully verified email.`);

						return res.redirect(`${appUrl}?toast=Thank you for verifying your email`);
					}
				);
			});

			resolve();
		});
	}

	/**
	 * Returns the express server
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	SERVER() {
		return new Promise(resolve => {
			resolve(AppModule.server);
		});
	}

	/**
	 * Returns the app object
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	GET_APP() {
		return new Promise(resolve => {
			resolve({ app: AppModule.app });
		});
	}

	// EXAMPLE_JOB() {
	// 	return new Promise((resolve, reject) => {
	// 		if (true) resolve({});
	// 		else reject(new Error("Nothing changed."));
	// 	});
	// }
}

export default new _AppModule();