import * as express from "express";
import * as bodyparser from "body-parser";

import { Db } from "mongodb";

import { authRoute } from "../security/token";

import { userFromSession } from "../utils/user";

import { IUser, IUserDBModel } from "../models/user";
import { ISM2Metadata } from "../models/review";
import { LRequest } from "../types/express";

const userRouter = express.Router();
userRouter.use(authRoute);
userRouter.use(bodyparser.json());

// Return the user object if the user is still authenticated
userRouter.get("/me", async (req: LRequest, res) => {
    const { db, token } = req;

    const session = await db.collection("sessions").findOne({ token, });
    if (session !== null) {
        const user = await db.collection("users").findOne({ username: session.username });

        // Copy and strip unneeded attributes
        let copy = Object.assign({}, user, {
            sessionToken: token,
        });
        delete copy._id;
        delete copy.hash;
        delete copy.salt;
        delete copy.lastReview;
        delete copy.lastLevel;
        delete copy.vocabMetadata;

        res.send({
            error: "0",
            data: copy,
        });
    } else {
        res.send({
            error: "404",
            data: {},
        });
    }
});

// Removes the user's session
userRouter.get("/logout", async (req: LRequest, res) => {
    // Try to remove the session
    const { db, token } = req;
    await db.collection("sessions").findOneAndDelete({ token, });

    res.send({
        error: "0",
        data: {},
    });
});

// TODO: This should be shared with the frontend, to remove code duplication
export enum VocabType {
    NOMEN = 0,
    VERB = 1,
    ADJEKTIV = 2,
    ADVERB = 3,
};

// Gets the user's review queue
userRouter.get("/queue", async (req: LRequest, res) => {
    // TODO: if (user)
    const { token, db } = req;
    let user = Object.assign({}, await userFromSession(token, db));

    const sm2 = user.vocabMetadata;
    const data = Object.keys(sm2).map(id => Object.assign({}, sm2[parseInt(id)], { id, }));
    const sorted = data.sort((a: any, b: any) => {
        if (a.nextDueDate > b.nextDueDate)
            return 1;
        if (a.nextDueDate < b.nextDueDate)
            return -1;

        return 0;
    }).slice(0, 20)
        .map((el: any) => {
            return parseInt(el.id);
        });

    // Fetch all vocab ids from the vocabulary collection
    const vocabRaw = await db.collection("vocabulary").find({
        id: { $in: sorted },
    }, {
            // TODO: Make this configurable?
            sort: {

            },
            limit: 20,
        }).toArray();

    // Remove unneeded data
    const vocab = vocabRaw.map(el => {
        let tmp = Object.assign({}, el);
        delete tmp._id;

        return tmp;
    });

    res.send({
        error: "0",
        data: {
            queue: vocab,
        },
    });
});

// Get ot set the last review results
userRouter.get("/lastReview", async (req: LRequest, res) => {
    // TODO: if(user)
    const { token, db } = req;
    const user = await userFromSession(token, db);

    res.send({
        error: "0",
        data: user.lastReview,
    });
});

function dateInNDays(n: number): number {
    let today = new Date();
    today.setDate(today.getDate() + n);
    return Date.parse(today.toString());
}

userRouter.post("/lastReview", async (req: LRequest, res) => {
    // Check if we get the needed data
    if (!req.body || !("meta" in req.body) || !("sm2" in req.body) || !("delta" in req.body)) {
        res.send({
            error: "400",
            data: {
                msg: "Last review state or SM2 data not specified!",
            },
        });
        return;
    }

    const { token, db } = req;
    let user = Object.assign({}, await userFromSession(token, db));
    if (!user) {
        res.send({
            error: "404",
            data: {
                msg: "Failed to find user with the specified session!",
            },
        });
        return;
    }

    user.score += parseInt(req.body.delta);

    Object.keys(req.body.sm2).forEach((id: string) => {
        const vocabId = parseInt(id);
        const correct: boolean = req.body.sm2[id];
        let vocab_sm2: ISM2Metadata = Object.assign({},
            user.vocabMetadata[vocabId]);

        // TODO: Tuning?
        // TODO: Move into another module
        const perf = correct ? 3 : 1;
        vocab_sm2.easiness += -0.8 + 0.28 * perf + 0.02 * Math.pow(perf, 2);
        // Update the consecutive correct answers and the due date
        if (correct) {
            vocab_sm2.consecutiveCorrectAnswers += 1;
            vocab_sm2.nextDueDate = dateInNDays(6 * Math.pow(vocab_sm2.easiness, vocab_sm2.consecutiveCorrectAnswers - 1));
        } else {
            vocab_sm2.consecutiveCorrectAnswers = 0;
            vocab_sm2.nextDueDate = dateInNDays(1);
        }

        user.vocabMetadata[vocabId] = vocab_sm2;
    });

    // Update the last review
    user.lastReview = req.body.meta;

    // TODO: Error handling
    await db.collection("users").updateOne({
        username: user.username,
    }, {
            $set: {
                lastReview: user.lastReview,
                vocabMetadata: user.vocabMetadata,
                score: user.score,
            },
        });

    res.send({
        error: "0",
        data: {},
    });
});

// Returns the next level (level + 1) or the current level, if no higher level
// can be found
async function getNextLevel(token: string, db: Db): Promise<any> {
    // TODO: if(user)
    const user = await userFromSession(token, db);
    const { lastLevel } = user;

    // Try to find a level, which level is lastLevel + 1
    const level = await db.collection("levels").findOne({
        level: lastLevel + 1,
    });

    if (level) {
        return level;
    } else {
        // TODO: Send different data, so that the Client can say "Hey, no more levels"
        return await db.collection("levels").findOne({
            level: lastLevel
        });
    }
}

// Get the next level
userRouter.get("/nextLevel", async (req: LRequest, res) => {
    const level = await getNextLevel(req.token, req.db);
    res.send({
        error: "0",
        data: level,
    });
});

// Mark a level as done
userRouter.post("/level/:id", async (req: LRequest, res) => {
    // Is everything specified?
    if (!req.params || !req.params.id) {
        res.send({
            error: "400",
            data: {
                msg: "No level ID specified",
            },
        });
        return;
    }

    const id = parseInt(req.params.id);
    if (id === NaN) {
        res.send({
            error: "400",
            data: {
                msg: "Invalid level ID",
            },
        });
        return;
    }

    // TODO: if (user)
    const { token, db } = req;
    const user = await userFromSession(token, db);

    if (id in user.levels) {
        // Nothing
    } else {
        // The level is new to the user
        // Is the new level higher than the "highest" already completed level?
        let update = {
            levels: user.levels.concat(id),
        };
        if (id > Math.max(...user.levels)) {
            // TODO: if (!level)
            const level = await db.collection("levels").findOne({
                level: id,
            });

            // Convert the level's vocabulary to SM2 metadata
            let sm2: { [id: number]: ISM2Metadata } = {};
            level.vocab.forEach((id: number) => {
                sm2[id] = {
                    easiness: 1.3,
                    consecutiveCorrectAnswers: 0,
                    nextDueDate: Date.parse((new Date()).toString()),
                };
            });
            const newVocabMetadata = Object.assign({}, user.vocabMetadata, sm2);

            // Also update the lastLevel attribute
            Object.assign(update, {
                lastLevel: id,
                vocabMetadata: newVocabMetadata,
            });
        }
        await db.collection("users").updateOne({
            username: user.username,
        }, {
                $set: update,
            });
    }

    res.send({
        error: "200",
        data: {},
    });
});

// Get the data needed for the dashboard 
userRouter.get("/dashboard", async (req: LRequest, res) => {
    const { db, token } = req;

    // Get the user
    // TODO: if (user)
    const user = await userFromSession(token, db);
    const { classId } = user;

    // Fetch the top ten of the class
    const rawTopTen = await db.collection("users").find({
        classId,
    }, {
            sort: {
                score: -1,
            },
            limit: 10,
        }).toArray();

    let nr = 1;
    const topTen = rawTopTen.map((user: IUser) => {
        return {
            username: user.username,
            score: user.score,
            // TODO: Calculate on the client?
            level: 1,
            nr: nr++,
        };
    });

    const nextLevel = await getNextLevel(token, db);
    res.send({
        error: "200",
        data: {
            nextLevel,
            topTen,
            lastReview: user.lastReview,
        },
    });
});

userRouter.post("/showWelcome", async (req: LRequest, res) => {
    const { db, token } = req;

    // Are all arguments specified?
    if (!req.body || !("state" in req.body)) {
        res.send({
            error: "400",
            data: {
                msg: "State not specified",
            },
        });
        return;
    }

    // Find the user
    // TODO: if (user)
    const user = await userFromSession(token, db);

    await db.collection("users").updateOne({
        username: user.username,
    }, {
            $set: {
                showWelcome: req.body.state,
            },
        });
    res.send({
        error: "200",
        data: {},
    });
});

userRouter.post("/score", async (req: LRequest, res) => {
    const { token, db } = req;

    // Are all arguments specified
    if (!req.body || !("score" in req.body)) {
        res.send({
            error: "400",
            data: {
                msg: "No score specified!",
            },
        });
        return;
    }

    // TODO: if (user)
    const user = await userFromSession(token, db);
    // Update the score
    db.collection("users").updateOne({
        username: user.username,
    }, {
            $set: {
                score: req.body.score,
            },
        });
});

export default userRouter;
