import assert from 'node:assert';
import AuditSource from '../auditsource.js';
import BoxError from '../boxerror.js';
import constants from '../constants.js';
import logger from '../logger.js';
import eventlog from '../eventlog.js';
import { HttpError } from '@cloudron/connect-lastmile';
import { HttpSuccess } from '@cloudron/connect-lastmile';
import oidcClients from '../oidcclients.js';
import safe from 'safetydance';
import speakeasy from 'speakeasy';
import tokens from '../tokens.js';
import users from '../users.js';

const { log } = logger('routes/cloudron');


async function login(req, res, next) {
    assert.strictEqual(typeof req.user, 'object');

    if ('type' in req.body && typeof req.body.type !== 'string') return next(new HttpError(400, 'type must be a string'));

    const type = req.body.type || oidcClients.ID_WEBADMIN;
    const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || null;
    const userAgent = req.headers['user-agent'] || '';

    const tokenTypeError = oidcClients.validateId(type);
    if (tokenTypeError) return next(new HttpError(400, tokenTypeError.message));

    const [error, token] = await safe(tokens.add({ clientId: type, identifier: req.user.id, allowedIpRanges: '', expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }));
    if (error) return next(new HttpError(500, error));

    const auditSource = AuditSource.fromRequest(req);
    await eventlog.add(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user), type, appId: oidcClients.ID_CLI });
    await safe(users.notifyLoginLocation(req.user, ip, userAgent, auditSource), { debug: log });

    next(new HttpSuccess(200, token));
}

async function logout(req, res) {
    assert.strictEqual(typeof req.token, 'object');

    await eventlog.add(eventlog.ACTION_USER_LOGOUT, AuditSource.fromRequest(req), { userId: req.user.id, user: users.removePrivateFields(req.user) });

    await safe(tokens.delByAccessToken(req.token.accessToken));
    res.redirect(301, '/');
}

async function passwordResetRequest(req, res, next) {
    if (!req.body.identifier || typeof req.body.identifier !== 'string') return next(new HttpError(400, 'A identifier must be non-empty string'));

    const [error] = await safe(users.sendPasswordResetByIdentifier(req.body.identifier, AuditSource.fromRequest(req)));
    if (error && !(error.reason === BoxError.NOT_FOUND || error.reason === BoxError.CONFLICT)) return next(BoxError.toHttpError(error));

    next(new HttpSuccess(202, {}));
}

async function passwordReset(req, res, next) {
    assert.strictEqual(typeof req.body, 'object');

    if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
    if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));

    const [getError, userObject] = await safe(users.getByResetToken(req.body.resetToken));
    if (getError) return next(new HttpError(401, 'Invalid resetToken'));
    if (!userObject) return next(new HttpError(401, 'Invalid resetToken'));

    if (userObject.totpEnabled) {
        if (typeof req.body.totpToken !== 'string') return next(new HttpError(401, 'A totpToken must be provided'));

        const verified = speakeasy.totp.verify({ secret: userObject.totpSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
        if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
    }

    // if you fix the duration here, the emails and UI have to be fixed as well
    if (Date.now() - userObject.resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
    if (!userObject.username) return next(new HttpError(409, 'No username set'));

    // setPassword clears the resetToken
    const [error] = await safe(users.setPassword(userObject, req.body.password, AuditSource.fromRequest(req)));
    if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
    if (error) return next(BoxError.toHttpError(error));

    const [addError, result] = await safe(tokens.add({ clientId: oidcClients.ID_WEBADMIN, identifier: userObject.id, allowedIpRanges: '', expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }));
    if (addError) return next(BoxError.toHttpError(addError));

    next(new HttpSuccess(202, { accessToken: result.accessToken }));
}

async function setupAccount(req, res, next) {
    assert.strictEqual(typeof req.body, 'object');

    if (!req.body.inviteToken || typeof req.body.inviteToken !== 'string') return next(new HttpError(400, 'inviteToken must be a non-empty string'));
    if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a non-empty string'));

    // only sent if profile is not locked
    if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string'));
    if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string'));

    const [error, userObject] = await safe(users.getByInviteToken(req.body.inviteToken));
    if (error) return next(new HttpError(401, 'Invalid inviteToken'));
    if (!userObject) return next(new HttpError(401, 'Invalid inviteToken'));

    const [setupAccountError, accessToken] = await safe(users.setupAccount(userObject, req.body, AuditSource.fromRequest(req)));
    if (setupAccountError) return next(BoxError.toHttpError(setupAccountError));

    next(new HttpSuccess(201, { accessToken }));
}

export default {
    login,
    logout,
    passwordResetRequest,
    passwordReset,
    setupAccount,
};
