import jwt from './jwt';
import {makeId, renderJson} from '../src/helpers';
const dbConnection = require('./db');
const SQL = require('sql-template-strings');
import { ulid } from 'ulid';
import translations from "./translations";
const mailer = require('./mailer');
import authenticate from './authenticate';

const now = Math.floor(Date.now() / 1000);

const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-';

const normalise = s => s.trim().toLowerCase();

const saveAuthenticator = async (db, type, user, payload, validForMinutes = null) => {
    const id = ulid();
    await db.get(SQL`INSERT INTO authenticators (id, userId, type, payload, validUntil) VALUES (
        ${id},
        ${user ? user.id : null},
        ${type},
        ${JSON.stringify(payload)},
        ${validForMinutes ? (now + validForMinutes * 60) : null}
    )`);
    return id;
}

const findAuthenticator = async (db, id, type) => {
    const authenticator = await db.get(SQL`SELECT * FROM authenticators
        WHERE id = ${id}
        AND type = ${type}
        AND (validUntil IS NULL OR validUntil > ${now})
    `);

    if (authenticator) {
        authenticator.payload = JSON.parse(authenticator.payload);
    }

    return authenticator
}

const invalidateAuthenticator = async (db, id) => {
    await db.get(SQL`UPDATE authenticators
        SET validUntil = ${now}
        WHERE id = ${id}
    `);
}

const init = async (db, usernameOrEmail) => {
    let user = undefined;

    const isEmail = usernameOrEmail.indexOf('@') > -1;
    let isTest = false;

    if (process.env.NODE_ENV === 'development' && usernameOrEmail.endsWith('+')) {
        isTest = true;
        usernameOrEmail = usernameOrEmail.substring(0, usernameOrEmail.length - 1);
    }

    if (isEmail) {
        user = await db.get(SQL`SELECT * FROM users WHERE email = ${normalise(usernameOrEmail)}`);
    } else {
        user = await db.get(SQL`SELECT * FROM users WHERE lower(trim(username)) = ${normalise(usernameOrEmail)}`);
    }

    if (!user && !isEmail) {
        return {error: 'user.login.userNotFound'}
    }

    const payload = {
        username: isEmail ? (user ? user.username : null) : usernameOrEmail,
        email: isEmail ? normalise(usernameOrEmail) : user.email,
        code: isTest ? '999999' : makeId(6, '0123456789'),
    }

    const codeKey = await saveAuthenticator(db, 'email', user, payload, 15);

    if (!isTest) {
        mailer(
            payload.email,
            `[${translations.title}] ${translations.user.login.email.subject.replace('%code%', payload.code)}`,
            translations.user.login.email.content.replace('%code%', payload.code),
        )
    }

    return {
        token: jwt.sign({...payload, code: null, codeKey}, '15m'),
    };
}

const validate = async (db, user, code) => {
    if (!user || !user.codeKey) {
        return {error: 'user.tokenExpired'};
    }

    const authenticator = await findAuthenticator(db, user.codeKey, 'email');
    if (!authenticator) {
        return {error: 'user.tokenExpired'};
    }

    if (authenticator.payload.code !== normalise(code)) {
        return {error: 'user.code.invalid'};
    }

    await invalidateAuthenticator(db, authenticator);

    return await issueAuthentication(db, user);
}

const defaultUsername = async (db, email) => {
    const base = email.substring(0, email.indexOf('@'))
        .padEnd(4, '0')
        .substring(0, 12)
        .replace(new RegExp(`[^${USERNAME_CHARS}]`, 'g'), '_');

    let c = 0;
    while (true) {
        let proposal = base + (c || '');
        let dbUser = await db.get(SQL`SELECT id FROM users WHERE lower(trim(username)) = ${normalise(proposal)}`);
        if (!dbUser) {
            return proposal;
        }
        c++;
    }
}

const issueAuthentication = async (db, user) => {
    let dbUser = await db.get(SQL`SELECT * FROM users WHERE email = ${normalise(user.email)}`);
    if (!dbUser) {
        dbUser = {
            id: ulid(),
            username: await defaultUsername(db, user.email),
            email: normalise(user.email),
            roles: 'user',
            avatarSource: null,
        }
        await db.get(SQL`INSERT INTO users(id, username, email, roles, avatarSource)
            VALUES (${dbUser.id}, ${dbUser.username}, ${dbUser.email}, ${dbUser.roles}, ${dbUser.avatarSource})`)
    }

    return {
        token: jwt.sign({
            ...dbUser,
            authenticated: true,
        }),
    };
}

const changeUsername = async (db, user, username) => {
    if (username.length < 4 || username.length > 16 || !username.match(new RegExp(`^[${USERNAME_CHARS}]+$`))) {
        return { error: 'user.account.changeUsername.invalid' }
    }

    const dbUser = await db.get(SQL`SELECT * FROM users WHERE lower(trim(username)) = ${normalise(username)}`);
    if (dbUser) {
        return { error: 'user.account.changeUsername.taken' }
    }

    await db.get(SQL`UPDATE users SET username = ${username} WHERE email = ${normalise(user.email)}`);

    return await issueAuthentication(db, user);
}

const validateEmail = (email) => {
    const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    return re.test(String(email).toLowerCase());
}

const changeEmail = async (db, user, email, authId, code) => {
    if (!validateEmail(email)) {
        return { error: 'user.account.changeEmail.invalid' }
    }

    const dbUser = await db.get(SQL`SELECT * FROM users WHERE lower(trim(email)) = ${normalise(email)}`);
    if (dbUser) {
        return { error: 'user.account.changeEmail.taken' }
    }

    if (!authId) {
        const payload = {
            from: user.email,
            to: normalise(email),
            code: makeId(6, '0123456789'),
        };

        const authId = await saveAuthenticator(db, 'changeEmail', user, payload, 15);

        mailer(
            payload.to,
            `[${translations.title}] ${translations.user.login.email.subject.replace('%code%', payload.code)}`,
            translations.user.login.email.content.replace('%code%', payload.code),
        )

        return { authId };
    }

    const authenticator = await findAuthenticator(db, authId, 'changeEmail');
    if (!authenticator) {
        return {error: 'user.tokenExpired'};
    }

    if (authenticator.payload.code !== normalise(code)) {
        return {error: 'user.code.invalid'};
    }

    await invalidateAuthenticator(db, authenticator);

    await db.get(SQL`UPDATE users SET email = ${authenticator.payload.to} WHERE email = ${normalise(user.email)}`);
    user.email = authenticator.payload.to;

    return await issueAuthentication(db, user);
}

const removeAccount = async (db, user) => {
    const userId = (await db.get(SQL`SELECT id FROM users WHERE username = ${user.username}`)).id;
    if (!userId) {
        return false;
    }
    await db.get(SQL`DELETE FROM profiles WHERE userId = ${userId}`)
    await db.get(SQL`DELETE FROM authenticators WHERE userId = ${userId}`)
    await db.get(SQL`DELETE FROM users WHERE id = ${userId}`)
    return true;
}

export default async function (req, res, next) {
    const db = await dbConnection();
    const user = authenticate(req);

    if (req.method === 'POST' && req.url === '/init' && req.body.usernameOrEmail) {
        return renderJson(res, await init(db, req.body.usernameOrEmail));
    }

    if (req.method === 'POST' && req.url === '/validate' && req.body.code) {
        return renderJson(res, await validate(db, user, req.body.code));
    }

    if (req.method === 'POST' && req.url === '/change-username' && user && user.authenticated && req.body.username) {
        return renderJson(res, await changeUsername(db, user, req.body.username));
    }

    if (req.method === 'POST' && req.url === '/change-email' && user && user.authenticated && req.body.email) {
        return renderJson(res, await changeEmail(db, user, req.body.email, req.body.authId, req.body.code));
    }

    if (req.method === 'POST' && req.url === '/delete' && user && user.authenticated) {
        return renderJson(res, await removeAccount(db, user));
    }

    return renderJson(res, {error: 'Not found'}, 404);
}
