import apps from '../apps.js';
import assert from 'node:assert';
import BoxError from '../boxerror.js';
import logger from '../logger.js';
import { HttpError } from '@cloudron/connect-lastmile';
import safe from 'safetydance';
import tokens from '../tokens.js';
import users from '../users.js';

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


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

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

    const { username, password, totpToken } = req.body;

    const verifyFunc = username.indexOf('@') === -1 ? users.verifyWithUsername : users.verifyWithEmail;

    const [error, user] =  await safe(verifyFunc(username, password, users.AP_WEBADMIN, { totpToken, skipTotpCheck: false }));
    if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, error.message));
    if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
    if (error) return next(new HttpError(500, error));
    if (!user) return next(new HttpError(401, 'Unauthorized'));

    req.user = user;

    next();
}

async function tokenAuth(req, res, next) {
    let accessToken;

    // this determines the priority
    if (req.body?.access_token) accessToken = req.body.access_token;
    if (typeof req.query?.access_token === 'string') accessToken = req.query.access_token;
    if (req.headers?.authorization) {
        const parts = req.headers.authorization.split(' ');
        if (parts.length == 2) {
            const [scheme, credentials] = parts;

            if (/^Bearer$/i.test(scheme)) accessToken = credentials;
        }
    }

    if (!accessToken) return next(new HttpError(401, 'Token required'));

    const token = await tokens.getByAccessToken(accessToken);
    if (!token) return next(new HttpError(401, 'No such token'));

    const user = await users.get(token.identifier);
    if (!user) return next(new HttpError(401, 'User not found'));
    if (!user.active) {
        log(`tokenAuth: ${user.username || user.id} is not active`);
        return next(new HttpError(401, 'User not active'));
    }

    const remoteAddress = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
    if (!tokens.isIpAllowedSync(token, remoteAddress)) return next(new HttpError(401, 'Token not allowed from this IP'));

    await tokens.update(token.id, { lastUsedTime: new Date() });

    req.token = token;
    req.user = user;

    next();
}

function authorize(requiredRole) {
    assert.strictEqual(typeof requiredRole, 'string');

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

        if (users.compareRoles(req.user.role, requiredRole) < 0) return next(new HttpError(403, `role '${requiredRole}' is required but user has only '${req.user.role}'`));
        if (!tokens.hasScope(req.token, req.method, req.path)) return next(new HttpError(403, 'access token does not have this scope'));

        next();
    };
}

async function authorizeOperator(req, res, next) {
    assert.strictEqual(typeof req.params.id, 'string');
    assert.strictEqual(typeof req.user, 'object');
    assert.strictEqual(typeof req.resources.app, 'object');
    assert.strictEqual(typeof req.token, 'object');

    if (!tokens.hasScope(req.token, req.method, req.path)) return next(new HttpError(403, 'access token does not have this scope'));
    if (apps.isOperator(req.resources.app, req.user)) return next();

    return next(new HttpError(403, 'user is not an operator'));
}

export default {
    passwordAuth,
    tokenAuth,

    authorize,
    authorizeOperator,
};
